Java: Klasse mit korrektem Interface dynamisch laden

21.10.2015 yahe code java legacy

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 aufgesetzt, das aus drei Teilen besteht:

  1. 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.
  2. Die Klassenbibliothek "plugin", in der die Erweiterung implementiert ist: Die Erweiterung ist eine Klasse, die das Interface aus der Klassenbiliothek "lib" implementiert.
  3. 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:
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:
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.
  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. Anschließend können wir die Main-Klasse mit Leben füllen. Wir beginnen mit ein paar Definitionen, die später in den Methoden benötigt werden. Die Methode "checkName()" dient dazu, herauszufinden, ob ein angegebener Plugin-Pfad tatsächlich korrekt ist. Als Pluginpfad verwenden wir die Form "<Klassenname>@<Bibliothekspfad>". Die Methode "loadClass()" verwendet den URLClassLoader, um die angegebene Klasse zu laden. Die Methode "createObject()" erzeugt eine Instanz der geladenen Klasse. Und in der Methode "main()" wird schlussendlich alles zusammengefügt:
package com.example.loadclass.app;

import com.example.loadclass.lib.ExampleInterface;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;

public class Main {

    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;

    // structure is "<class>@<path>", where "<path>" may be a folder or a *.jar file
    protected 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;
    }

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

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

    public static void main(String[] args) {
        ExampleInterface lObject = (ExampleInterface)createObject(APP_PLUGIN_PATH,
                                                                  ExampleInterface.class);
        if (null != lObject) {
            System.out.println(lObject.returnValue());
        }
    }

}

Wenn ihr die Pakete "lib.jar", "plugin.jar" und "app.jar" alle einmal erzeugt habt, 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.


Search

Categories

administration (40)
arduino (12)
calcpw (2)
code (33)
hardware (16)
java (2)
legacy (113)
linux (27)
publicity (6)
review (2)
security (58)
thoughts (21)
windows (17)
wordpress (19)