javapluginsurlclassloader

Issues with making plugin loader for a java app


I am trying to make a plugin system for an app I'm working on, what I want is to have a directory that can be filled with plugin jar files that is then loaded into the project when it starts. I have made the plugin structure dependency and have test plugins, the part that I am having problems with is the plugin loading system. So far I have been using this blog post as reference but the class loader provided in it does not work.

The structure that I am aiming for with this plugin system is like this:

C:.
├───commands
│   ├───plugin
│   │    └plugin.jar
│   └───plugin2
│        └plugin2.jar
└───src
    └───Java app
         ├───controller
         │   └───loaders
         ├───model
         └───view

Inside the loaders package are the following two classes that were taken from the blog:

PluginLoader.class

public class PluginLoader {
    private final Map<String, CommandFactory> commandFactoryMap = new HashMap<>();
    private final File pluginsDir;
    private final AtomicBoolean loading = new AtomicBoolean();

    public PluginLoader(final File pluginsDir) {
        this.pluginsDir = pluginsDir;
    }

    public void loadCommands() {
        if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
            System.err.println("Skipping Plugin Loading. Plugin dir not found: " + pluginsDir);
            return;
        }

        if (loading.compareAndSet(false, true)) {
            final File[] files = requireNonNull(pluginsDir.listFiles());
            for (File pluginDir : files) {
                if (pluginDir.isDirectory()) {
                    loadCommand(pluginDir);
                }
            }
        }
    }

    private void loadCommand(final File pluginDir) {
        System.out.println("Loading plugin: " + pluginDir);
        final URLClassLoader pluginClassLoader = createPluginClassLoader(pluginDir);
        final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(pluginClassLoader);
// broken for loop
            for (OmenChatPlugin omenChatPlugin : ServiceLoader.load(OmenChatPlugin.class, pluginClassLoader)) {
                installPlugin(omenChatPlugin);
            }
        } finally {
            Thread.currentThread().setContextClassLoader(currentClassLoader);
        }
    }


    private void installPlugin(final OmenChatPlugin plugin) {
        System.out.println("Installing plugin: " + plugin.getClass().getName());
        for (CommandFactory c : plugin.getCommandFactories()) {
            commandFactoryMap.put(c.name(), c);
        }
    }

    private URLClassLoader createPluginClassLoader(File dir) {
        final URL[] urls = Arrays.stream(Optional.of(dir.listFiles()).orElse(new File[]{}))
                .sorted()
                .map(File::toURI)
                .map(this::toUrl)
                .toArray(URL[]::new);

        System.out.println("cpcl: " + Arrays.toString(urls));

        return new PluginClassLoader(urls, getClass().getClassLoader());
    }

    private URL toUrl(final URI uri) {
        try {
            return uri.toURL();
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    public CommandFactory getCommandFactory(String name) {
        return commandFactoryMap.get(name);
    }
}

PluginClassLoader.class

public class PluginClassLoader extends URLClassLoader {
    public static final List<String> SHARED_PACKAGES = Arrays.asList(
            "com.github.OMEN44",
            "com.github.omen"
    );

    private final ClassLoader parentClassLoader;

    public PluginClassLoader(URL[] urls, ClassLoader parentClassLoader) {
        super(urls, null);
        this.parentClassLoader = parentClassLoader;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // has the class loaded already?
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass == null) {
            final boolean isSharedClass = SHARED_PACKAGES.stream().anyMatch(name::startsWith);
            if (isSharedClass) {
                loadedClass = parentClassLoader.loadClass(name);
            } else {
                loadedClass = super.loadClass(name, resolve);
            }
        }

        if (resolve) {      // marked to resolve
            resolveClass(loadedClass);
        }
        return loadedClass;
    }
}

When I try to load the plugins using these classes it prints the URL generated in the URLClassLoader method but when it gets to the for loop that I've commented the ServiceLoader returns an empty list. If anyone has a solution to this issue or knows a better method to use your help is appreciated.

Here is a link to the dependency and app github.


Solution

  • I found the issue, when you jar the plugin you need to add a file in the META-INF folder, the project tructure should look like this:

    .
    ├───main
    │   ├───java
    │   │   └───com
    │   │       └───example
    │   │           └───Plugin.java
    │   └───resources
    │       └───META-INF
    │           └───services
    │               └───com.library.PluginInterface
    └───test
        └───java
    

    The com.library.PluginInterface file should contain the file path to the class in your plugin that is being loaded with this example it would contain: com.example.Plugin. com.library is the file path to the interface or abstract class in library shared by the app and the plugin.