In diesem Artikel möchte ich Ihnen erklären, wie Sie schnell und einfach ein Java-Anwendungsframework mit Unterstützung für das dynamische Laden von Plugins erstellen. Der Leser wird wahrscheinlich sofort denken, dass eine solche Aufgabe schon lange gelöst ist, und Sie können einfach vorgefertigte Frameworks verwenden oder Ihren Klassenlader schreiben, aber in der von mir vorgeschlagenen Lösung ist nichts davon erforderlich:
- Wir benötigen keine speziellen Bibliotheken oder Frameworks ( OSGi , Guice usw.)
- Wir werden keine Bytecode-Analyse mit ASM und ähnlichen Bibliotheken verwenden.
- Wir werden unseren Klassenlader nicht schreiben.
- Reflexionen und Anmerkungen werden nicht verwendet.
- Sie müssen sich nicht mit dem Klassenpfad herumärgern, um Plugins zu finden. Wir werden den Klassenweg überhaupt nicht berühren.
- Wir werden auch kein XML, YAML oder andere deklarative Sprachen verwenden, um Erweiterungspunkte (Erweiterungspunkte in Plugins) zu beschreiben.
Es gibt jedoch noch eine Anforderung: Eine solche Lösung funktioniert nur mit Java 9 oder höher. Weil es auf
Modulen und Diensten basieren wird.
Also fangen wir an. Wir formulieren das Problem genauer:
Sie müssen ein minimales Anwendungsframework implementieren, das beim Start Benutzer-Plugins aus dem plugins
Ordner lädt.
Das heißt, die zusammengestellte Anwendung sollte ungefähr so aussehen:
plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar …
Beginnen wir mit dem Kernmodul. Dieses Modul ist der Kern unserer Anwendung, das heißt unser Framework.
Für diejenigen, die Wert auf Zeit legen, steht das fertige Projekt auf GitHub zur Verfügung. Montageanleitung.Link git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core
Erstellen Sie die folgenden 4 Java-Dateien im Modul:
core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java
Die erste Datei,
IService.java
, beschreibt unseren Erweiterungspunkt. Andere Plugins können dann zu diesem Erweiterungspunkt beitragen („Contribute“). Dies ist das Standardprinzip für die Erstellung von Plug-In-Anwendungen, das als
Prinzip der Abhängigkeitsinversion (Dependency Inversion) bezeichnet wird. Dieses Prinzip basiert auf der Tatsache, dass der Kernel nicht von bestimmten Klassen abhängt, sondern von Schnittstellen.
Ich habe dem Erweiterungspunkt den abstrakten Namen
IService
, da ich jetzt ausschließlich ein Konzept demonstriere. In Wirklichkeit kann es sich um einen beliebigen bestimmten Erweiterungspunkt handeln. Wenn Sie beispielsweise einen Grafikeditor schreiben, kann dies die Auswirkung der Bildverarbeitung sein, z. B.
IEffectProvider
,
IEffectContribution
oder etwas anderes, je nachdem, wie Sie die Erweiterungspunkte benennen möchten. Gleichzeitig wird die Anwendung selbst einige grundlegende Effekte enthalten, und Entwickler von Drittanbietern werden in der Lage sein, zusätzliche komplexere Effekte zu schreiben und diese in Form von Plug-Ins bereitzustellen. Der Benutzer muss diese Effekte nur in den
plugins
Ordner stellen und die Anwendung neu starten.
Die Datei
IService.java
lautet wie folgt:
… public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } }
Daher ist
IService
nur eine Schnittstelle, die einige abstrakte
doJob()
ich wiederhole, die Details sind nicht wichtig, in Wirklichkeit wird es etwas Konkretes sein).
getServices()
die zweite Methode
getServices()
. Diese Methode gibt alle Implementierungen der
IService
Schnittstelle zurück, die in dieser Modulschicht und ihren
IService
gefunden wurden. Wir werden später genauer darauf eingehen.
Die zweite Datei,
BasicService.java
, ist die
IService
der
IService
Schnittstelle. Es ist immer vorhanden, auch wenn die Anwendung keine Plugins enthält. Mit anderen Worten,
core
ist nicht nur der Core, sondern auch gleichzeitig ein Plugin für sich, das immer geladen wird. Die
BasicService.java
Datei sieht folgendermaßen aus:
… public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } }
Der Einfachheit halber
doJob()
nur die Zeichenfolge
"Basic service"
und das
doJob()
.
So haben wir im Moment folgendes Bild:

In der dritten Datei,
Main.java
, ist die Methode
main()
implementiert. Es ist ein wenig magisch, zu verstehen, was Sie wissen müssen, was eine Modulebene ist.
Über Modulebenen
Wenn Java die Anwendung startet, fallen alle Plattformmodule + Module, die im Argument
--module-path
(und gegebenenfalls auch
--module-path
) aufgeführt sind, in die sogenannte
Boot
Schicht. Wenn wir in unserem Fall das Modul core.jar kompilieren und
java --module-path core.jar --module core
über die Befehlszeile
java --module-path core.jar --module core
mindestens die Module
java.base
und
core
in der
Boot
Ebene:

Die
Boot
Schicht ist in jeder Java-Anwendung immer vorhanden, und dies ist die kleinstmögliche Konfiguration. Die meisten Anwendungen existieren in einer einzigen Modulschicht. In unserem Fall möchten wir jedoch das dynamische Laden von Plugins aus dem
plugins
Ordner durchführen. Wir könnten den Benutzer nur zwingen, die Startzeile der Anwendung zu korrigieren, damit er selbst die erforderlichen Plugins zu
--module-path
hinzufügt. Dies ist jedoch nicht die beste Lösung. Vor allem diejenigen, die keine Programmierer sind und nicht verstehen, warum sie irgendwo hinklettern und etwas für eine so einfache Sache reparieren müssen.
Zum Glück gibt es eine Lösung: Mit Java können Sie in Runtime Ihre eigenen Modul-Layer erstellen, die die Module von dem Ort laden, den wir benötigen. Für unsere Zwecke ist eine neue Ebene für Plugins ausreichend, die eine
Boot
Ebene als übergeordnete Ebene hat (jede Ebene muss eine übergeordnete Ebene haben):

Die Tatsache, dass die Plugin-Ebene die
Boot
Ebene als übergeordnete Ebene hat, bedeutet, dass die Module aus der Plugin-Ebene auf die Module aus der
Boot
Ebene verweisen können, aber nicht umgekehrt.
Main.java
Sie nun wissen, was eine Modulebene ist, können Sie sich endlich den Inhalt der Datei
Main.java
:
… public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins");
Wenn Sie sich diesen Code zum ersten Mal ansehen, scheint er sehr kompliziert zu sein, aber aufgrund der großen Anzahl neuer unbekannter Klassen ist dies ein falsches Gefühl. Wenn Sie etwas über die Bedeutung der
Klassen ModuleFinder ,
Configuration und
ModuleLayer wissen , ist alles in Ordnung. Und außerdem gibt es nur ein paar Dutzend Zeilen! Dies ist die gesamte Logik, die einmal geschrieben wurde.
Modulbeschreibung
Es gibt eine weitere (vierte) Datei, die wir nicht berücksichtigt haben:
module-info.java
. Dies ist die kürzeste Datei, die die Deklaration unseres Moduls und eine Beschreibung der Dienste (Erweiterungspunkte) enthält:
… module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; }
Die Bedeutung der Zeilen dieser Datei sollte offensichtlich sein:
- Erstens exportiert das Modul das Paket
org.example.pluginapp.core
, org.example.pluginapp.core
Plugins von der IService
Schnittstelle erben können (andernfalls wäre IService
außerhalb des Kernmoduls nicht IService
). - Zweitens gibt er bekannt, dass er den
IService
. - Drittens sagt er, dass er eine Implementierung des
IService
Dienstes über die BasicService
Klasse BasicService
.
Da die Moduldeklaration in Java geschrieben ist, erhalten wir sehr wichtige Vorteile:
Compilerprüfungen und statische Garantien . Wenn wir beispielsweise im Namen der Typen einen Fehler gemacht oder ein nicht vorhandenes Paket angegeben hätten, hätten wir es sofort erhalten. Bei einigen OSGi-Versionen hätten wir zum Zeitpunkt der Kompilierung keine Überprüfungen, da die Deklaration der Erweiterungspunkte in XML geschrieben wäre.
Der Rahmen ist also fertig. Lass es uns versuchen:
> java --module-path core.jar --module core Basic service
Was ist passiert?
- Java hat versucht, die Module im Ordner
plugins
finden und keine gefunden. - Eine leere Ebene wurde erstellt.
- ServiceLoader suchte nach allen
IService
Implementierungen. - In der leeren Ebene hat er keine Service-Implementierungen gefunden, da dort keine Module vorhanden sind.
- Nach dieser Schicht setzte er die Suche in der übergeordneten Schicht (d. H. Der
Boot
Schicht) fort und fand eine Implementierung von BasicService
im Kernmodul. - Alle gefundenen Implementierungen hatten die Methode
doJob()
aufgerufen. Da nur eine Implementierung gefunden wurde, wurde nur der "Basic service"
gedruckt.
Ein Plugin schreiben
Nachdem Sie den Kern unserer Anwendung geschrieben haben, ist es jetzt an der Zeit, Plugins dafür zu schreiben. Schreiben wir zwei Plugins
plugin1
und
plugin2
: Lassen Sie den ersten
"Service 1"
, den zweiten
"Service 2"
plugin2
. Dazu müssen Sie in
plugin1
und
plugin2
zwei weitere
IService
Implementierungen
plugin1
:

Erstelle das erste Plugin mit zwei Dateien:
plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java
Service1.java
Datei:
… public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } }
module-info.java
Datei:
… module plugin1 { requires core; provides IService with Service1; }
Beachten Sie, dass
plugin1
ist. Dies ist das zuvor erwähnte Prinzip der Abhängigkeitsinversion: Der Kernel ist nicht von Plugins abhängig, sondern umgekehrt.
Das zweite Plugin ist dem ersten Plugin völlig ähnlich, daher werde ich es hier nicht geben.
Jetzt sammeln wir die Plugins, legen sie in den
plugins
Ordner und führen die Anwendung aus:
> java --module-path core.jar --module core Service 1 Service 2 Basic service
Hurra, die Plugins wurden abgeholt! Wie ist das passiert:
- Java hat zwei Module im Ordner
plugins
. - Es wurde eine Ebene mit zwei Modulen
plugins1
und plugins2
. - ServiceLoader suchte nach allen
IService
Implementierungen. - In der Plugin-Ebene fand er zwei Implementierungen des
IService
Dienstes. - Danach setzte er die Suche in der übergeordneten Ebene (d. H. Der
Boot
Ebene) fort und fand eine Implementierung von BasicService
im Kernmodul. - Alle gefundenen Implementierungen hatten die Methode
doJob()
aufgerufen.
Beachten Sie, dass genau deshalb, weil die Suche nach Dienstanbietern mit untergeordneten Schichten beginnt und dann zu den übergeordneten Schichten geht, zuerst
"Service 1"
und
"Service 2"
und dann
"Basic Service"
gedruckt werden. Wenn die Dienste so sortiert werden sollen, dass zuerst die Basisdienste und dann die Plugins
IService.getServices()
werden, können Sie die Methode
IService.getServices()
optimieren, indem Sie die Sortierung dort hinzufügen (möglicherweise müssen Sie die Methode
int getOrdering()
zur
IService
Schnittstelle
IService
).
Zusammenfassung
Daher habe ich gezeigt, wie Sie eine Plug-in-Java-Anwendung mit den folgenden Eigenschaften schnell und effizient organisieren können:
- Einfachheit: Für Erweiterungspunkte und ihre Bindungen werden nur die grundlegenden Java-Funktionen (Schnittstellen, Klassen und ServiceLoader ) verwendet, ohne Frameworks, Reflektionen, Anmerkungen und Klassenladeprogramme.
- Deklarierbarkeit: Erweiterungspunkte werden in Modulbeschreibungen beschrieben. Schauen Sie sich
module-info.java
und verstehen Sie, welche Erweiterungspunkte existieren und welche Plugins zu diesen Punkten beitragen. - Statische Garantien: Bei Fehlern in den Modulbeschreibungen wird das Programm nicht kompiliert. Außerdem erhalten Sie als Bonus zusätzliche Warnungen, wenn Sie IntelliJ IDEA verwenden (zum Beispiel, wenn Sie vergessen haben,
uses
und ServiceLoader.load()
). - Sicherheit: Das modulare Java-System prüft beim Start, ob die Konfiguration der Module korrekt ist und weigert sich, das Programm bei Fehlern auszuführen.
Ich wiederhole, ich habe nur die Idee gezeigt. In einer echten Plug-in-Anwendung gibt es zehn bis hundert Module und hundert bis tausend Erweiterungspunkte.
Ich habe mich dazu entschlossen, dieses Thema anzusprechen, weil ich in den letzten 7 Jahren eine modulare Anwendung mit Eclipse RCP geschrieben habe, in der das berüchtigte OSGi als Plug-In-System verwendet wird und die Plug-In-Deskriptoren in XML geschrieben sind. Wir haben mehr als hundert Plugins und arbeiten immer noch mit Java 8. Aber selbst wenn wir auf die neue Java-Version wechseln, werden wir wahrscheinlich keine Java-Module verwenden, da diese stark an OSGi gebunden sind.
Wenn Sie jedoch eine Plug-in-Anwendung von Grund auf neu schreiben, sind Java-Module eine der möglichen Optionen für deren Implementierung. Denken Sie daran, dass Module nur ein Werkzeug und kein Ziel sind.
Kurz über mich
Ich programmiere seit mehr als 10 Jahren (8 davon in Java), reagiere auf
StackOverflow und
starte meinen eigenen
Kanal in Telegram, der sich Java widmet.