Java Plugin-Schnittstelle mit dem ServiceLoader entwickeln

veröffentlicht am 02. Juni 2011

Vor einiger Zeit habe ich bereits darüber geschrieben, wie man mit Java eine einfache Plugin-Schnittstelle entwickeln kann. In den Kommentaren wurde ich von Fabian darauf hingewiesen, dass es noch leichter ist, eine Plugin-Schnitstelle mit dem Service Provider Mechanismus – der inzwischen in der Klasse java.util.ServiceLoader aufgegangen ist – zu implementieren. Das habe ich heute mal aufgegriffen und natürlich bekommt ihr ein kleines Tutorial dazu.

Anwendung implementieren

Zunächst entwirfst du ein Interface für die Plugins:

package demo.app;

public interface Plugin {

	String getName();

	String getVersion();

	String getMessage();

}

Danach implementierst du die Hauptanwendung:

package demo.app;

import java.util.Iterator;
import java.util.ServiceLoader;

public class Application {

	public static void main(String[] args) {
		Iterator iter = ServiceLoader.load(Plugin.class).iterator();
		while (iter.hasNext()) {
			Plugin plugin = iter.next();
			System.out.print(plugin.getName() + " ");
			System.out.print(plugin.getVersion() + " ");
			System.out.println(plugin.getMessage() + "\n");
		}
	}

}

Jetzt ist die Anwendung schon bereit, um Plugins einzusetzen. Die Plugins müssen später als JAR-Dateien im Classpath der Anwendung liegen, wobei ein paar Bedingungen erfüllt werden müssen.

Plugin entwickeln

Die Entwicklung des Plugins erfolgt zunächst wie gewohnt unter Implementierung des Plugin-Interface:

package demo.plugin;

import demo.app.Plugin;

public class HelloWorldPlugin implements Plugin {

	public String getName() {
		return "HelloWorldPlugin";
	}

	public String getVersion() {
		return "1.0";
	}

	public String getMessage() {
		return "Hallo Welt!";
	}

}

Wie bereits erwähnt, muss das Plugin als JAR-Datei eingebunden werden. Damit der ServiceLoader mit der JAR-Datei etwas anfangen kann, müssen noch weitere Bedingungen erfüllt werden.

  • Die JAR-Datei muss über ein Verzeichnis namens META-INF\services verfügen
  • In diesem Verzeichnis wird eine Textdatei mit dem vollqualifizierten Namen des Plugin-Interface bzw. der Oberklasse angelegt – in dem Beispiel also die Datei demo.app.Plugin
  • In dieser Datei werden die vollqualifizerten Namen aller Klassen eingetragen, die das entsprechende Interface bzw. die Oberklasse implementieren und als Plugin genutzt werden sollen. In diesem Beispiel trägt man also demo.plugin.HelloWorldPlugin ein

Jetzt muss aus dem Ganzen nurnoch eine JAR-Datei erstellt werden und bei der Ausführung im Classpath vorliegen. Fertig ist die Plugin-fähige Anwendung.

Quellen und weiterführende Links

Kommentare

Funktioniert leider nicht in der Praxis bei getrennten Projekten

Kommentar #1 von User am 27. Juni 2012


Kannst du deine Aussage vielleicht näher erläutern? Wenn man die API öffentlich zugängig macht (z.B. per Maven Artefakt, eingebunden mit dem Scope ‚provided‘) kann man ganz einfach ein entsprechendes Plugin entwickeln. Das wiederum daraus resultierende jar-File muss dann einfach irgendwo im Classpath abgelegt werden (z.B ein gesondertes Plugin Verzeichnis) und schon das Plugin eingebunden.

Kommentar #2 von Patrick am 27. Juni 2012


Ich habe Anwendung+Interface und Plugin in getrennten Projekten entwickelt. Die Anwendung habe ich zu einem JAR kompiliert und im Workspace des Plugins als externes JAR referenziert. Die Datei im Ordner services/ habe ich entsprechend [Anwendungsprojekt].[Package].[Interfaceklasse] genannt. Ansonsten wie in Deinem Beispiel. Zur Laufzeit allerdings ignoriert die Anwendung das Plugin.

Kommentar #3 von User am 29. Juni 2012


Du hast den Classpath nicht (korrekt) gesetzt. Ich habe nun noch einmal die Beispielanwendung (in zwei getrennten Projekten) implementiert und die Eclipse Projektverzeichnisse sowie die lauffähige Version hochgeladen. Den Download findest du ganz am Ende des Artikels.

Schau dir dort einmal die Datei start.cmd im Texteditor an. Dort siehst du, wie man den Classpath unter anderem setzen kann.

Kommentar #4 von Patrick am 29. Juni 2012


Tatsache. Danke. Ich war davon ausgegangen, dass alle Dateien im Eclispe-bin-Verzeichnis automatisch zum Classpath gehörn und nicht einzeln angegeben werden müssen.

Funktioniert. Vielen Dank.

Kommentar #5 von user am 02. Juli 2012


Hallo Patrick,
habe deinen Code als Beispiel für ein Projekt genutzt, wo ich mittels des ServiceLoaders die Module beim Start und zur Laufzeit lade. Funktioniert super und das Beispiel ist nicht zu überladen sodas man den Überblick verlieren würde.

Mein Projekt wollte ich jetzt mit Maven verpacken. Das Hauptprojekt besteht ja dann aus zwei Jars. Application + Plugin-Interface. Wie bekommt man den sowas am Besten mit Maven gebaut? Müssen das zwei Maven-Projekte werden? Habe nur ganz wenig Klassen die sich im Interface befinden und es erscheint mir da schon etwas übertrieben diese in ein eigenes Projekt zu verpacken. Sicher bin ich nicht der erste Mensch der sowas macht und auch nicht der Letzte :-)

Gruß
Thomas

Kommentar #6 von Thomas am 25. Juli 2012


Hallo Thomas,

das Plugin Interface kann natürlich auch zusammen mit der Anwendung ausgeliefert werden. Diesen Teil in ein eigenes Projekt auszulagern, hätte nur den Vorteil, dass das API für Plugin Entwickler übersichtlicher wird und nicht das ganze Projekt als Bibliothek eingebunden werden muss.

Du könntest aber z.B. mit dem Maven Assembly Plugin deine Anwendung und die Plugins gleich beim Bauen in die gewünschte Form bringen (falls nicht bereits geschehen).

Gruß
Patrick

Kommentar #7 von Patrick am 25. Juli 2012


Hallo Patrick,
danke für die schnelle Antwort :-)

Ich möchte das Plugin-Interface separat haben, damit die Module unabhängig zur Applikation entwickelt werden können. Das Ziel ist ein über Applikations-Releases hinweg gleichbleibendes Interface. Dabei liegt das Plugin-Interface in einem eigenem Package und lässt sich autonom kompilieren.

Habs im ersten Wurf mit einem einfachen Ant-Skript gelöst, welches einfach alles bis auf das Plugin-Package kompiliert und im zweiten Schritt das Plugin-Interface ohne die Applikation baut.

Wenn das Projekt mal wachsen sollte, hätten es meiner Ansicht nach viele Entwickler leicht, wenn sie die Abhängigkeit (Plugin-Interface) zum Entwickeln über die Maven Repositories beziehen könnten.

Habe deshalb das kleine Projekt kürzlich auf Maven umgestellt. Übrigens mein erstes Maven Projekt. :-) Deshalb fehlte mir so der Ansatz, wie man es direkt in Maven wieder so hinbekommt, wie es mit Ant lief.

Die Lösung mit dem Assembly Plugin würde zu jedem Applications-Release direkt ein Plugin-Interface-Release machen, müsste genau das sein was ich mit Ant schon gemacht habe. Ist jetzt nur die Frage, was ein Release bei Maven ist. Bei Ant muss ich per Hand das Tag setzen und würde nur das Modul taggen, welches die Änderungen erfahren hat. Bei Maven scheint es mir als ob er beide Versionen immer gleich hochzieht.

Mal sehen wie ich das demnächst lösen werde. Nochmals danke für die schnelle Antwort.

Schöne Grüße
Thomas

Kommentar #8 von Thomas am 25. Juli 2012


Moin Thomas,

bei Maven kannst du nur pro Projekt die Versionsnummer setzen. Wenn du verschiedene Maven Projekte für Anwendung und (Plugin) API anlegst, können diese Projekte natürlich auch unterschiedlich versioniert werden. So kann deine Anwendung immer wieder andere Versionsnummern bekommen, während das Plugin API durchgehend auf derselben Versionsnummer bleibt.

Gruß
Patrick

Kommentar #9 von Patrick am 26. Juli 2012


Hallo Patrick,

ein sehr interessanter Artikel! Weißt Du, ob es auch möglich ist, das Plugin-JAR mit Java-fremden Dateien anzureichern und diese im Projekt auch zu nutzen? Ich habe das Problem, dass ich bei meiner Webanwendung sowohl auf Server- als auch auf Clientseite (JavaScript, CSS) Plugin-Funktionalität benötige.

Viele Grüße,
Oliver

Kommentar #10 von Oliver am 21. März 2013


Hallo Oliver,

wahrscheinlich müsstest du dir die entsprechenden Dateien per Stream aus den JAR-Dateien laden. Damit die Dateien in einer Webapplikation genutzt werden können, müsstest du dann wahrscheinlich ein eigenes Servlet für diesen Zweck schreiben und in der web.xml hinterlegen. Vielleicht gibt es dafür aber auch bereits fertige Implementierungen.

Ich habe so etwas aber noch nie implementiert, vielleicht gibt es dafür auch einen viel eleganteren Weg :)

Gruß
Patrick

Update #1: Nach sichten einiger Threads bei StackOverflow müsste ich mit der Vermutung richtig liegen. Schau dir z.B. einmal diesen Thread inkl. der dort verlinkten Threads an: http://stackoverflow.com/questions/2584162/java-webapp-loading-resource-from-jar-located-in-web-inf

Ich würde die Dateien beim Starten der Anwendung indizieren und dann über ein Servlet für die Anwendung verfügbar machen. Die Indizierung soll aufwändiges durchforsten der JAR Dateien bei jeder Anfrage einer Datei verhindern.

Update #2: damit du in verschiedenen Plugins dieselben Dateinamen verwenden kannst, sollte dem Servlet immer der Name des Plugins übergeben werden. Dann kann auch die Indizierung entfallen. Eventuell sollten die Dateien jedoch nach dem ersten Laden gecached werden.

Kommentar #11 von Patrick am 21. März 2013


Hallo,

Der Blog ist zwar schon älter – aber Alter schützt vor Fragen nicht :-)

Wenn ich jetzt mehrere jars habe:

1.Hauptanwendung

2.ServiceProviderInterface (SPI)

3.SPI Implementierung 1

4.SPI Implementierung 2

So etwa wie in dem „dictionary“ tutorial

http://docs.oracle.com/javase/tutorial/ext/basics/spi.html

Wie mache ich so etwas mit Eclipse(als Service-Neuling)?

-Ist es richtig, dass nur die jars mit den SPI-Implementierungen das META-INF/service Verzeichnis mit Eintrag benötigen?

-Wie erzeuge ich auf einfachsten Wege diese jar mit diesem Verzeichnis in Eclipse, geht das nur mit build.xml.

-Wenn alle jars vorhanden sind, setzte ich die Klassenpfade so?

|
|
v

1. –> add external jar –> 2.

2. –> add external jar –> 3. , 4.

Also, danke im voraus –
ich habe noch die Hoffnung, dass service-loaden einfach ist, wenn man weiß wie…

Kommentar #12 von mäx am 28. Juli 2014


edit:
Ich gehe davon aus, dass für jedes jar ein project erzeugt wird.

Kommentar #13 von mäx am 28. Juli 2014


Hallo mäx,

ja, es müssen nur Einträge für die Implementierungen erzeugt werden. Wie man die JAR Dateien erzeugt, hängt ganz vom gewählten Build Tool ab. Ich empfehle Maven oder Gradle dafür – Ant (build.xml) ist out ;) Man kann die JAR Dateien aber auch ganz ohne Build Tool mit der Export-Funktion von Eclipse erstellen.

Die JAR Dateien können entweder einzeln eingebunden werden oder man legt alle JAR Dateien in einem Ordner ab und bindet den Ordner in den Classpath ein.

Gruß
Patrick

Kommentar #14 von Patrick am 29. Juli 2014


Hinterlasse einen Kommentar