Dynamically loading plugin jars using ServiceLoader
Asked Answered
D

3

50

I'm trying to create a plugin system for my application, and I want to start with something simple. Every plugin should be packed in a .jar file and implement the SimplePlugin interface:

package plugintest;

public interface SimplePlugin {
    public String getName();
}

Now I've created an implementation of SimplePlugin, packed in a .jar and put it in the plugin/ subdirectory of the main application:

package plugintest;

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

In the main application, I want to get an instance of PluginTest. I've tried two alternatives, both using java.util.ServiceLoader.

1. Dynamically extending the classpath

This uses the known hack to use reflection on the system class loader to avoid encapsulation, in order to add URLs the the classpath.

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

The plugins/ directory is added as expected (as one can check calling sysLoader.getURLs()), but then the iterator given by the ServiceLoader object is empty.

2. Using URLClassLoader

This uses another definition of ServiceLoader.load with a second argument of the class 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());
    }
}

Once again, the iterator has never a "next" element.

There's surely something I'm missing since it's the first time I'm "playing" with class paths and loading.

Dambro answered 19/4, 2013 at 9:58 Comment(4)
why not use a simple library with reflection support? such as apache common configuration with beans support: commons.apache.org/proper/commons-configuration/userguide/…Goaltender
@omerschleifer Because, as I said, it's the first time I'm playing with these sort of things and I want to learn how they work. Secondly, your advice is welcome but libraries have the common problem that they can do way more than what you want to do, so things are often more complicated than necessary. I don't know if it's the case, but I want to resolve to an external library only if it's my last chance.Dambro
If this is for educational purposes, have fun. but as for complexity, reflection is much easier to perform and master using a simple library like the one I've sent you rather than by inventing the wheel yourself. Reflection is, after all, a well explored area. good luckGoaltender
I wanted to use this prinzip to load plugin-jar from anywhere on the harddrive. But for some reason I get an ClassNotFoudException on the iterator.next. Have you any Idea how to use this principal with external jars?Gob
D
41

The problem was very simple. And stupid. In the plugin .jar files the /services/plugintest.SimplePlugin file was missing inside the META-INF directory, so the ServiceLoader couldn't identify the jars as services and load the class.

That's pretty much all, the second (and cleaner) way works like a charm.

Dambro answered 19/4, 2013 at 15:26 Comment(0)
T
6

Starting from Java 9 the service providing scanning will be much easier and efficient. No more need for META-INF/services.

In the interface module declaration declare:

uses com.foo.spi.Service;

And in the provider's module:

provides com.foo.spi.Service with com.bar.ServiceImplementation
Tenet answered 21/7, 2017 at 5:42 Comment(4)
So this solves the issue of finding plugins in program-specified paths - how?Scrutator
ServiceLoader isn't really well geared for this, the jar must already be in the classpath or modulepath.Tenet
You could add jars dynamically though. Check out #61264Tenet
Also, the ServiceLoader constructor has overloads that accept ClassLoader or ModuleLayer so you can still do some dynamics with it.Tenet
W
3

The solution for your application concept has been already described in Oracle Documentation (including dynamically loading JARs)

Creating Extensible Applications With the Java Platform http://www.oracle.com/technetwork/articles/javase/extensible-137159.html

on the bottom of the article you will find links to

  • source code of the example
  • Javadoc ServiceLoader API

In my opinion it is better to slightly modify Oracle's example than reinventing the wheel as Omer Schleifer said.

Whisper answered 28/1, 2015 at 19:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.