Java: Klasse mit korrektem Interface dynamisch laden

Bei größeren Anwendungen, die flexibel in verschiedenen Bereichen einsetzbar sein sollen, kommt schnell der Wunsch auf, diese erweiterbar zu gestalten, sodass andere Nutzer zur Not eigene Erweiterungen schreiben können. Java bietet hierfür mit seinen von Hause aus mitgebrachten Classloadern bereits die passende Grundlage - diese muss nur noch richtig zusammengesetzt werden.

Um das Vorgehen einmal darzustellen, habe ich ein kleines Projekt ("LoadClass") aufgesetzt, das aus drei Teilen besteht:

  • die Klassenbibliothek "lib", in der ein Interface definiert ist: Das Interface dient als Schnittstelle zwischen der eigentlichen Anwendung und der Erweiterung. Durch Zuhilfenahme eines Interfaces müssen die späteren Methodenaufrufe des Plugins nicht über Reflection realisiert werden.
  • die Klassenbibliothek "plugin", in der die Erweiterung implementiert ist: Die Erweiterung ist eine Klasse, die das Interface aus der Klassenbiliothek "lib" implementiert.
  • die Anwendung "app", die die eigentliche Anwendungslogik implementiert: Die Anwendung ist der eigentliche Nutznießer der Erweiterbarkeit. Sie lädt das Plugin und führt den darin enthaltenen Code aus.

Fangen wir mit der Implementierung der Klassenbibliothek "lib" an. Die beschriebenen Schritte sind beispielhaft für die Entwicklungsumgebung Netbeans, sollten in anderen IDEs jedoch genauso einfach umsetzbar sein.

  1. Zuerst legen wir ein neues Projekt an und wähle als Projektart "Java Class Library" aus. Als Projektnamen vergeben wir einfach den Namen "lib".
  2. Als nächstes legen wir im Projekt eine neue Datei an und wähle als Typ das "Java Package" aus. Als Namen erhält es in diesem Beispiel "com.example.loadclass.lib".
  3. Innerhalb des Packages wiederum legen wir eine neue Datei vom Typ "Java Interface" an. Die Klasse erhält den Namen "ExampleInterface."
  4. Der Inhalt der Interface-Datei ist in diesem Beispiel minimal:
    1
    2
    3
    4
    5
    6
    7
    package com.example.loadclass.lib;

    public interface ExampleInterface {

        public String returnValue();

    }

Nun machen wir weiter mit der Implementierung des Plugins.

  1. Wir legen ein weiteres Projekt an und wähle als Projektart wieder die "Java Class Library" aus. Als Projektnamen vergeben wir dieses Mal den Namen "plugin".
  2. Mit einem Rechtsklick auf den Projekteintrag im Projektbrowser gehen wir auf "Properties" und im Konfigurationsfenster auf die Kategorie "Libraries". Hier können wir über "Add Project..." nun das vorhin angelegte Projekt "lib" als Abhängigkeit aufnehmen. Nach einem Klick auf "OK" geht es weiter.
  3. Wir legen im Projekt eine neue Datei an und wählen als Typ das "Java Package" aus. Als Namen erhält es nun "com.example.loadclass.plugin".
  4. Innerhalb des Packages legen wir eine neue Datei vom Typ "Java Class" an. Die Klasse erhält den Namen "ExamplePlugin."
  5. Die Funktion des Plugins ist entsprechend des Interfaces ebenfalls minimal:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.example.loadclass.plugin;

    import com.example.loadclass.lib.ExampleInterface;

    public class ExamplePlugin implements ExampleInterface {

        @Override
        public String returnValue() {
            return "Hello world!";
        }

    }

Bis hierhin gibt es, abgesehen davon, dass wir Interface und Implementierung in eigene Projekte auslagern, wenig Neues im Vergleich zu einer einfachen Java-Anwendung. Spannend wird es in der eigentlichen Anwendung, die wir nun anlegen.

  1. Wir legen ein weiteres Projekt an und wählen nun als Typ die "Java Application" aus. Als Namen erhält das Projekt dieses Mal den Wert "app". Zudem wählen wir aus, dass eine Main-Klasse mit dem Namen "com.example.loadclass.app.Main" erstellt werden soll. Das daraus entstehende Gerüst ist ziemlich schmalspurig:
    1
    2
    3
    4
    5
    6
    7
    8
    package com.example.loadclass.app;

    public class Main {

        public static void main(String[] args) {
        }

    }
  2. Als nächsten Schritt machen wir wieder einen Rechtsklick auf den Projekteintrag im Projektbrowser, gehen auf "Properties" und im Konfigurationsfenster auf die Kategorie "Libraries". Über "Add Project..." nehmen wir das Projekt "lib" wieder als Abhängigkeit auf und klicken abschließend auf "OK".
  3. In die Main-Klasse nehmen wir nun als erstes ein paar Definitionen auf, die später in den Methoden benötigt werden.
    1
    2
    3
    4
    5
    6
    7
    8
    9
        protected static final String LOADCLASS_CLASS_EXTENSION = ".class";
        protected static final char   LOADCLASS_CLASS_SEPARATOR = '.';
        protected static final String LOADCLASS_JAR_EXTENSION   = ".jar";
        protected static final char   LOADCLASS_PATH_SEPARATOR  = '@';
        protected static final String LOADCLASS_URL_PREFIX      = "file://";

        protected static final String APP_PLUGIN_CLASS = "com.example.loadclass.plugin.ExamplePlugin";
        protected static final String APP_PLUGIN_JAR   = "/Users/kenny/Desktop/loadClass/plugin/dist/plugin.jar";
        protected static final String APP_PLUGIN_PATH  = APP_PLUGIN_CLASS + "@" + APP_PLUGIN_JAR;
  4. Als nächstes fügen wir mehrere Methoden hinzu. Die erste dient dazu, herauszufinden, ob ein angegebener Plugin-Pfad tatsächlich korrekt ist. Als Pluginpfad verwenden wir an dieser Stelle die Form "<Klassenname>@<Bibliothekspfad>". Das bedeutet beispielsweise auch, dass wir sicherheitshalber in einer *.jar Datei nach der angegebenen Klassendatei suchen.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
        // structure is "<class>@<path>", where "<path>" may be a folder or a *.jar file
        public static boolean checkName(String aString) {
            boolean lResult = false;

            if (null != aString) {
                // the combinator must not be at the very beginning or at the very end
                if ((aString.indexOf(LOADCLASS_PATH_SEPARATOR) > 0) &&
                    (aString.indexOf(LOADCLASS_PATH_SEPARATOR) < aString.length()-1)) {
                    String lClassName = aString.substring(0, aString.indexOf(LOADCLASS_PATH_SEPARATOR));
                    String lPathName  = aString.substring(aString.indexOf(LOADCLASS_PATH_SEPARATOR)+1);

                    File lPath = new File(lPathName);
                    if (lPath.exists()) {
                        if (lPath.isFile()) {
                            if (lPathName.endsWith(LOADCLASS_JAR_EXTENSION)) {
                                try (JarFile lJarFile = new JarFile(lPath)) {
                                    if (null != lJarFile.getJarEntry(lClassName.replace(LOADCLASS_CLASS_SEPARATOR,
                                                                                        File.separatorChar) +
                                                                     LOADCLASS_CLASS_EXTENSION)) {
                                        lResult = true;
                                    }
                                } catch (Exception lException) {}
                            }
                        } else {
                            if (lPath.isDirectory()) {
                                // fix path delimiter
                                if (File.separatorChar != lPathName.charAt(lPathName.length()-1)) {
                                    lPathName = lPathName + File.separator;
                                }

                                // check if the class exists
                                lPath = new File(lPathName +
                                                 lClassName.replace(LOADCLASS_CLASS_SEPARATOR, File.separatorChar) +
                                                 LOADCLASS_CLASS_EXTENSION);
                                if (lPath.isFile()) {
                                    lResult = true;
                                }
                            }
                        }
                    }
                }
            }

            return lResult;
        }
  5. Wenn wir sicher sind, dass wir eine Klasse laden könnten, gehen wir den nächsten Schritt und tun das auch. Hierfür verwenden wir den URLClassLoader, der neben Ordnern und *.jar Dateien auch mit Webressourcen umgehen kann (die wir an dieser Stelle jedoch nicht verwenden).
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
        public static Class loadClass(String aName) {
            Class lResult = null;

            if (checkName(aName)) {
                // get name parts of parameter
                String lClassName = aName.substring(0, aName.indexOf(LOADCLASS_PATH_SEPARATOR));
                String lPathName  = aName.substring(aName.indexOf(LOADCLASS_PATH_SEPARATOR)+1);

                // check that directory name ends with path separator
                if (new File(lPathName).isDirectory()) {
                    if (File.separatorChar != lPathName.charAt(lPathName.length()-1)) {
                        lPathName = lPathName + File.separator;
                    }
                }

                try {
                    URL lURL = new URL(LOADCLASS_URL_PREFIX + lPathName);

                    URLClassLoader lClassLoader = new URLClassLoader(new URL[]{lURL},
                                                                     ClassLoader.getSystemClassLoader());

                    lResult = lClassLoader.loadClass(lClassName);
                } catch (Exception lException) {}
            }

            return lResult;
        }
  6. Mit dem Laden der Klasse ist es jedoch noch nicht getan, denn wir wollen ja schlussendlich eine Instanz der geladenen Klasse erhalten. Hierbei sollten wir natürlich prüfen, dass die geladene Klasse das erwartete Interface auch tatsächlich implementiert hat.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
        protected static Object createObject(String aName, Class aInterface) {
            Object lResult = null;

            if (null != aInterface) {
                Class lClass = loadClass(aName);
                if (null != lClass) {
                    boolean lCorrectInterface = false;

                    for (Class lInterface : lClass.getInterfaces()) {
                        if (aInterface.equals(lInterface)) {
                            lCorrectInterface = true;
                            break;
                        }
                    }

                    if (lCorrectInterface) {
                        try {
                            lResult = lClass.newInstance();
                        } catch (Exception lException) {}
                    }
                }
            }

            return lResult;
        }
  7. Mit Hilfe dieser Methoden können wir unsere main() Methode nun so erweitern, dass wir die angegebene Klasse aus ihrer Bibliothek laden, instanziieren und auch nutzen.
    1
    2
    3
    4
    5
    6
    7
        public static void main(String[] args) {
            ExampleInterface lObject = (ExampleInterface)createObject(APP_PLUGIN_PATH,
                                                                      ExampleInterface.class);
            if (null != lObject) {
                System.out.println(lObject.returnValue());
            }
        }
  8. Sollte sich eure IDE nicht automatisch um das Einfügen benötigter Import-Befehle kümmern, müsst ihr diese selbst ergänzen.
    1
    2
    3
    4
    5
    import com.example.loadclass.lib.ExampleInterface;
    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.util.jar.JarFile;

Wenn ihr alles korrekt zusammengefügt habt und die Pakete alle einmal erzeugt habt ("lib.jar", "plugin.jar" und "app.jar"), müsst ihr euch ansehen, unter welchem Dateipfad das Plugin-Paket erreichbar ist (in meinem Beispiel ist es "/Users/kenny/Desktop/loadClass/plugin/dist/plugin.jar"). Diesen vollständigen Pfad müsst ihr in die Variable "APP_PLUGIN_JAR" schreiben. In einem tatsächlichen Anwendungsfall würde man solche Informationen dann wahrscheinlich eher aus einer Konfigurationsdatei auslesen.

Wie gezeigt, ist es dank der vielen Hilfsmittel der Java Runtime recht einfach möglich, eine erweiterbare Anwendung zu implementieren. Voraussetzung ist, dass man die Hilfsmittel kennt und sie richtig zusammenfügen kann.

Dynamische Kaffee-Grüße, Kenny

P.S.: Der Code wurde nur unter unixoiden Betriebssystemen getestet. Wenn ein Windows-Nutzer einen Test damit durchführen sollte, würde ich mich freuen, zu erfahren, ob er auf Anhieb funktioniert hat. 🙂

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.