Writing a post-delegation classloader

22

Sometimes in Java you need to load a set of classes that are isolated from the classpath. For instance, you might need to support the capability to plugin a library to your main application and you want to be able to isolate the libraries used by the plugins from your main application.

In this situation, you need to instantiate each plugin in a classloader that subverts the normal classloader delegation hierarchy. Java classloading usually works by first checking the parent for a loaded class, then checking the current classloader if the class is not found. This is the recommended and expected behavior for a classloader.

However, in the plugin scenario, you want just the opposite behavior. You want to check your own classloader first so that the plugin’s libraries are used before any used by the main application. You then must delegate to the parent classloader after checking your own classloader so that any shared interfaces or libraries and all the system classloaders are still checked.

A key design point is that the plugin should integrate with the main application entirely through interfaces or classes that are defined in the parent classloader. Most commonly, the main application defines interfaces and the plugin returns implementations of those interface. Usually the plugin is loaded in the main application via reflection to force the use of the plugin classloader.

It is very important that the plugin not include these shared interfaces in its classloader as the post-delegation classloader will load those instead of the ones in the main application. Classes (even from the same identical jar) loaded in different classloaders are seen as different classes by the Java VM. This will typically cause a Java VM to freak out and throw a LinkageError. So, don’t do that.

In the rest of this post, I’ll show how to implement the classloader and call the classloader, and then put it together in an example that demonstrates two versions of a class being used in the same application.

First, the classloader. We will extend URLClassLoader as it provides a ton of useful capability for loading classes from a classpath defined as a set of URLs. Normally when you create a classloader, you are expected to override the findClass method. This is the method that actually takes a class name and returns a Class. However, we’re fine with the findClass implementation, we really want to manipulate the delegation order and that is defined in the loadClass method.

The implementation will look something like this:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // First check whether it's already been loaded, if so use it
        Class loadedClass = findLoadedClass(name);
        
        // Not loaded, try to load it 
        if (loadedClass == null) {
            try {
                // Ignore parent delegation and just try to load locally
                loadedClass = findClass(name);
            } catch (ClassNotFoundException e) {
                // Swallow exception - does not exist locally
            }
            
            // If not found locally, use normal parent delegation in URLClassloader
            if (loadedClass == null) {
                // throws ClassNotFoundException if not found in delegation hierarchy at all
                loadedClass = super.loadClass(name);
            }
        } 
        // will never return null (ClassNotFoundException will be thrown)
        return loadedClass;
    }

Other than implementing the constructors, that’s it!

Let’s look at a sample application. First, let’s define a trivial application with plugins.

// Simple "common" class that will have multiple versions
public class CommonClass {
    public String toString() {
        return "Common class version 1";
    }
}

// Simple plugin interface
public interface Plugin {
    public String test();
}

// The application
public class Application {
    public static void main(String arg[]) throws Exception {
        // Use the initial version of CommonClass in the main application
        System.out.println("Main says: " + new CommonClass().toString());

        // Instantiate the PostDelegationClassLoader with a classpath pointing to a plugin
        URL[] classpath = new URL[] { new URL("file:../plugin/") };
        PostDelegationClassLoader classLoader = new PostDelegationClassLoader(classpath);

        // Use the classloader to instantiate the plugin in the classloader.  Since this is done 
        // by reflection in a classloader, there is no need for the main application to have 
        // the PluginImpl class at compile time, although it must have the Plugin interface.
        Plugin plugin = (Plugin) classLoader.loadClass("PluginImpl").newInstance();

        // Use the plugin, which will use a different version of CommonClass
        System.out.println("Plugin says: " + plugin.test());
    }
}

Then, we need to actually define the plugin and second version of CommonClass:

// Second version of common class used by the plugin
public class CommonClass {
    public String toString() {
        return "Common class version 2";
    }
}

// Plugin implementation which uses CommonClass
public class PluginImpl implements Plugin {
    public String test() {
        return new CommonClass().toString();
    }
}

We can then compile these two sets of code and run the application. Note that at runtime, the main application will not include the PluginImpl and CommonClass (version 2) code in the system classpath. Instead, they will be included only in the post-delegation classloader.

At runtime you will see:

Main says: Common class version 1
Plugin says: Common class version 2

This shows that two versions of the same class are being used in the same application without conflict through classloader isolation. This is a trivial example but this technique is often needed to use different versions of the same library in different parts of an application. This technique is used pervasively in servlet containers and application servers.

Attached here are two Eclipse projects encompassing the main and plugin parts of this example. You should be able to unzip these projects in an Eclipse workspace and be ready to run. All code is included.

Main (5 KB) Plugin (2 KB)

Comments are closed.