Java-Plug-In ohne Schmerzen

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"); //      plugins ModuleFinder pluginsFinder = ModuleFinder.of(pluginsDir); //  ModuleFinder      plugins       List<String> plugins = pluginsFinder .findAll() .stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.toList()); //  ,      (   ) Configuration pluginsConfiguration = ModuleLayer .boot() .configuration() .resolve(pluginsFinder, ModuleFinder.of(), plugins); //      ModuleLayer layer = ModuleLayer .boot() .defineModulesWithOneLoader(pluginsConfiguration, ClassLoader.getSystemClassLoader()); //     IService       Boot List<IService> services = IService.getServices(layer); for (IService service : services) { service.doJob(); } } } 

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?

  1. Java hat versucht, die Module im Ordner plugins finden und keine gefunden.
  2. Eine leere Ebene wurde erstellt.
  3. ServiceLoader suchte nach allen IService Implementierungen.
  4. In der leeren Ebene hat er keine Service-Implementierungen gefunden, da dort keine Module vorhanden sind.
  5. 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.
  6. 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:

  1. Java hat zwei Module im Ordner plugins .
  2. Es wurde eine Ebene mit zwei Modulen plugins1 und plugins2 .
  3. ServiceLoader suchte nach allen IService Implementierungen.
  4. In der Plugin-Ebene fand er zwei Implementierungen des IService Dienstes.
  5. Danach setzte er die Suche in der übergeordneten Ebene (d. H. Der Boot Ebene) fort und fand eine Implementierung von BasicService im Kernmodul.
  6. 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.

Source: https://habr.com/ru/post/de479478/


All Articles