Search code examples
javaclassloader

java class identify about classloader


I have doubt with Class's identify,Class's identify should be The same classloader instance + The same class full path。But I make some test cases,it doesn't work。

I have a self-defined ClassLoader:

import java.io.*;

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

The Sample class:

public class Sample {

    private Sample instance; 

    public void setSample(Object instance) { 
        this.instance = (Sample) instance; 
    } 
}

And a test case:

String classDataRootPath = "/Users/haolin/Github/jvm/target/classes";
FileSystemClassLoader fscl1 = new    FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "me.hao0.jvm.classloader.Sample";
try {
    Class<?> class1 = fscl1.loadClass(className);
    Object obj1 = class1.newInstance();
    Class<?> class2 = fscl2.loadClass(className);
    Object obj2 = class2.newInstance();
    Method setSampleMethod = class1.getMethod("setSample", Object.class);
    setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
    e.printStackTrace();
}

It should occur ClassCastException when setSampleMethod.invoke(obj1, obj2),because obj1 and obj2 are different Class (their ClassLoader is different),but the code works well, doesn't throw ClassCastException

Could someone suggest it?


Solution

  • You would be correct except for one important detail. Classloaders exist in a hierarchy, and every ClassLoader delegates to its parent before attempting to load a class itself. So if a common ancestor of your classloaders finds the requested class, then your classloaders will both return the same instance of that class. Consider this example:

    public class Foo {
        public void isFoo(Object obj) {
            System.out.println("object is a Foo: " + (obj instanceof Foo));
            Foo foo = (Foo) obj;
        }
    }
    

    And the test:

    import java.lang.reflect.Method;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class Test {
        public static void main(String[] args) throws Exception {
            File path = new File(".");
            URL[] urls = new URL[] { path.toURI().toURL() };
            URLClassLoader cl1 = new URLClassLoader(urls);
            URLClassLoader cl2 = new URLClassLoader(urls);
    
            Class c1 = cl1.loadClass("Foo");
            Class c2 = cl2.loadClass("Foo");
            System.out.println("same class instance: " + (c1 == c2));
    
            Object o1 = c1.newInstance();
            Object o2 = c2.newInstance();
    
            Method m = c1.getDeclaredMethod("isFoo", Object.class);
            m.invoke(o1, o2);
        }
    }
    

    The output is:

    same class instance: true
    object is a Foo: true
    

    This occurs because the current directory is part of the default classpath, so the parent classloader finds and loads Foo, and both custom classloaders return the instance from their parent.

    Now, make this change to the test class and recompile:

    File path = new File("foo");
    

    Create the foo/ directory and move Foo.class there. Now the output is:

    same class instance: false
    object is a Foo: false
    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at Test.main(Test.java:21)
    Caused by: java.lang.ClassCastException: Foo cannot be cast to Foo
        at Foo.isFoo(Foo.java:4)
        ... 5 more
    

    This is what you expected. The system classloader can no longer find Foo, so the custom classloaders load separate instances of it. The JVM sees the two Class instances as different classes even though they're identical, and the cast fails.