关于类加载器的java类标识

标签 java classloader

我对Class的标识有疑问,Class的标识应该是同一个类加载器实例 + 同一个类完整路径。但是我做了一些测试用例,它不起作用。

我有一个自定义的类加载器:

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";
    }
}

示例类:

public class Sample {

    private Sample instance; 

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

还有一个测试用例:

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();
}

setSampleMethod.invoke(obj1, obj2)时应该会出现ClassCastException,因为obj1obj2是不同的Class(他们的ClassLoader不同),但是代码运行良好,不会抛出ClassCastException

有人可以建议一下吗?

最佳答案

除了一个重要的细节之外,你是正确的。类加载器存在于层次结构中,每个 ClassLoader 在尝试加载类本身之前都会委托(delegate)给其父级。因此,如果类加载器的共同祖先找到了所请求的类,那么您的类加载器将返回该类的相同实例。考虑这个例子:

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

测试:

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);
    }
}

输出为:

same class instance: true
object is a Foo: true

发生这种情况是因为当前目录是默认类路径的一部分,因此父类加载器找到并加载 Foo,并且两个自定义类加载器都从其父级返回实例。

现在,对测试类进行此更改并重新编译:

File path = new File("foo");

创建foo/目录并将Foo.class移动到那里。现在的输出是:

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

这就是你所期望的。系统类加载器无法再找到 Foo,因此自定义类加载器加载它的单独实例。 JVM 将两个 Class 实例视为不同的类,尽管它们是相同的,并且转换失败。

关于关于类加载器的java类标识,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36440731/

相关文章:

java - 使用 Spring Data Elasticsearch 存储库进行部分更新

java - 如何让 Byte Buddy 将多种类型加载到同一个包装类加载器中

android - 读取 SharedPreferences 中的自定义 ArrayList 调用运行时异常

java - Wildfly 和 Mybatis 存在延迟加载异常

Java:Spring Boot 类构造函数有太多 DI?

java - 使用 XStream 和 JsonHierarchicalStreamDriver 输出值,如何舍入 double ?

java - Java 中 int 与 Integer 的用法

java - 尝试更新 Hibernate 实体会导致 NonUniqueObjectionException

websphere - Websphere 中的加载约束违规

scala - 为什么SBT的线程上下文类加载器不能加载JDK类文件作为资源?