D) Plugin-Entwicklung in Java

Zusammenfassung PluginLoader

package de.jbb.plug.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import de.jbb.plug.interf.Pluggable;

public class PluginLoader {

  public static List<Pluggable> loadPlugins(File plugDir) throws IOException {

    File[] plugJars = plugDir.listFiles(new JARFileFilter());
    ClassLoader cl = new URLClassLoader(PluginLoader.fileArrayToURLArray(plugJars));
    List<Class<Pluggable>> plugClasses = PluginLoader.extractClassesFromJARs(plugJars, cl);
    return PluginLoader.createPluggableObjects(plugClasses);
  }

  private static URL[] fileArrayToURLArray(File[] files) throws MalformedURLException {

    URL[] urls = new URL[files.length];
    for (int i = 0; i < files.length; i++) {
      urls[i] = files[i].toURI().toURL();
    }
    return urls;
  }

  private static List<Class<Pluggable>> extractClassesFromJARs(File[] jars, ClassLoader cl) throws IOException {

    List<Class<Pluggable>> classes = new ArrayList<Class<Pluggable>>();
    for (File jar : jars) {
      classes.addAll(PluginLoader.extractClassesFromJAR(jar, cl));
    }
    return classes;
  }

  @SuppressWarnings("unchecked")
  private static List<Class<Pluggable>> extractClassesFromJAR(File jar, ClassLoader cl) throws IOException {

    List<Class<Pluggable>> classes = new ArrayList<Class<Pluggable>>();
    JarInputStream jaris = new JarInputStream(new FileInputStream(jar));
    JarEntry ent = null;
    while ((ent = jaris.getNextJarEntry()) != null) {
      if (ent.getName().toLowerCase().endsWith(".class")) {
        try {
          Class<?> cls = cl.loadClass(ent.getName().substring(0, ent.getName().length() - 6).replace('/', '.'));
          if (PluginLoader.isPluggableClass(cls)) {
            classes.add((Class<Pluggable>)cls);
          }
        }
        catch (ClassNotFoundException e) {
          System.err.println("Can't load Class " + ent.getName());
          e.printStackTrace();
        }
      }
    }
    jaris.close();
    return classes;
  }

  private static boolean isPluggableClass(Class<?> cls) {

    for (Class<?> i : cls.getInterfaces()) {
      if (i.equals(Pluggable.class)) {
        return true;
      }
    }
    return false;
  }

  private static List<Pluggable> createPluggableObjects(List<Class<Pluggable>> pluggables) {

    List<Pluggable> plugs = new ArrayList<Pluggable>(pluggables.size());
    for (Class<Pluggable> plug : pluggables) {
      try {
        plugs.add(plug.newInstance());
      }
      catch (InstantiationException e) {
        System.err.println("Can't instantiate plugin: " + plug.getName());
        e.printStackTrace();
      }
      catch (IllegalAccessException e) {
        System.err.println("IllegalAccess for plugin: " + plug.getName());
        e.printStackTrace();
      }
    }
    return plugs;
  }
}

Die Plugin-Anwendung testen

Es ist an der Zeit, Ihre Applikation zu testen. Erzeugen Sie hierzu (falls noch nicht geschehen) drei JAR-Files. Eines mit der Schnittstelle, eines für das Plugin, und eines für die Standalone-Anwendung (hier muss im Manifest auf die Schnittstelle verwiesen werden). Die JAR mit der Schnittstelle können Sie bspw. in einem Unterverzeichnis lib im Ordner, in welchem auch die eigentliche Applikation liegt, ablegen. Die JAR mit dem Plugin wird auch in einem Unterordner (plugin) auf selber Ebene wie Ihr Programm gespeichert.

Sie haben jetzt folgende (oder ähnliche) Ordnerstruktur:

Root
|-- PluginApp.jar
|-- plugin (dir)
|  |-- SimplePlugin.jar
|-- lib (dir)
   |-- PluginInterface.jar

Wenn Sie die Anwendung über die Konsole ausführen und alles richtig gemacht haben, erhalten Sie folgende Ausgabe:

java -jar PluginApp.jar
Started!
Stopped!

Sie haben hiermit erfolgreich eine pluginfähige Anwendung erstellt.

Ein weiteres Plugin

Da die Applikation auch mit mehr als einem Plugin umgehen kann, wird ein weiteres Plugin erstellt. Dieses läuft nach dem Start und bis zur Beendigung in einem extra Thread ab, berechnet das Ergebnis einer zufälligen Addition, gibt dieses Ergebnis aus, und schläft eine Anzahl an Millisekunden, die dem Ergebnis entspricht, bis ein neues Ergebnis berechnet wird. Um zu testen, ob der PluginLoader auch funktioniert und wirklich nur Pluggable-Klassen lädt, wird die Rechenfunktion in eine separate Klasse ausgelagert:

package us.ext.so;

public class Calculator {

  public int add(int one, int two) {
    return one + two;
  }
}
package us.ext.so;

import de.jbb.plug.interf.Pluggable;
import de.jbb.plug.interf.PluginManager;

public class CalculatingPlugin implements Pluggable {

  private Calculator calc;
  private PluginManager manager;
  private boolean running = false;

  public CalculatingPlugin() {
    this.calc = new Calculator();
  }

  public void setPluginManager(PluginManager manager) {
    this.manager = manager;
  }

  public boolean start() {

    this.running = true;
    this.manager.showVisualMessage("Calculation started");
    new Thread(new Runnable() {

      public void run() {
        int one = 0;
        int two = 0;
        int res = 0;
        while (CalculatingPlugin.this.running) {
          one = (int)(Math.random() * 1000);
          two = (int)(Math.random() * 1000);
          res = CalculatingPlugin.this.calc.add(one, two);
          CalculatingPlugin.this.manager.showVisualMessage(one + " + " + two + " = " + res);
          try {
            Thread.sleep(res);
          }
          catch (InterruptedException ie) {
            ie.printStackTrace();
          }
        }
      }
    }).start();
    return true;
  }

  public boolean stop() {
    this.manager.showVisualMessage("Calculation stopped");
    this.running = false;
    return true;
  }

}

Aus diesen zwei Klassen können Sie nun wieder ein JAR erstellen (bspw. CalculatingPlugin.jar) und dieses in den plugin-Ordner des Hauptprogramms legen. Starten Sie Ihr Programm erneut, wird das zweite Plugin ebenfalls automatisch gestartet und ausgeführt. Die Ausgabe könnte in etwa so aussehen:

C:\jPlug>java -jar PluginApp.jar
Calculation started
Started!
381 + 366 = 747
898 + 608 = 1506
860 + 407 = 1267
589 + 909 = 1498
428 + 39 = 467
144 + 831 = 975
657 + 374 = 1031
948 + 254 = 1202
619 + 197 = 816
893 + 231 = 1124
Calculation stopped
Stopped!

Dokumentation der Plugin-Schnittstelle

Es ist sehr wichtig, potentielle Plugin-Entwickler mit allen benötigten Informationen zu versorgen, die für die Entwicklung eines Plugins notwendig sind und nicht durch die Schnittstelle alleine vorgegeben werden können. In diesem konkreten Fall gehört hierzu u. a. folgende Informationen:

  • Jede Einstiegsklasse eines Plugins muss das Interface Pluggable implementieren
  • Wenn Pluggable implementiert wurde, muss es einen leeren Standardkonstruktor in der implementierenden Klasse geben, da das Plugin sonst nicht geladen werden kann
  • Bevor ein Plugin über die start-Methode gestartet wird, bekommt dieses von der eigenständigen Applikation auf jeden Fall einen PluginManager zugewiesen
  • Das Plugin muss als JAR-Datei vorliegen

Natürlich sind auch weitere Informationen denkbar. Bspw. könnte es eine Spezifikation geben, die einem Plugin-Entwickler vorschreibt, dass er ins Manifest der Plugin-JAR einen weiteren Eintrag aufnehmen muss, welcher auf Pluggable-Klassen verweist. So müssten Sie nicht das komplette Plugin-JAR nach Klassen durchsuchen, die Pluggable implementieren, sondern könnten lediglich den entsprechenden Eintrag im Manifest interpretieren und auswerten. Dies ist in solcher oder ähnlicher Weise vor allem bei komplexen Anwendungen mit sehr vielen und/oder großen Plugins ratsam.

5 Replies to “D) Plugin-Entwicklung in Java”

  1. Anonymous

    Mittlerweile einfach:

    ServiceLoader loader = ServiceLoader.load(Pluggable.class);
    Iterator it = loader.iterator();

  2. admin

    Freut uns, wenn du hier Inspiration finden konntest. 🙂
    Für eine Erwähnung in deinem Blog-Post sind wir dankbar.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.