Lose Kopplung mit Java 9

Java 9 Bootcamp Logo

Im ersten und zweiten Teil dieser Serie habe ich gezeigt, wie man Module erstellt und Abhängigkeiten zwischen ihnen setzt. In diesem Teil geht es darum, wie man lose gekoppelte Anwendungen baut. Dazu verwenden wir den für Java 9 erweiterten ServiceLoader und ein Service Provider Interface.

Kompilieren von mehreren Modulen

Damit wir nicht jedesmal alle Java Klassen manuell angeben müssen, verwenden wir ab jetzt zum Kompilieren diesen Aufruf:

    javac -d modules --module-source-path src $(find src -name "*.java")

ServiceLoader

Seit Java 6 gibt es die Möglichkeit Services mit Hilfe der Klasse ServiceLoader zu laden. Ein Service wird dabei durch ein Service Provider Interface definiert. Das kann ein Interface, eine anbstrakte Klasse, oder eine ganz normale Klasse sein. Die konkrete Implementierung (Provider) ist dann eine Klasse die das SPI implementiert oder erweitert und einen parameterlosen Konstruktor hat. In Java 6 wird dieser Provider dann über einer einfachen Text-Datei im META-INF/services Verzeichnis des JAR registriert. Der Name der Datei ist der voll qualifizierte Name des SPI. Der Inhalt der Datei ist der voll qualifizierte Name der Implementierung. Der Consumer des Service kann dann über die Klasse ServiceLoader eine Instanz der Implementierung erhalten.

Damit ergeben sich folgende Abhängigkeiten:

SPI

Der Vorteil dieses Modells ist, dass der Provider dem Consumer zur Compile-Zeit nicht bekannt sein muss. Neue Service-Implementierungen können jederzeit nachträglich hinzugefügt, ergänzt oder ersetzt werden. Wenn das JAR mit der Implementierung auf dem Klassenpfad der Anwendung ist, kann der ServiceLoader die Implementerung finden. Dieses Konzept passt natürlich sehr gut zu einem Modulsystem. Deshalb ist in Java 9 ist dieser Mechanismus weiter entwickelt und an Jigsaw angepasst worden. Sehen wir uns an, wie das funktioniert:

Ein modularer Greeter

Um unseren Greeter modular zu machen definieren wir als erstes eine Service Provider Interface (SPI). Unser SPI ist ein ganz normales Java Interface und heisst "GreeterService":

package de.eppleton.greetings.spi;

public interface GreeterService{
    public void greet(String who);
}

Damit wir dieses Interface in einem anderen Modul implementieren können müssen wir es exportieren. Wir ändern also erneut die module-info und exportieren die Package de.eppleton.greetings.spi. Zusätzlich geben wir mit dem keyword "uses" bekannt, dass wir das Service Provider Interface "GreeterService" nutzen möchten:

 module de.eppleton.greetings {
    exports de.eppleton.greetings;
    exports de.eppleton.greetings.spi;
    uses de.eppleton.greetings.spi.GreeterService;
 }

Die Verwendung des Keywords "uses" geht immer einher mit der Verwendung des Keywords "provides". Ein anderes Modul kann damit bekannt geben, dass es eine Implementierung des Interfaces bereitstellt.

Die Implementierung des Service

Beginnen wir mit der Implementierung des Service. Damit sich das Ergebnis sichtbar von der alten Version unterscheidet, verwenden wir die Swing JOptionPane um unsere User zu begrüßen. Unser Modul nennen wir "de.eppleton.swinggreeter", damit ergibt sich dieser Pfad:

src/de.eppleton.swinggreeter/de/eppleton/swinggreeter/SwingGreeterService.java

package de.eppleton.swinggreeter;

import javax.swing.JOptionPane;
import de.eppleton.greetings.spi.GreeterService;

public class SwingGreeterService implements GreeterService{

    public void greet(String who){
        JOptionPane.showMessageDialog(null, "Greetings "+who, "Hello "+who+"!", JOptionPane.INFORMATION_MESSAGE);
        }

}

Die zugehörige module-info.java sieht so aus:

module de.eppleton.swinggreeter{
    requires de.eppleton.greetings;
    provides de.eppleton.greetings.spi.GreeterService
        with de.eppleton.swinggreeter.SwingGreeterService;
}

Mit dem Schlüsselwort "provides" geben wir an, welcher Service implementiert wird. Mit "with" legen wir fest, wo die Implementierung liegt. Vor Java 9 war wie schon erwähnt für die Registrierung eines Services eine "provider-configuration"-Datei im META-INF/services Verzeichnis nötig, damit der ServiceLoader einen Service auffinden kann. Das entfällt in Java 9.

Damit haben wir folgenden Aufbau:

SPI

Vorsicht: Java selbst ist auch modular!

Wenn wir versuchen zu kompilieren, erhalten wir eine Fehlermeldung:

javac -d modules --module-source-path src $(find src -name "*.java")
src/de.eppleton.swinggreeter/de/eppleton/swinggreeter/SwingGreeterService.java:3: error: package javax.swing is not visible
import javax.swing.JOptionPane;
            ^
  (package javax.swing is declared in module java.desktop, but module de.eppleton.swinggreeter does not read it)
1 error

Das liegt daran, dass die Java Plattform selbst ebenfalls in Module aufgeteilt wurde. Bisher haben wir nur die Klassen des java.base-Moduls verwendet. Diese müssen nicht explizit als Abhängigkeit deklariert werden. JOptionPane gehört jedoch zu Swing und damit zum Modul "java.desktop". Das ist nicht automatisch mit dabei. Wir müssen also die module-info.java ergänzen:

module de.eppleton.swinggreeter{
    requires de.eppleton.greetings;
    requires java.desktop;
    provides de.eppleton.greetings.spi.GreeterService
        with de.eppleton.swinggreeter.SwingGreeterService;
}

Nun kompiliert das Projekt. Jetzt müssen wir lediglich noch Greeter.java so anpassen, dass unser SwingGreeterService verwendet wird.

Der ServiceLoader

Bei der Verwendung der Klasse ServiceLoader hat sich im Vergleich zu früheren Versionen nichts geändert. Wir passen also unsere Greeter.java Klasse folgendermassen an:

src/de.eppleton.hello/de/eppleton/greetings/Greeter.java

package de.eppleton.greetings;
import java.util.ServiceLoader;
import de.eppleton.greetings.spi.GreeterService;
public class Greeter {
    public static void greet(String who) {
        ServiceLoader<GreeterService> services = ServiceLoader.load(GreeterService.class);
        for (GreeterService service : services){
            service.greet(who);
        }
    }
}

Nun können wir unsere Module kompilieren und die Anwendung starten:

   java --module-path modules -m de.eppleton.hello/de.eppleton.hello.HelloWorld 

Die Anwendung startet und zeigt die JOptionPane:

Screenshot Hello Modular World

Lose Kopplung

Und wo ist nun die lose Kopplung? Am Anfang dieses Teils habe ich gesagt, dass die Anwendung lose gekoppelt ist. Das bedeutet, dass die Anzahl der direkten Abhängigkeiten so weit wie möglich reduziert wird. Bei uns ist das im greetings-Modul umgesetzt. Es verwendet die Implementierung aus dem swinggreeter-Modul, hat aber keine Abhängigkeit darauf. In der module-info.java ist daher kein "requires" zu finden, sondern nur "exports":

 module de.eppleton.greetings {
    exports de.eppleton.greetings;
    exports de.eppleton.greetings.spi;
    uses de.eppleton.greetings.spi.GreeterService;
 }

Zusammenfassung

In den ersten beiden Teilen dieser Serie haben wir gelernt, wie Module gebaut werden und wie man Modul-Abhängigkeiten festlegt. In diesem Teil habe ich gezeigt, wie man die Anzahl der direkten Abhängigkeiten mit Hilfe des ServiceLoaders verringern kann. Wir haben auch gesehen, dass die Java Plattform selbst modularisiert wurde, und wie man hier die Abhängigkeiten setzt.

Im nächsten Teil geht es darum, wie man Module verpackt.

Besuchen Sie unsere Workshops zum Thema Java 9 um mehr über dieses und weitere neue Features von Java 9 zu erfahren: