web-dev-qa-db-fra.com

Chargement dynamique des fichiers de plug-in à l'aide de ServiceLoader

J'essaie de créer un système de plugins pour mon application, et je veux commencer par quelque chose de simple. Chaque plugin doit être emballé dans un fichier .jar et implémenter l'interface SimplePlugin:

package plugintest;

public interface SimplePlugin {
    public String getName();
}

Maintenant, j'ai créé une implémentation de SimplePlugin, emballée dans un .jar et l'ai placée dans le plugin/sous-répertoire de l'application principale:

package plugintest;

public class PluginTest implements SimplePlugin {
    public String getName() {
        return "I'm the plugin!";
    }
}

Dans l'application principale, je veux obtenir une instance de PluginTest. J'ai essayé deux alternatives, toutes deux utilisant Java.util.ServiceLoader.

1. Extension dynamique du chemin de classe

Cela utilise le hack connu pour utiliser la réflexion sur le chargeur de classe système pour éviter l'encapsulation, afin d'ajouter URLs le chemin de classe.

package plugintest.system;

import plugintest.SimplePlugin;

import Java.io.File;
import Java.io.IOException;
import Java.net.URL;
import Java.net.URLClassLoader;
import Java.util.Iterator;
import Java.util.ServiceLoader;

public class ManagePlugins {
    public static void main(String[] args) throws IOException {
        File loc = new File("plugins");
        extendClasspath(loc);

        ServiceLoader<SimplePlugin> sl = ServiceLoader.load(SimplePlugin.class);
        Iterator<SimplePlugin> apit = sl.iterator();
        while (apit.hasNext())
            System.out.println(apit.next().getName());
    }

    private static void extendClasspath(File dir) throws IOException {
        URLClassLoader sysLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        URL urls[] = sysLoader.getURLs(), udir = dir.toURI().toURL();
        String udirs = udir.toString();
        for (int i = 0; i < urls.length; i++)
            if (urls[i].toString().equalsIgnoreCase(udirs)) return;
        Class<URLClassLoader> sysClass = URLClassLoader.class;
        try {
            Method method = sysClass.getDeclaredMethod("addURL", new Class[]{URL.class});
            method.setAccessible(true);
            method.invoke(sysLoader, new Object[] {udir});
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

Le répertoire plugins/est ajouté comme prévu (comme on peut vérifier l'appel de sysLoader.getURLs()), mais alors l'itérateur donné par l'objet ServiceLoader est vide.

2. Utilisation d'URLClassLoader

Cela utilise une autre définition de ServiceLoader.load Avec un deuxième argument de la classe ClassLoader.

package plugintest.system;

import plugintest.SimplePlugin;

import Java.io.File;
import Java.io.FileFilter;
import Java.io.IOException;
import Java.net.URL;
import Java.net.URLClassLoader;
import Java.util.Iterator;
import Java.util.ServiceLoader;

public class ManagePlugins {
    public static void main(String[] args) throws IOException {
        File loc = new File("plugins");

        File[] flist = loc.listFiles(new FileFilter() {
            public boolean accept(File file) {return file.getPath().toLowerCase().endsWith(".jar");}
        });
        URL[] urls = new URL[flist.length];
        for (int i = 0; i < flist.length; i++)
            urls[i] = flist[i].toURI().toURL();
        URLClassLoader ucl = new URLClassLoader(urls);

        ServiceLoader<SimplePlugin> sl = ServiceLoader.load(SimplePlugin.class, ucl);
        Iterator<SimplePlugin> apit = sl.iterator();
        while (apit.hasNext())
            System.out.println(apit.next().getName());
    }
}

Encore une fois, l'itérateur n'a jamais d'élément "suivant".

Il y a sûrement quelque chose qui me manque puisque c'est la première fois que je "joue" avec les chemins de classe et le chargement.

36
MaxArt

Le problème était très simple. Et stupide. Dans les fichiers du plugin .jar, le /services/plugintest.SimplePlugin le fichier manquait à l'intérieur du META-INF, donc ServiceLoader n'a pas pu identifier les jars en tant que services et charger la classe.

C'est à peu près tout, la deuxième façon (et plus propre) fonctionne comme un charme.

37
MaxArt

À partir de Java 9, le service de numérisation sera beaucoup plus simple et efficace. Plus besoin de META-INF/services.

Dans la déclaration du module d'interface, déclarez:

uses com.foo.spi.Service;

Et dans le module du fournisseur:

provides com.foo.spi.Service with com.bar.ServiceImplementation
3
Mordechai

La solution pour votre concept d'application a déjà été décrite dans la documentation Oracle (y compris le chargement dynamique des fichiers JAR)

Création d'applications extensibles avec la plate-forme Java http://www.Oracle.com/technetwork/ articles/javase/extensible-137159.html

au bas de l'article, vous trouverez des liens vers

  • code source de l'exemple
  • API Javadoc ServiceLoader

À mon avis, il vaut mieux modifier légèrement l'exemple d'Oracle que de réinventer la roue comme l'a dit Omer Schleifer.

1
ANTARA