Sunday, August 03, 2008

Dynamically loading plugins in Java (without OSGi)

In a previous post, I have discussed a simple solution to find dynamically find Guice Modules located in jar files, placed in a given directory but not initially part of an application classpath.

After some discussions, on the Guice mailing list, linked to that topic, I found that there had been some similar efforts to do the same (or almost the same); in particular, Java 6 brought the java.util.ServiceLoader class that does the same, except that the dynamic jar files discovery is left to you (by creating your own ClassLoader to include these jars).

Based on the discussions above, I have reworked my prototype to make it generic, thus independent of Guice (but usable with Gucie Modules of course).

This is what I'll present in this post today. Some code (or even entire classes) from my previous post have been reused.

The basic API for "service discovery" lies in the PluginDiscoveryManager interface:
public interface PluginDiscoveryManager
{
public <T> Iterable<T> getPluggedServices(Class<T> service);
}
Usage is quite straightforward once you got an implementation of that interface, you just need to call getPluggedServices() passing it the type of services you want to dynamically load:
PluginDiscoveryManager manager = ...;
for (IMyService service: manager.getPluggedServices(IMyService.class))
{
// Do something with service
service.doSomething();
}
My prototype has currently two implementations for PluginDiscoveryManager: ClassPathPluginDiscoveryManager and DirectoryPluginDiscoveryManager.

ClassPathPluginDiscoveryManager looks for services in jars that are already in the classpath, whereas DirectoryPluginDiscoveryManager will dynamically add all jars present in a given directory to the classpath before searching for services.

Let's take a look at ClassPathPluginDiscoveryManager first (I removed all exception handling code for clarity):
public class ClassPathPluginDiscoveryManager implements PluginDiscoveryManager
{
static private final String MANIFEST_PATH = "META-INF/MANIFEST.MF";
static private final String MANIFEST_PLUGIN_ATTRIBUTE = "Plugins";

public ClassPathPluginDiscoveryManager(ClassLoader loader)
{
_loader = loader;
}

public <T> Iterable<T> getPluggedServices(Class<T> clazz)
{
List<T> services = new ArrayList<T>();
// Get all MANIFEST files in the classpath
Enumeration<URL> manifests = _loader.getResources(MANIFEST_PATH);
while (manifests.hasMoreElements())
{
addOnePluginServices(manifests.nextElement(), clazz, services);
}
return services;
}

private <T> void addOnePluginServices(
URL manifestUrl, Class<T> clazz, List<T> services)
{
InputStream input = null;
input = manifestUrl.openStream();
Manifest manifest = new Manifest(input);
String implementations =
manifest.getMainAttributes().getValue(MANIFEST_PLUGIN_ATTRIBUTE);
if (implementations != null)
{
for (String impl: implementations.split("[ \t]+"))
{
addOneService(impl, clazz, services);
}
}
}

private <T> void addOneService(
String implementation, Class<T> clazz, List<T> services)
{
Class<?> service = Class.forName(implementation, false, _loader);
// Check is service implements clazz
if (clazz.isAssignableFrom(service))
{
services.add(clazz.cast(service.newInstance()));
}
}

private final ClassLoader _loader;
}
As in my previous prototype with Guice, service implementation classes are discovered by checking a special attribute in the MANIFEST file:
Plugins: package1.MyService1Impl package2.MyService2Impl
What's new here is that you can mix different types of services in the attribute (with Guice, they all had to be Guice Modules). ClassPathPluginDiscoveryManager.getPluggedServices() will retain only implementations for the required service class.

Now DirectoryPluginDiscoveryManager is simply based on ClassPathPluginDiscoveryManager but creates a special ClassLoader to dynamically inspect the required directories:
public class DirectoryPluginDiscoveryManager extends ClassPathPluginDiscoveryManager
{
public DirectoryPluginDiscoveryManager(
boolean includeSubDirs, File... directories)
{
this(Thread.currentThread().getContextClassLoader(), includeSubDirs,
directories);
}

public DirectoryPluginDiscoveryManager(
ClassLoader parent, boolean includeSubDirs, File... directories)
{
super(ClassLoaderHelper.buildClassLoader(includeSubDirs, parent, directories));
}
}
All ClassLoader support is provided by the ClassLoaderHelper class, already presented in my previous post.

The main advantage of the solution presented here is its simplicity: PluginDiscoveryManager implementations are not complex, writing a supported plugin is very simple, just a few lines in your ant or maven configuration file, e.g.
<build>
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Plugins>package1.MyService1Impl package2.MyService2Impl</Plugins>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
in the maven pom.xml of one actual plugin project.

Additionally, this solution works with Java 5 or higher, no need to wait for JSR-277!

You can find the full code of this proto, along with javadoc, tests and maven configuration in this zip archive.

3 comments:

  1. What I would like to have is a solution that works both from inside a JAR file and outside of it. The latter is typical during development, the former during deployment.

    The best I could come up with was to have a predefined location where one puts a property file with configuration data. Then one can use ClassLoader.getResources() to find all of those property files.

    ReplyDelete
  2. Well I think it should be possible with just a few changes the ClassLoaderHelper class. All the plumbing is there, we just need to add some args to the constructor (or a new constructor) to specify you also want subdirectories in the classpath.

    ReplyDelete
  3. I realize that this blog is from 2008. I was wondering what is the best technique for dynamically loading plugins today.
    Is using OSGI for dynamic loading of plugins the best solution today?

    ReplyDelete