D) Plugin-Entwicklung in Java

Heutzutage werden Anwendungen immer dynamischer, komplexer und müssen ständig erweitert und verbessert werden. Oftmals möchte/muss man als Entwickler aber auch Anderen die Möglichkeit geben, die eigene Applikation zu erweitern. Hierfür verwendet man Plugins. In diesem Kapitel lernen Sie, wie Sie eine Anwendung pluginfähig machen und um einfache Funktionalitäten erweitern, ohne den Code des Programms anzufassen.

Die Plugin-Schnittstelle

Als ersten Schritt gilt es zu definieren, wie externe Programmteile Ihre Applikation erweitern können. Oder anders gefragt: Was soll ein Plugin alles können/dürfen? Wird das Plugin lediglich gestartet und gestoppt und agiert dann selbstständig? Oder darf die eigentliche Applikation auch während des Ausführung des Plugins eingreifen und es so steuern? Und darf das Plugin auch auf Daten und Funktionen Ihrer eigenständigen Anwendung zugreifen? Wenn Sie diese Punkte definiert haben, können Sie eine Schnittstelle designen, über die das Programm mit dem Plugin und ggf. auch das Plugin mit dem Programm interagieren kann.

In diesem Beispiel besteht die Schnittstelle aus zwei Komponenten: Dem Interface Pluggable (die eigentliche Schnittstelle, die von einer Klasse eines jeden Plugins implementiert werden muss) und dem Interface PluginManager (hiermit kann das Plugin Anweisungen an das Programm senden). Pluggable besteht aus einer start– und einer stop-Methode, mit der das Plugin gestartet und gestoppt werden kann. Diese geben einen boolean zurück, falls der Start-/Stop-Vorgang erfolgreich ausgeführt wurde. Außerdem gibt es noch die Methode setPluginManager, mit der dem Plugin ein Manager gesetzt werden kann, über den es auf die eigentliche Anwendung zugreifen kann (natürlich nur so weit, wie es der Manager zulässt).

package de.jbb.plug.interf;

public interface Pluggable {

  boolean start();
  boolean stop();
  void setPluginManager(PluginManager manager);
}

Da der PluginManager die einzige Schnittstelle zum eigenständigen Programm ist, enthält dieser alle Methoden, mit der ein Plugin auf die Applikation zugreifen kann. Damit der Code nicht zu komplex wird, enthält der PluginManager in diesem Beispiel nur eine einzige Methode: showVisualMessage. Der PluginManager hat beim Aufruf dieser Methode die Aufgabe, eine Nachricht auf dem Bildschirm auszugeben. Ob das via GUI, in der Konsole, oder auf einen ganz anderen Weg geschieht, wird nicht spezifiziert.

package de.jbb.plug.interf;

public interface PluginManager {

  void showVisualMessage(String message);
}

Damit diese beiden Klassen als Schnittstelle zwischen Plugin und Programm agieren können, müssen sowohl die Plugins, als auch die Anwendung selbst, diese beiden Interfaces kennen. Es bietet sich also an, ein eigenes PluginInterface.jar zu erstellen, welches aus dem PluginManager und dem Pluggable Interface besteht. Dieses JAR muss dann während der Entwicklung der Applikation und der Plugins als Classpath-Eintrag zur Verfügung stehen. Das Programm selbst benötigt die Schnittstelle natürlich auch beim Ausführen.

Ein einfaches Plugin

Das Schöne bei der Entwicklung von Plugins ist die Tatsache, dass ein Plugin in vielen Fällen wirklich nur die Schnittstelle und nicht die interne Funktionsweise des Programms kennen muss (bei entsprechend komplexeren Systemen empfiehlt es sich für den Plugin-Entwickler dennoch, sich ein tieferes Verständnis für und über die zu erweiternde Applikation anzueignen). Deshalb sind wir so dreist, und entwickeln zuerst ein einfaches Plugin, ohne vorher die Anwendung programmiert zu haben.

Dieses erste, sehr einfache Plugin macht nichts weiter als beim Starten und Beenden einen Text auszugeben. Zur Textausgabe ruft es die entsprechende Methode des PluginManagers auf. Selbstverständlich muss das Plugin das Interface Pluggable implementieren.

package en.ext.plug;

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

public class SimplePlugin implements Pluggable {

  private PluginManager manager;

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

  public boolean start() {
    this.manager.showVisualMessage("Started!");
    return true;
  }

  public boolean stop() {
    this.manager.showVisualMessage("Stopped!");
    return true;
  }
}

Am Besten erzeugen Sie jetzt schon einmal ein JAR aus dieser Klasse. Dies ist Ihr erstes, einfaches Java-Plugin!

Eine pluginfähige Applikation

Selbstverständlich soll das Plugin nun auch in einer entsprechenden Anwendung ausgeführt werden. Dies übernimmt die Klasse PluginApp. Sie besteht lediglich aus einer Main-Methode in der die Plugins aus einem bestimmten Verzeichnis (plugin, relativ zum Ausführungsort des Programms) durch eine weitere Klasse (PluginLoader) geladen werden. Die geladenen Plugins bekommen zuerst einen PluginManager gesetzt, welchen es auch noch zu implementieren gilt (PluginManagerImpl). Anschließend werden sie über Ihre start-Methode gestartet, nur um nach einer kurzen Wartezeit über die stop-Methode wieder beenden zu werden. Dies soll für den Anfang ausreichen.

package de.jbb.plug;

import java.io.File;
import java.io.IOException;
import java.util.List;

import de.jbb.plug.interf.Pluggable;
import de.jbb.plug.interf.PluginManager;
import de.jbb.plug.man.PluginManagerImpl;
import de.jbb.plug.util.PluginLoader;

public class PluginApp {

  public static void main(String[] args) throws IOException {

    List<Pluggable> plugins = PluginLoader.loadPlugins(new File("./plugin"));
    PluginManager manager = new PluginManagerImpl();
    for (Pluggable p : plugins) {
      p.setPluginManager(manager);
    }
    for (Pluggable p : plugins) {
      p.start();
    }
    // wait
    try {
      Thread.sleep(10000);
    }
    catch (InterruptedException ie) {
      ie.printStackTrace();
    }
    for (Pluggable p : plugins) {
      p.stop();
    }
  }
}

Einen PluginManager implementieren

Werfen Sie zuerst einen Blick auf den PluginManager, der hier durch die Klasse PluginManagerImpl repräsentiert wird. Die PluginManagerImpl-Klasse muss das Interface PluginManager und die Methode showVisualMessage implementieren. Für dieses Beispiel ist es ausreichend, wenn der PluginManager die Nachricht einfach auf der Konsole ausgibt.

package de.jbb.plug.man;

import de.jbb.plug.interf.PluginManager;

public class PluginManagerImpl implements PluginManager {

  public void showVisualMessage(String message) {
    System.out.println(message);
  }
}

Plugins laden

Jetzt beginnt der interessanteste Teil: Wie kommen die Plugins in die Applikation? Diese Funktionalität wurde in eine separate Klasse, dem PluginLoader, ausgelagert. Da diese Klasse keine Daten bereitstellen muss, sind ihre Methoden statisch.

Als einzige öffentliche Methode bietet der PluginLoader die Methode public static List loadPlugins(File plugDir) throws IOException an. Diese Methode lädt alle Klassen (Plugins) aus dem entsprechenden Verzeichnis, welche das Interface Pluggable implementieren, erzeugt Objekte der konkreten Implementierungen, und gibt diese in einer java.util.List zurück.

Um die Plugins zu laden, benötigen Sie zuerst einmal alle JARs, die sich im Plugin-Verzeichnis befinden. Diese erhalten Sie über die Methode listFiles des File-Objekts. Damit Sie auch wirklich nur JAR-Dateien erhalten, wird zusätzlich noch ein eigener JARFileFilter gesetzt.

package de.jbb.plug.util;

import java.io.File;
import java.io.FileFilter;

public class JARFileFilter implements FileFilter {

  public boolean accept(File f) {
    return f.getName().toLowerCase().endsWith(".jar");
  }
}
File[] plugJars = plugDir.listFiles(new JARFileFilter());

Damit Sie Klassen laden können, die nicht beim Start Ihrer Anwendung über den Classpath eingebunden wurden, benötigen Sie einen eigenen ClassLoader.

Ein ClassLoader weiß, welche Klassen in seinen Ressourcen vorhanden sind und kann diese bei Bedarf laden.

Um Klassen aus JAR-Dateien zu laden, kann ein java.net.URLClassLoader verwendet werden. Diesem werden die zu ladenden JAR-Dateien in Form eines java.net.URL-Arrays übergeben. Hierzu müssen Sie das File-Array in ein URL-Array umwandeln.

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;
}
ClassLoader cl = new URLClassLoader(PluginLoader.fileArrayToURLArray(plugJars));

Nun können Sie über den eigenen ClassLoader auf die externen Plugin-Klassen zugreifen. Es fehlt Ihnen jedoch noch die Information, wie die Klassen konkret heißen, und welche davon Pluggable implementieren. Sie müssen also einen Blick in die JAR Datei werfen, um alle verfügbaren Klassen zu ermitteln. Idealerweise werden alle Klassen, die nicht Pluggable sind, erst gar nicht aufgelistet. Diese Aufgabe übernimmt für Sie die Methode private static List<Class<Pluggable>> extractClassesFromJARs(File[] jars, ClassLoader cl) throws IOException. Ihr werden die Verknüpfungen zu den JAR-Dateien und der gerade erzeugte ClassLoader übergeben.

List<Class<Pluggable>> plugClasses = PluginLoader.extractClassesFromJARs(plugJars, cl);

In der Methode extractClassesFromJARs werden die Klassen nicht direkt ausgelesen. Diese Aufgabe wird an die Methode extractClassesFromJAR delegiert, in welcher genau von einem einzigen JAR die Klassen ausgelesen werden. Das Ergebnis dieses zweiten Methodenaufrufs (ebenfalls eine List<Class<Pluggable>>) wird dann der Liste aller JARs hinzugefügt.

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;
}

Die Klassen in einem JAR ermittelt man am Besten mit Hilfe eines java.util.jar.JarInputStreams. Mit diesem Stream lässt sich über die einzelnen Einträge/Objekte (java.util.jar.JarEntry) in einem JAR iterieren. Wenn es sich hierbei um eine Klasse handelt (der Eintrag endet mit .class), wird versucht, die Klasse über den ClassLoader zu laden. Zuvor müssen jedoch noch die Package-Trennzeichen von „/“ in „.“ umgewandelt, und die Endung (.class) vom Namen des Eintrags abgeschnitten werden. Nachdem die Klasse erfolgreich geladen wurde, kann über eine weitere Methode überprüft werden, ob es sich um eine Klasse handelt, die Pluggable implementiert. Falls dies der Fall ist, wird sie der Ergebnisliste hinzugefügt.

@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;
}

Die Methode isPluggableClass stellt fest, ob eine Klasse Pluggable ist oder nicht, indem die Interfaces der Klasse überprüft werden, ob eines dem Pluggable-Interface entspricht.

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

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

Zu guter Letzt müssen aus den Pluggable-Klassen noch Objekte konstruiert werden. Dies übernimmt die Methode createPluggableObjects, indem auf eine Pluggable-Klasse die Methode newInstance ausgeführt wird. Voraussetzung hierfür ist natürlich, dass alle Plugin-Klassen einen leeren Standardkonstruktor ohne Parameter implementieren.

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;
}

Somit kann die loadPlugins-Methode vervollständigt werden:

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);
}

Auf der nächsten Seite finden Sie eine Zusammenfassung der PluginLoader-Klasse. Außerdem wird ein weiteres Plugin erstellt.

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