Java distinguishes class files from their fully qualified class names. So unless you use some technology like OSGi or your own Classloader hack, you can't effectively use two versions of the same library in your Java program. In this post, Ill give you a piece of code I've written to solve Jar Hell.
In Java the classes are loaded by a hierarchy of ClassLoaders. There are three types of Classloaders in the Classloader hierarchy.
- Bootstrap Classloader
- Extensions Classloader
- System Classloader
Bootstrap Classloader loads the core java classes like java.lang.* when the JVM starts up. Extensions Classloader loads jar files in the extension directory (JAVA_HOME/jre/lib/ext). These are the extensions of standard core java classes. System Classloader is the one that loads your classes. When Java instantiates an Object, it searches for the Class which is loaded into the Runtime. This search happens in a funny way.
Say you are instantiating a java.lang.String object in your Main java class which is loaded by the System Classloader. The Class java.lang.String is looked for by calling the findClass(classname) method in that System Classloader. What the findClass(classname) method does is that it calls the findclass() method in its parent Classloader which is the Extensions Classloader. And the findclass() method in the Extensions Classloader delegates the call to the findclass() method of the Bootstrap Classloader which is its parent. If the parent has loaded the class already, the parent will return the Class and if the parent returns null, the child Classloader will try to load that class. This way Java makes sure that it loads the correct java.lang.String class and even if you have implemented your own String class with java.lang.String fully qualified class name, it wont be loaded since the Bootstrap Classloader will already have loaded that class.
If you instantiate an Object for your own Class say com.you.test.YourTestClass, even if the findClass() method of the System Classloader which loaded your Main class delegates it to its parents, the parent classloaders will not know about a class called com.you.test.YourTestClass, so it will come back to the System Classloader by returning null, and the object will be created from the class loaded from the System Classloader.
The problem with this approach is that if you have two classes with the same fully qualified class name say com.you.test.YourClassCausingJarHell in two Jars, the System Classloader will return the one it has already loaded, resulting in a Jar Hell. To solve this we can use a ParentLastClassLoader like follows to load the classes from the jar files you specify irrespective of the fact that the Classloaders have already loaded a class with that fully qualified class name. The ParentLastClassloader will first try to load the Classes from the given Jars and it will delegate to its parents only if it can't find the Classes hence named ParentLast. Take a look at the implementation and note that it puts the already loaded classes in a data structure to use when needed.
public class ParentLastClassLoader extends ClassLoader { private String[] jarFiles; //Paths to the jar files private Hashtable classes = new Hashtable(); //used to cache already defined classes public ParentLastClassLoader(ClassLoader parent, String[] paths) { super(parent); this.jarFiles = paths; } @Override public Class findClass(String name) throws ClassNotFoundException { System.out.println("Trying to find"); throw new ClassNotFoundException(); } @Override protected synchronized Class loadClass(String className, boolean resolve) throws ClassNotFoundException { System.out.println("Trying to load"); try { System.out.println("Loading class in Child : " + className); byte classByte[]; Class result = null; //checks in cached classes result = (Class) classes.get(className); if (result != null) { return result; } for(String jarFile: jarFiles){ try { JarFile jar = new JarFile(jarFile); JarEntry entry = jar.getJarEntry(className.replace(".","/") + ".class"); InputStream is = jar.getInputStream(entry); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); int nextValue = is.read(); while (-1 != nextValue) { byteStream.write(nextValue); nextValue = is.read(); } classByte = byteStream.toByteArray(); result = defineClass(className, classByte, 0, classByte.length, null); classes.put(className, result); } catch (Exception e) { continue; } } result = (Class) classes.get(className); if (result != null) { return result; } else{ throw new ClassNotFoundException("Not found "+ className); } } catch( ClassNotFoundException e ){ System.out.println("Delegating to parent : " + className); // didn't find it, try the parent return super.loadClass(className, resolve); } } }
When instantiating this ParentLastClassLoader, the current SystemClassLoader which loaded your Main class will be set as its parent. So you can write your Main class like follows
public class MainClass { public static void main(String [] args) { //This will instantiate the object from the allready loaded class from the System Classloader com.you.test.YourClassCausingJarHell yourClass = new com.you.test.YourClassCausingJarHell(); String[] pathsToJars = {"jar_path1", "jar_path2"}; ClassLoader loader = new ParentLastClassLoader(Thread.currentThread().getContextClassLoader(), pathsToJars); Class correctClass = loader.loadClass("com.you.test.YourClassCausingJarHell"); Method theMethod = correctClass.getMethod("theMethodYouWant"); //This calls the right method from the right class. theMethod.invoke(correctClass.getConstructor().newInstance()); } }