Java Module: Services
Das Service-Loader-Muster im Java-Modulsystem mit den Direktiven uses und provides erklärt.
Ein zentrales Ziel von Modulen ist die Entkopplung: Code, der eine Funktion nutzt, sollte nicht die Klasse benennen, die sie implementiert. Das Java Platform Module System (JPMS) macht daraus ein erstklassiges Feature mit zwei Direktiven — uses und provides — die zur Laufzeit durch ServiceLoader miteinander verbunden werden. Das ist der modulare Ersatz für die alte Classpath-Konvention, bei der eine META-INF/services-Datei in ein JAR abgelegt wurde.
Dieses Kapitel setzt voraus, dass Sie bereits wissen, wie man einen module-info.java-Deskriptor schreibt; hier fügen wir die Service-Direktiven hinzu.
Die drei Rollen
Ein Service besteht aus drei Teilen, idealerweise in drei verschiedenen Modulen:
- Das Service-Interface (oder die abstrakte Klasse) — der Vertrag, z. B.
PricingRule. Es befindet sich in einem Modul, das es exportiert. - Der Konsument — Code, der nach Implementierungen fragt. Sein Modul deklariert
uses com.acme.PricingRule;und ruftServiceLoader.load(PricingRule.class)auf. - Ein oder mehrere Anbieter — Implementierungsmodule. Jedes deklariert
provides com.acme.PricingRule with com.acme.impl.StandardPricing;.
Der Konsument importiert StandardPricing nie. Er kennt nur das Interface. Fügen Sie ein neues Anbietermodul zum Modulpfad hinzu und der Konsument erkennt es automatisch — ohne Neukompilierung, ohne Codeänderung.
Die Direktiven 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 teilt dem Resolver mit, dass der Konsument diesen Service nachschlagen wird, sodass das Modul ServiceLoader.load aufrufen darf. provides … with … registriert eine Implementierung. Die Anbieterklasse muss entweder einen öffentlichen No-Arg-Konstruktor haben oder eine öffentliche statische provider()-Methode, die eine Instanz zurückgibt.
Konsumieren mit 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 ist lazy — jeder Anbieter wird nur dann instanziiert, wenn der Iterator ihn erreicht — und er cached Instanzen. Er implementiert Iterable, sodass eine For-each-Schleife alle registrierten Anbieter durchläuft.
Ein praktisches Beispiel: ServiceLoader über einen echten JDK-Service
Das JDK liefert bereits einen Service, den man laden kann, ohne drei Module zu erstellen: java.util.spi.ToolProvider. Der Compiler (javac), das JAR-Tool (jar) und andere sind als Anbieter dieses Interfaces innerhalb von JDK-Modulen registriert. Dieses Programm lädt sie über ServiceLoader — genau der oben gezeigte Konsumenten-Code, ausgeführt gegen einen Service, der bereits verdrahtet ist.
Was man aus dem Lauf mitnehmen kann:
- Die For-each-Schleife durchlief jeden
ToolProvider, den die Laufzeit registriert hat (typischerweisejavac,jar,javadoc, …), ohne dass das Programm jemals eine dieser Implementierungsklassen importiert hätte. Das ist der eigentliche Zweck von Services: Der Konsument hängt vonToolProvider, dem Interface, ab und entdeckt konkrete Anbieter erst zur Laufzeit. - Jeder Anbieter gab einen anderen Implementierungsklassennamen aus, obwohl sie alle ein Interface teilen. Die JDK-Module haben
provides java.util.spi.ToolProvider with …in ihren Deskriptoren deklariert;ServiceLoaderhat sie alle gesammelt. Ein weiteres Anbietermodul hinzuzufügen würde es in dieser gleichen Schleife erscheinen lassen, ohne hier etwas zu ändern. ToolProvider.findFirst("javac")gab einOptionalzurück und der Code behandelte beide Fälle. Service-Lookups sind von Natur aus „könnte fehlen" — eine minimale Laufzeit könnte keine Tool-Provider liefern — daher zwingt die API Sie dazu, den leeren Fall einzuplanen, anstatt das Vorhandensein einer Implementierung vorauszusetzen.- Das Ausführen von
javac --versionüber den geladenen Anbieter beweist, dass das Objekt voll funktionsfähig ist und rein über den Service-Vertrag erreichbar war. Der Konsument rief echtes Verhalten auf, ohne eine Compile-Time-Abhängigkeit von den Klassen des Compilers zu haben. ServiceLoaderinstanziiert lazy und nur das, worüber man iteriert; in einem echten Drei-Modul-Setup müsste diemodule-infodes Konsumentenuses java.util.spi.ToolProvider;deklarieren, damit der Aufruf erlaubt ist. Die eigenen Module des JDK deklarieren dies bereits, weshalb dieses Programm unverändert läuft.
Warum Services verwenden
- Plugin-Architekturen — legen Sie ein Anbieter-JAR auf den Modulpfad, um eine Anwendung zu erweitern.
- Optionale Implementierungen — wählen Sie zur Laufzeit ein SSL-, Logging- oder Datenbank-Treiber-Modul anhand des vorhandenen Anbietermoduls.
- Umkehrung der Abhängigkeit — das übergeordnete Konsumentenmodul hängt von einem Interface-Modul ab, nie von den untergeordneten Anbietern, sodass alle Abhängigkeitspfeile auf den stabilen Vertrag zeigen.
Das ist derselbe Mechanismus, den das JDK für DriverManager, Charset-Anbieter und die java.time-Chronologien verwendet. Das abschließende Kapitel dieses Teils, Migrating to Java Modules, bringt alles zusammen: wie man eine bestehende Classpath-Anwendung Schritt für Schritt auf den Modulpfad migriert.