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.corePlugins von derIServiceSchnittstelle erben können (andernfalls wäreIServiceaußerhalb des Kernmoduls nichtIService).
- Zweitens gibt er bekannt, dass er den IService.
- Drittens sagt er, dass er eine Implementierung des IServiceDienstes über dieBasicServiceKlasseBasicService.
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 pluginsfinden und keine gefunden.
- Eine leere Ebene wurde erstellt.
- ServiceLoader suchte nach allen IServiceImplementierungen.
- 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 BootSchicht) fort und fand eine Implementierung vonBasicServiceim 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 plugins1undplugins2.
- ServiceLoader suchte nach allen IServiceImplementierungen.
- In der Plugin-Ebene fand er zwei Implementierungen des IServiceDienstes.
- Danach setzte er die Suche in der übergeordneten Ebene (d. H. Der BootEbene) fort und fand eine Implementierung vonBasicServiceim 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.javaund 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, usesundServiceLoader.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.