Service Provider & Consumer - lose gekoppelt ein starkes Team
Dass spezifische Funktionalitäten in unterschiedliche Services gebündelt sind, ist ja nichts Neues. Und dass die Funktionalitäten eines Services oft in einem Interface definiert und dann in der einer anderen Klasse implementiert werden, auch nicht. Doch mit dem in Java 9 dazugekommenem Modul-System können nun auch einzelne Services modularisiert werden und das bringt viele Vorteile mit sich!
Bevor wir zu den Vorzügen dieser Modularisierung kommen, möchte ich zunächst einmal zeigen, wie einfach diese umzusetzen ist. Hierzu ein kleines Beispiel, welches das Prinzip veranschaulichen soll: Zur Begrüßung und Verabschiedung soll eine nette Nachricht geliefert werden.
Das Interface sieht in diesem Fall wie folgt aus:
package de.ordix.interfaces; public interface HelloAndBye { public String getWelcomeMessage(); public String getGoodbyeMessage(); }
Das Interface besitzt nur zwei Methoden, welche die entsprechenden Nachrichten zurückgeben. Damit dieser Service auch für andere Module verfügbar ist, muss dieser exportiert werden. Ich habe hierfür im Modul-Deskriptor einfach das jeweilige Package nach außen hin verfügbar gemacht. Man hätte aber auch nur das Interface selbst exportieren können.
/** * This module-info.java is part of the ServiceInterface project. */ module Interface { exports de.ordix.interfaces; }
Unser vorhin definierter Service muss nun noch implementiert werden. Zum Beispiel kann dieser Nachrichten in verschiedenen Sprachen oder spezifisch für einzelne Personen(gruppen) zurückgeben. Ich habe nun in einem anderen Projekt einen Service geschrieben, der für die Begrüßung/Verabschiedung in deutscher Sprache verantwortlich ist.
package de.ordix.providers; import de.ordix.interfaces.HelloAndBye; /** * This implementation of the interface HelloAndBye is responsible for the messages in German. */ public class GermanHelloAndByeService implements HelloAndBye { @Override public String getWelcomeMessage() { return "Herzlich Willkommen!"; } @Override public String getGoodbyeMessage() { return "Auf Wiedersehen & bis bald!"; } }
Um das Interface implementieren zu können, muss das Modul des Service Providers den Service kennen (Schlüsselwort requires).
Wenn die Umsetzung des Services in einem anderen Modul verwendet werden können soll, dann muss dieser für die Außenwelt bereitgestellt werden. In unserem Beispiel bedeutet dies konkret, dass der Modul Provider den Service HelloAndBye mit der Implementierung der Klasse GermanHelloAndByeService zur Verfügung stellt (provides <service> with <implementation>).
/** * This module-info.java is part of the ServiceProvider project. */ module Provider { requires ServiceInterface; provides de.ordix.interfaces.HelloAndBye with de.ordix.providers.GermanHelloAndByeService; }
Nun kann der Service auch in anderen Modulen genutzt werden. Ich habe dafür wieder ein eigenes Modul definiert, welches diesen Service nutzt (Stichwort uses).
/** * This module-info.java is part of the ServiceConsumer project. */ module Consumer { requires Interface; uses de.ordix.interfaces.HelloAndBye; }
Der Consumer kennt also bisher nur das Interface und muss die Verknüpfung zum Provider noch auflösen. Diese Aufgabe übernimmt für uns die Klasse ServiceLoader, welche bereits seit Java 6 vorhanden ist und das Laden von Services ermöglicht. Erst zur Laufzeit findet der ServiceLoader heraus, welche Implementierung genutzt werden soll. Hierfür muss sich diese dann auch im Klassen- oder Modulpfad befinden.
package de.ordix.consumer; import java.util.ServiceLoader; import de.ordix.interfaces.HelloAndBye; public class Consumer { public static void main(String[] args) { ServiceLoader<HelloAndBye> loader = ServiceLoader.load(HelloAndBye.class); for (HelloAndBye service : loader) { System.out.println(service.getWelcomeMessage()); System.out.println(service.getGoodbyeMessage()); } } }
In unserem Beispiel findet der ServiceLoader zur Laufzeit genau die eine Implementierung des angegebenen Services HelloAndBye. Die Methoden des Services können dann angesprochen werden und liefern das gewünschte Ergebnis:
Herzlich Willkommen! Auf Wiedersehen & bis bald!
Funktionieren tut das Ganze also schon Mal. Aber was bringt es uns?
Das Praktische ist, dass der Consumer zur Compile-Zeit den Provider nicht kennt und völlig unabhängig von ihm entwickelt werden muss. Auch der Provider weiß nichts von dem Consumer. Es bestehen bis auf das definierte Service Interface keine Abhängigkeiten zueinander. Der Provider kann so sehr einfach wiederverwendet werden.
Neue Service-Implementierungen können außerdem zu jeder Zeit erweitert, nachträglich hinzugefügt der komplett ausgetauscht werden. Dafür muss beim Consumer keine einzige Zeile Programmcode geändert werden! Der entsprechende Service Provider muss nur im Modul-/Klassenpfad angepasst werden.
Insgesamt ziemlich simpel und passt hervorragend zum Modulsystem von Java 9. Mich hat diese Neuheit überzeugt!
Senior Consultant bei ORDIX
Bei Updates im Blog, informieren wir per E-Mail.
Kommentare