How to solve JAR hell with a Parent Last Classload

7606 단어 자바ClassLoader
Java Classpath Hell or Jar Hell is a term used in the Java community when a wrong class is loaded from a wrong Jar into the runtime causing the system not to function properly. Say you have two classes with the same fully qualified class name in two Jars. When the two Jar files are loaded, only the first class will be loaded into the runtime. So if your program depends on the second class which is not loaded, the runtime will provide your program the first class, which is wrong. A common issue is that when you use two versions of the same library, it is most likely that the classes in these two jars have the same fully qualified class names. So in Java only one set of classes from one jar will be in the runtime and if your program depends on the second jar, i.e. the second version of the library, your program will not function as you expect. 
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 saycom.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 namedParentLast. 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()); 
     
        }
    }

    좋은 웹페이지 즐겨찾기