Java – Handmade Classloader Isolation
In a recent project we had a typical libraries conflict problem. One component that we could control wanted a specific version of an Apache Commons library, while another component was expecting a different one. Due to external constraints we could not specify any class loading isolation at the Container level. It wasn’t an option for us. What we decided to do instead has been to use the two different classes definition at the same time. To obtain this we had to let one class be loaded by the current thread class loader and to load manually the second one; in this way the two classes still have the same fully qualified name.
The only restriction to this approach is the we had to interact with the manually loaded class only via reflection, since the current context, that is using a different class loader,
has a different definition of a class and we would be able to cast or assign a instance of the class loaded with a classloader to a variable defined in the context of the other. Our implementation is in effect a Classloader itself:
DirectoryBasedParentLastURLClassLoader extends ClassLoader
The characteristic of this Classloader is that we are passing it a file system folder path:
public DirectoryBasedParentLastURLClassLoader(String jarDir)
Our implementation scans the filesystem path to produce URLs and uses this information to pass them to a wrapped instance of a URLClassLoader that we are encapsulating with our CustomClassloader:
public DirectoryBasedParentLastURLClassLoader(String jarDir) { super(Thread.currentThread().getContextClassLoader()); // search for JAR files in the given directory FileFilter jarFilter = new FileFilter() { public boolean accept(File pathname) { return pathname.getName().endsWith('.jar'); } }; // create URL for each JAR file found File[] jarFiles = new File(jarDir).listFiles(jarFilter); URL[] urls; if (null != jarFiles) { urls = new URL[jarFiles.length]; for (int i = 0; i < jarFiles.length; i++) { try { urls[i] = jarFiles[i].toURI().toURL(); } catch (MalformedURLException e) { throw new RuntimeException( 'Could not get URL for JAR file: ' + jarFiles[i], e); } } } else { // no JAR files found urls = new URL[0]; } childClassLoader = new ChildURLClassLoader(urls, this.getParent()); }
With this setup we can override the behaviour of the main classloading functionality, giving priority to the loading from our folder and falling back to the parent classloader only if we could find the requested class:
@Override protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { try { // first try to find a class inside the child classloader return childClassLoader.findClass(name); } catch (ClassNotFoundException e) { // didn't find it, try the parent return super.loadClass(name, resolve); } }
With our CustomClassloader in place we can use it in this way:
//instantiate our custom classloader DirectoryBasedParentLastURLClassLoader classLoader = new DirectoryBasedParentLastURLClassLoader( ClassLoaderTest.JARS_DIR ); //manually load a specific class Class classManuallyLoaded = classLoader .loadClass('paolo.test.custom_classloader.support.MyBean'); //request a class via reflection Object myBeanInstanceFromReflection = classManuallyLoaded.newInstance(); //keep using the class via reflection Method methodToString = classManuallyLoaded.getMethod('toString'); assertEquals('v1', methodToString.invoke(myBeanInstanceFromReflection));
This idea for this post and part of its code come from this interesting discussion on Stackoverflow. A fully working Maven project is available on GitHub with a bunch of unit tests to verify the right behaviour.
Reference: Java – Handmade Classloader Isolation from our JCG partner Paolo Antinori at the Someday Never Comes blog.