W3docs

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:

  1. Das Service-Interface (oder die abstrakte Klasse) — der Vertrag, z. B. PricingRule. Es befindet sich in einem Modul, das es exportiert.
  2. Der Konsument — Code, der nach Implementierungen fragt. Sein Modul deklariert uses com.acme.PricingRule; und ruft ServiceLoader.load(PricingRule.class) auf.
  3. 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.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen kann:

  • Die For-each-Schleife durchlief jeden ToolProvider, den die Laufzeit registriert hat (typischerweise javac, jar, javadoc, …), ohne dass das Programm jemals eine dieser Implementierungsklassen importiert hätte. Das ist der eigentliche Zweck von Services: Der Konsument hängt von ToolProvider, 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; ServiceLoader hat 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 ein Optional zurü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.
  • ServiceLoader instanziiert lazy und nur das, worüber man iteriert; in einem echten Drei-Modul-Setup müsste die module-info des Konsumenten uses 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.

Übungen

Übung
Ein Konsumentenmodul ruft 'ServiceLoader.load(PaymentGateway.class)' auf, aber die Schleife findet zur Laufzeit keine Anbieter, obwohl ein Anbietermodul mit 'provides PaymentGateway with StripeGateway' auf dem Modulpfad liegt. Das Konsumentenmodul kompiliert und startet einwandfrei. Was ist die wahrscheinlichste Ursache?
Ein Konsumentenmodul ruft 'ServiceLoader.load(PaymentGateway.class)' auf, aber die Schleife findet zur Laufzeit keine Anbieter, obwohl ein Anbietermodul mit 'provides PaymentGateway with StripeGateway' auf dem Modulpfad liegt. Das Konsumentenmodul kompiliert und startet einwandfrei. Was ist die wahrscheinlichste Ursache?
Was this page helpful?