W3docs

Java Modules: Servizi

Pattern service-loader nel sistema di moduli Java con le direttive uses e provides, gestite da ServiceLoader a runtime.

Un obiettivo centrale dei moduli è il disaccoppiamento: il codice che utilizza una funzionalità non dovrebbe nominare la classe che la implementa. Il Java Platform Module System (JPMS) trasforma questo principio in una funzionalità di primo livello tramite due direttive — uses e provides — collegate a runtime da ServiceLoader. Questo è il sostituto modulare della vecchia convenzione del classpath che prevedeva l'inserimento di un file META-INF/services in un JAR.

Questo capitolo presuppone che tu sappia già come scrivere un descrittore module-info.java; qui aggiungiamo le direttive di servizio.

I tre ruoli

Un servizio è composto da tre parti, idealmente in tre moduli distinti:

  1. L'interfaccia del servizio (o classe astratta) — il contratto, ad es. PricingRule. Risiede in un modulo che la esporta.
  2. Il consumatore — codice che richiede le implementazioni. Il suo modulo dichiara uses com.acme.PricingRule; e chiama ServiceLoader.load(PricingRule.class).
  3. Uno o più provider — moduli di implementazione. Ciascuno dichiara provides com.acme.PricingRule with com.acme.impl.StandardPricing;.

Il consumatore non importa mai StandardPricing. Conosce solo l'interfaccia. Aggiungendo un nuovo modulo provider al module path, il consumatore lo rileva automaticamente — senza ricompilazione, senza modifiche al codice.

Le direttive in module-info.java

// module com.acme.api
module com.acme.api {
    exports com.acme;            // export the PricingRule interface
}

// module com.acme.app (the consumer)
module com.acme.app {
    requires com.acme.api;
    uses com.acme.PricingRule;   // "I will ServiceLoader.load this"
}

// module com.acme.standard (a provider)
module com.acme.standard {
    requires com.acme.api;
    provides com.acme.PricingRule
        with com.acme.standard.StandardPricing;
}

uses indica al resolver che il consumatore cercherà quel servizio, quindi al modulo è consentito chiamare ServiceLoader.load. provides … with … registra un'implementazione. La classe provider deve avere un costruttore pubblico senza argomenti oppure un metodo statico pubblico provider() che restituisce un'istanza.

Consumo con ServiceLoader

ServiceLoader<PricingRule> loader = ServiceLoader.load(PricingRule.class);
for (PricingRule rule : loader) {
    System.out.println(rule.describe());
}
// or pick the first available
PricingRule rule = ServiceLoader.load(PricingRule.class)
    .findFirst()
    .orElseThrow();

ServiceLoader è lazy — ogni provider viene istanziato solo quando l'iteratore lo raggiunge — e memorizza nella cache le istanze. Implementa Iterable, quindi un ciclo for-each percorre tutti i provider registrati.

Un esempio pratico: ServiceLoader su un servizio JDK reale

Il JDK include già un servizio che puoi caricare senza dover costruire tre moduli: java.util.spi.ToolProvider. Il compilatore (javac), lo strumento JAR (jar) e altri sono registrati come provider di quell'interfaccia nei moduli JDK. Questo programma li carica tramite ServiceLoader — esattamente il codice del consumatore visto sopra, contro un servizio già configurato.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • Il ciclo for-each ha percorso ogni ToolProvider registrato dal runtime (in genere javac, jar, javadoc, …) senza che il programma importasse mai una di quelle classi di implementazione. Questo è il punto fondamentale dei servizi: il consumatore dipende da ToolProvider, l'interfaccia, e scopre i provider concreti a runtime.
  • Ogni provider ha stampato un nome di classe di implementazione diverso, pur condividendo un'unica interfaccia. I moduli JDK hanno dichiarato provides java.util.spi.ToolProvider with … nei loro descrittori; ServiceLoader li ha raccolti tutti. Aggiungere un altro modulo provider lo farebbe comparire in questo stesso ciclo senza alcuna modifica qui.
  • ToolProvider.findFirst("javac") ha restituito un Optional e il codice ha gestito entrambi i rami. Le ricerche di servizi sono intrinsecamente "potrebbe essere assente" — un runtime minimale potrebbe non includere alcun tool provider — quindi l'API ti obbliga a pianificare il caso vuoto invece di dare per scontata un'implementazione.
  • Eseguire javac --version tramite il provider caricato dimostra che l'oggetto è pienamente funzionante, raggiunto unicamente attraverso il contratto del servizio. Il consumatore ha invocato un comportamento reale senza una dipendenza a tempo di compilazione sulle classi del compilatore.
  • ServiceLoader istanzia in modo lazy e solo ciò che si itera; in un setup reale a tre moduli il module-info del consumatore avrebbe bisogno di uses java.util.spi.ToolProvider; per consentire la chiamata. I moduli propri del JDK lo dichiarano già, motivo per cui questo codice funziona senza modifiche.

Perché usare i servizi

  • Architetture a plugin — rilascia un JAR provider sul module path per estendere un'applicazione.
  • Implementazioni opzionali — scegli un driver SSL, di logging o di database a runtime in base al modulo provider presente.
  • Inversione della dipendenza — il modulo consumatore di alto livello dipende da un modulo interfaccia, mai dai provider di basso livello, quindi le frecce delle dipendenze puntano tutte verso il contratto stabile.

Questo è lo stesso meccanismo che il JDK utilizza per DriverManager, i provider di charset e le cronologie di java.time. Il capitolo finale di questa parte, Migrazione ai moduli Java, mette insieme tutto: come spostare un'applicazione esistente su classpath verso il module path un passo alla volta.

Esercitazione

Pratica
Un modulo consumatore chiama 'ServiceLoader.load(PaymentGateway.class)' ma il ciclo non trova alcun provider a runtime, anche se un modulo provider che dichiara 'provides PaymentGateway with StripeGateway' è sul module path. Il modulo consumatore compila e si avvia correttamente. Qual è la causa più probabile?
Un modulo consumatore chiama 'ServiceLoader.load(PaymentGateway.class)' ma il ciclo non trova alcun provider a runtime, anche se un modulo provider che dichiara 'provides PaymentGateway with StripeGateway' è sul module path. Il modulo consumatore compila e si avvia correttamente. Qual è la causa più probabile?
Was this page helpful?