Maven Jars dynamisch laden / Plugin-Struktur erstellen

Saheeda

Top Contributor
Soll:
Ich habe einen Generator (Annotation Processing) geschrieben, der mir auf Befehl verschiedene Dateien erzeugt.
Der Generator besteht aus drei Modulen: 1 zum Auslesen der Dateien, 1 "Domain-Modell" welches die ausgelesenen Infos kapselt, 1 zum Erzeugen von Dateien (FileWriter).
Die Datei-Templates liegen dabei im Generator-Projekt selbst. Was dazu führt, dass bei jeder Änderung an einem solchen Datei-Template das Projekt neu releast werden muss.

Ist:
Das Einbinden von FileWritern soll dynamischer werden. Ich stelle mir das so vor, dass ich in einem Projekt (MeinTollerTaschenrechner) einen FileWriter definiere und den Generator einbinde.
Beim Starten des Generators scannt er das MeinTollerTaschenrechner-Projekt nach passenden FileWritern und ruft sie auf.

-------------------

Ich scheitere jetzt am "wie". Ich habe mit dem ServiceLoader von Java etwas herum experimentiert, schaffe es aber nicht, im Generator die FileWriter zu finden, die im MeinTollerTaschenrechner-Projekt definiert sind.

Hat jemand ne Idee, wie ich vorgehen könnte? Oder einen komplett anderen Ansatz?
 
Also es gibt zwei Varianten:
  1. Die "einfache" ist mit ServiceLoader.
  2. Die komplizierte ist mit OSGi.
OSGi würde in deinem Fall eher mit Kanonen auf Spatzen schiessen gleichkommen.
Also ServiceLoader (ich setze jetzt hier voraus, dass du dich darin selbst einliest).
http://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html
https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
Wenn nötig mit Maven:
https://github.com/francisdb/serviceloader-maven-plugin
http://fgaliegue.blogspot.ch/2013/06/gradle-serviceloader-support.html (vielleicht gibt es hier ja schon etwas besseres - ich habe auf die Schnelle nichts gefunden)

Ich hab in meinem Repo ein Beispiel für UI-Plugins (genutzt um Plugins als separate Fenster zu laden, aber das ist eine Detailfrage und wie du deine Pluginstruktur festlegst und am Ende im Parent-Projekt nutzt).
https://github.com/bgmf/poc/tree/master/shared/src/main/java/eu/dzim/shared/fx/integration
Dort ist das Interface für die Plugins, eine Definition für einen Service und die Implementierung (Unterverzeichnis).

In deinem Fall könntest du ein Basis-Interface machen ("Plugin") ohne Methode. Dann ein paar spezielle Interfaces für deine Zwecke (z.B. "PluginFileWriter"). Dann die "Plugin" im ServiceLoader verwenden und beim verwenden dann prüfen, welches konkrete Interface sie implementieren ("PluginFileWriter", ...).
So etwa wäre meine Rangehensweise.
 
Ich habe einen Generator (MeinGenerator), der über Annotation Processing Klassen ausliest und Dateien erzeugt. Im Generator-Projekt liegen diverse Hilfsklassen für die Dateigenerierung.

Ich habe desweiteren ein anderes Projekt (MeinTollerTaschenrechner), in welchem ich den Generator benutze (Eingebunden als Maven-Dependency).

Wenn ich jetzt feststelle, dass irgendwas an den Hilfsklassen nicht stimmt, sieht der Ablauf so aus:
- Generator-Projekt auschecken
- Hilfsklassen im Generator-Projekt anpassen
- Generator-Projekt releasen
- Versionsnummer des Generator-Projekts im MeinTollerTaschenrechner-Projekt anpassen


Ich würde das Ganze jetzt gern etwas flexibler gestalten, indem die Hilfsklassen zur Dateigenerierung im MeinTollerTaschenrechner-Projekt selbst liegen.

Ich stelle mir das ungefähr so vor:
- Ich binde MeinGenerator in MeinTollerTaschenrechner ein (wie bisher auch)
- Ich führe MeinGenerator aus (wie bisher auch)
- MeinGenerator sucht im MeinTollerTaschenrechner-Projekt nach passenden Hilfsklassen zur Dateigenerierung (über ein gemeinsames Interface)
- MeinGenerator führt nacheinander alle Hilfsklassen aus

Und ich scheitere grad an den letzten beiden Punkten. Ich bin mir nichtmal sicher, ob das überhaupt so möglich ist, was ich versuche zu bauen.
 
Naja. Dein dein Generator wäre das, was in meiner Struktur "shared" ist. Deine Hilfsklassen müssten dann wieder in extra-Projekt (Modul), das denn Generator referenziert.
Dein annotation preprocessing weird fav vieleicht nicht mehr gehen. Aber sonst wüsste ich nicht, was gegen das Konzept spricht.
Und jetzt verstehe ich auch die Frage von @JuKu
 
Vermutlich nur Text. Aber solange der TO da nicht konkreter wird und uns nur unverständliche Brocken zuwirft, werden wir es wohl nicht erfahren.
 
Inwiefern ist es relevant, was für Dateien generiert werden? Die landen so oder so alle im target-Verzeichnis und der Entwickler kümmert sich dann drum, wo die hin sollen. (Das ist auch so gewollt. Es ist nicht geplant, dass irgendwas direkt ins src-Verzeichnis generiert werden soll.)
Aber wenn euch das hilft: ich generiere Java-Klassen, Properties (Sprachdateien) und JSPXs.
Eventuell kommt auch noch Javascript und HTML dazu, aber das ist noch in der Entwurfsphase.

Mein Problem ist ja grade auch eher noch konzeptioneller Natur, sonst würde ich euch Code zeigen. Aber nur n Interface oder ne Dependency aus ner pom hat keinen allzu großen Mehrwert.
 
Zuletzt bearbeitet:
Inwiefern ist es relevant, was für Dateien generiert werden?

Also sind die generierten Daten für das Problem komplett irrelevant?

Kannst du bitte noch einmal dein Problem konkretisieren?
Soll:
Ich habe mit dem ServiceLoader von Java etwas herum experimentiert, schaffe es aber nicht, im Generator die FileWriter zu finden, die im MeinTollerTaschenrechner-Projekt definiert sind.

Was genau tust du mit dem ServiceLoader bzw. was willst du erreichen?

Bezüglich FileWriter:
Hast du denn einen Getter / Setter dafür?
 
Ich glaube, das wollte er ja eben nicht.
Ich denke, du müsstest das Konzept wirklich noch mal irgendwie darlegen. Ich vermute mal, hier versteht außer dir niemand Recht, was wir dir weiter empfehlen sollen, will die bisherigen Ideen ja scheinbar nicht dein Verständnis oder Ziel beiden zu haben scheinen...
 
"sie" bitte, nicht "er".


Ich versuchs nochmal mit ein paar Screenshots zu verdeutlichen:
Unter "generator_jetzt" sehr ihr einen Ausschnitt aus dem jetzigen Code-Generator:
generator_jetzt.PNG

Links sind die verschiedenen Services zum Erstellen von Dateien (~FileWriter), rechts ein Ausschnitt, wie ich momentan die verfügbaren FileWriter registriere. "FileWriter" ist das gemeinsame Interface. Die daraus resultierende Struktur sieht ungefähr so aus wie im Bild (nur mit mehr konkreten Implementierungen):
uml.PNG

Problem: Das ist alles relativ statisch. Wenn ich für ein bestimmtes Projekt einen neuen FileWriter benötige (oder die Dateien hier irgendwie anders generieren möchte), muss ich das komplette Generator-Projekt auschecken, ändern, committen und releasen.


Es wäre hier jetzt toll, wenn ich neue FileWriter registrieren könnte, ohne eben jedesmal den kompletten Generator auschecken, ändern, committen und releasen zu müssen.

----------------------------

Ich habe also n bisschen mit dem ServiceLoader rumgespielt.

Im ersten Schritt wollte ich jetzt erstmal die bereits vorhandenen FileWriter statt über die ArrayList über den ServiceLoader registrieren:
generator_soll.PNG
Seltsamerweise bekomme ich hier aber die Fehlermeldung "Provider xxx.xx.filewriter.filewriter.CommandFileWriter not a subtype". CommandFileWriter implementiert aber definitiv FileWriter (siehe UML-Diagramm).
 
- Wo genau bekommst du die Fehlermeldung? ServiceLoader#load?
- hast du das Tool mal zu einem JAR verpackt und geschaut, dass die Struktur stimmt? (speziell die META-INF-Struktur für den ServiceLoader)
- ich vermute, du hast beim Start von Eclipse aus die Probleme. Dass hatte ich nicht und ich habe auch ein Maven-Plugin sich um das Text-File mit den Plugins kümmern lassen:
Code:
            <plugin>
                <groupId>eu.somatik.serviceloader-maven-plugin</groupId>
                <artifactId>serviceloader-maven-plugin</artifactId>
                <version>1.0.7</version>
                <configuration>
                    <services>
                        <param>bla.blub.FileWriter</param>
                    </services>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
- im Moment schaust du im selben classpath nach den Plugins - da hatte ich ja in meinem Code einen etwas anderen Ansatz (er kann ein JAR File laden, oder eine Verzeichnisstruktur, ich verwende das z.b. zum Laden meiner Plugins über deren target/classes-Verzeichnis, wenn ich die Hauptanwendung von Eclipse aus testen möchte):
Java:
    private <T> Pair<ServiceLoader<T>, ClassLoader> loadDirecotryIntegrations(Path path, Class<T> clazz) {
        List<URL> fileNames = new ArrayList<>();
        try {
            fileNames.add(path.toUri().toURL());
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
        }
        URL[] array = fileNames.stream().toArray(size -> new URL[size]);
        URLClassLoader cl = new URLClassLoader(array, getClass().getClassLoader());
        return new Pair<>(ServiceLoader.load(clazz, cl), cl);
    }

Ich frage und sage all das, weil ich vermute, dass die ServiceLoader-Konfiguration nicht erkannt wird (das Text-File unter META-INF).
 
Noch einmal: Meine gesamte Plugin-Integration sieht so aus (aus einer produktiven Anwendung kopiert)
Java:
package ch.cnlab.aschwanden.ui.integration.impl;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import ch.cnlab.aschwanden.ui.integration.Plugin;
import ch.cnlab.aschwanden.ui.integration.PluginIntegrator;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.util.Pair;

public class PluginServiceLoader implements PluginIntegrator {
   
    private static final Logger LOG = LogManager.getLogger(PluginServiceLoader.class.getName());
   
    private static final String JAR_SUFFIX = "plugin.jar";
   
    private final ObservableMap<String, Plugin> plugins = FXCollections.observableMap(new TreeMap<>());
   
    public PluginServiceLoader() {
        // sonar
    }
   
    @Override
    public ObservableMap<String, Plugin> getPlugins() {
        return plugins;
    }
   
    private <T> List<Pair<ServiceLoader<T>, ClassLoader>> loadIntegrations(Path path, Class<T> clazz) {
        if (path != null) {
            if (path.toFile().isFile() && path.getFileName().toString().endsWith(JAR_SUFFIX)) {
                return Collections.singletonList(loadJarIntegrations(Collections.singletonList(path), clazz));
            }
            List<Path> jarFiles = findJarFiles(path);
            if (jarFiles.isEmpty()) {
                return Collections.singletonList(loadDirecotryIntegrations(path, clazz));
            } else {
                return jarFiles.stream().map(jarFile -> loadDirecotryIntegrations(jarFile, clazz)).collect(Collectors.toList());
            }
        } else {
            return Collections.singletonList(new Pair<>(ServiceLoader.load(clazz), getClass().getClassLoader()));
        }
    }
   
    private <T> Pair<ServiceLoader<T>, ClassLoader> loadJarIntegrations(List<Path> files, Class<T> clazz) {
        // TODO add sub-directory of the same name and all its containing jars
        URL[] array = files.stream().map(path -> {
            try {
                return path.toUri().toURL();
            } catch (MalformedURLException e) {
                LOG.error(e);
                return null;
            }
        }).filter(url -> url != null).toArray(size -> new URL[size]);
        URLClassLoader cl = new URLClassLoader(array, getClass().getClassLoader());
        return new Pair<>(ServiceLoader.load(clazz, cl), cl);
    }
   
    private List<Path> findJarFiles(Path path) {
        List<Path> fileNames = new ArrayList<>();
        // FIXME use a glob instead of the filter below
        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path)) {
            for (Path each : directoryStream) {
                if (!each.getFileName().toString().toLowerCase().endsWith(JAR_SUFFIX))
                    continue;
                fileNames.add(each);
            }
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
        }
        return fileNames;
    }
   
    private <T> Pair<ServiceLoader<T>, ClassLoader> loadDirecotryIntegrations(Path path, Class<T> clazz) {
        List<URL> fileNames = new ArrayList<>();
        try {
            fileNames.add(path.toUri().toURL());
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
        }
        URL[] array = fileNames.stream().toArray(size -> new URL[size]);
        URLClassLoader cl = new URLClassLoader(array, getClass().getClassLoader());
        return new Pair<>(ServiceLoader.load(clazz, cl), cl);
    }
   
    @Override
    public ObservableMap<String, Plugin> loadPlugins(Path... paths) {
       
        ServiceLoader<Plugin> internal = ServiceLoader.load(Plugin.class);
        Iterator<Plugin> internalIt = internal.iterator();
        while (internalIt.hasNext()) {
            Plugin plugin = internalIt.next();
            if (plugins.get(plugin.id()) != null) {
                // && CoreUtils.versionCompare(plugins.get(plugin.id()).version(), plugin.version()) <= 0) {
                continue;
            }
            plugins.put(plugin.id(), plugin);
            LOG.trace("Found and added internal plugin: {}", plugin.id());
        }
       
        if (!plugins.isEmpty())
            return plugins;
       
        for (Path path : paths) {
            if (!Files.exists(path)) {
                LOG.warn("Path {} doesn't exist.", path.toString());
                continue;
            }
            List<Pair<ServiceLoader<Plugin>, ClassLoader>> integrations = loadIntegrations(path, Plugin.class);
            for (Pair<ServiceLoader<Plugin>, ClassLoader> integration : integrations) {
                ServiceLoader<Plugin> serviceLoader = integration.getKey();
                Iterator<Plugin> iterator = serviceLoader.iterator();
                while (iterator.hasNext()) {
                    Plugin plugin = iterator.next();
                    if (plugins.get(plugin.id()) != null) {
                        // && CoreUtils.versionCompare(plugins.get(plugin.id()).version(), plugin.version()) <= 0) {
                        continue;
                    }
                    plugins.put(plugin.id(), plugin);
                    LOG.trace("Found and added plugin: {}", plugin.id());
                }
            }
        }
        return plugins;
    }
}
Dieser Code, samt Plugin-Definition etc., ist Teil des "core"-Moduls, sozusagen der Teil, den alle abhängigen GUIs wiederverwenden.

Die Implementierung des Interface (vielleicht füge es noch explizit zu deiner Implementierung hinzu, wo du wahrscheinlich nur deine Abstrakte Klasse nutzt) geschieht in separaten Modulen (die auch in separaten Gits gemanagt werden).
Dort ist im POM auch der Code-Teil von oben enthalten, damit die Meta-Inf-Struktur automatisch erstellt wird.

Teil des Haupt-Gits ist ein Modul, dass die Haupt-GUI bei mir darstellt (das Modul heisst "root" bei mir).
Dort versuche ich nun per Standart Plugins aus dem Verzeichnissen "./tools" oder "../tools" zu laden - immer vom Current Working Dir aus). Alternativ nimmt der Starter auch Parameter entgegen (wichtig für mich für das Testen, ob die Integration richtig funktioniert):
Java:
        // load directory with generated classes: --plugins=../../my-plugin/target/classes/
        // load directory with *plugin.jar files: --plugins=../../my-plugin/target/
        // load specific JAR file: --plugins=../../my-plugin/target/classes/orso-0.0.0-SNAPSHOT-plugin.jar
        // multiple entries possible: use comma as a separator
        Map<String, String> namedParams = getParameters().getNamed();
        String pluginsString = namedParams.get("plugins");
        List<Path> pathList = new ArrayList<>(Arrays.asList(Paths.get(".", "tools"), Paths.get("..", "tools")));
        if (pluginsString != null && !pluginsString.isEmpty()) {
            String[] split = pluginsString.split("[,]");
            for (String entry : split) {
                Path path = Paths.get(entry);
                if (path.toAbsolutePath().toFile().exists())
                    pathList.add(path);
            }
        }
       
        integrator = PluginIntegratorFactory.getInstance();
        integrator.loadPlugins(pathList.stream().toArray(size -> new Path[size]));

Ich hoffe, dass macht meinen (funktionierenden) Ansatz etwas klarer und vielleicht hilft dir dass ja beim Finden deines Problems.
 

Neue Themen


Zurück
Oben