Java在不同的类加载器下从子类向超类进行类型转换

huangapple go评论76阅读模式
英文:

Java cast from subclass to superclass under different classloader

问题

我知道由不同类加载器加载的Class实例无法相互强制转换。

但是如果一个Class扩展了另一个Class会怎样呢?我进行了一个实验,结果令人困惑。这是我定义的ClassLoader

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            if (name.startsWith("java")) {
                return super.loadClass(name);
            }

            String filename = "/" + name.replaceAll("\\.", "/") + ".class";
            InputStream is = getClass().getResourceAsStream(filename);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (Throwable e) {
            throw new ClassNotFoundException(name);
        }
    }
}

以及实验代码:

// 这些类将由MyClassLoader加载
class Parent { }
class Child extends Parent { }
class MyCalendarData_aa_DJ extends CalendarData_aa_DJ { }
class MyAppleScriptEngine extends AppleScriptEngine { }
class MyBufferedReader extends BufferedReader {
    public MyBufferedReader(Reader in) {
        super(in);
    }
}

public class DifferentClassLoaderCast {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new MyClassLoader();

        Class<?> pClass = classLoader.loadClass(Parent.class.getName());
        Class<?> cClass = classLoader.loadClass(Child.class.getName());
        // true,因为pClass和cClass由同一类加载器加载
        System.out.println(pClass.isAssignableFrom(cClass));
        // false,不同的类加载器
        System.out.println(Parent.class.isAssignableFrom(cClass));
        // true,为什么?
        System.out.println(Object.class.isAssignableFrom(pClass));

        Class<?> myCalendarData_aa_DJClass = classLoader.loadClass(MyCalendarData_aa_DJ.class.getName());
        // false,CalendarData_aa_DJ由JAVA ext-classloader加载
        System.out.println(CalendarData_aa_DJ.class.isAssignableFrom(myCalendarData_aa_DJClass));

        Class<?> myAppleScriptEngine = classLoader.loadClass(MyAppleScriptEngine.class.getName());
        // false,为什么?AppleScriptEngine由JAVA bootstrap-classloader加载
        System.out.println(AppleScriptEngine.class.isAssignableFrom(myAppleScriptEngine));

        Class<?> myBufferedReader = classLoader.loadClass(MyBufferedReader.class.getName());
        // true,为什么?BufferedReader由JAVA bootstrap-classloader加载
        System.out.println(BufferedReader.class.isAssignableFrom(myBufferedReader));
    }
}

似乎由MyClassLoader加载的子类可以强制转换为由以java开头的包或内置类加载的超类?

英文:

I know that Class instance loaded by different class loader can't be cast to each other.

But what if the one Class extends the other? I did an experiment and the result is confusing. Here is the ClassLoader I define:

public class MyClassLoader extends ClassLoader {
    @Override
    public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {
        try {
            if (name.startsWith(&quot;java&quot;)) {
                return super.loadClass(name);
            }

            String filename = &quot;/&quot; + name.replaceAll(&quot;\\.&quot;, &quot;/&quot;) + &quot;.class&quot;;
            InputStream is = getClass().getResourceAsStream(filename);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (Throwable e) {
            throw new ClassNotFoundException(name);
        }
    }
}

And the experiment code:

// These classes will be loaded by MyClassLoader
class Parent { }
class Child extends Parent { }
class MyCalendarData_aa_DJ extends CalendarData_aa_DJ { }
class MyAppleScriptEngine extends AppleScriptEngine { }
class MyBufferedReader extends BufferedReader {
    public MyBufferedReader(Reader in) {
        super(in);
    }
}

public class DifferentClassLoaderCast {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new MyClassLoader();

        Class&lt;?&gt; pClass = classLoader.loadClass(Parent.class.getName());
        Class&lt;?&gt; cClass = classLoader.loadClass(Child.class.getName());
        // true, as pClass and cClass are loaded by same classloader
        System.out.println(pClass.isAssignableFrom(cClass));
        // false, different classloader
        System.out.println(Parent.class.isAssignableFrom(cClass));
        // true, why?
        System.out.println(Object.class.isAssignableFrom(pClass));

        Class&lt;?&gt; myCalendarData_aa_DJClass = classLoader.loadClass(MyCalendarData_aa_DJ.class.getName());
        // false, CalendarData_aa_DJ is loaded by JAVA ext-classloader
        System.out.println(CalendarData_aa_DJ.class.isAssignableFrom(myCalendarData_aa_DJClass));

        Class&lt;?&gt; myAppleScriptEngine = classLoader.loadClass(MyAppleScriptEngine.class.getName());
        // false, why? AppleScriptEngine is loaded by JAVA bootstrap-classloader
        System.out.println(AppleScriptEngine.class.isAssignableFrom(myAppleScriptEngine));

        Class&lt;?&gt; myBufferedReader = classLoader.loadClass(MyBufferedReader.class.getName());
        // true, why? BufferedReader is loaded by JAVA bootstrap-classlaoder
        System.out.println(BufferedReader.class.isAssignableFrom(myBufferedReader));
    }
}

It seems that subclass loaded by MyClassLoader can be cast to superclass loaded by bootstrap class loader under package starts with java or built-in class?

答案1

得分: 3

System.out.println(Object.class.isAssignableFrom(pClass));
// true,为什么?
// 这个很明显。Object 是 `java.lang.Object`,如果完全合格的名称以 java 开头,你会相当笨拙地调用 `super.loadClass`。这意味着 Object.class 的加载器是系统加载器,对于所有的加载操作都是如此:无论是类加载器加载 Parent,还是系统加载器加载 Parent,它们都基于这样的概念,即 j.l.Object.class 是由系统加载器加载的:相同的类型,因此兼容。

System.out.println(AppleScriptEngine.class.isAssignableFrom(myAppleScriptEngine));
// false,为什么?AppleScriptEngine 是由 JAVA 引导类加载器加载的
// 同样的原因。相反的情况:`AppleScriptEngine` 的完全合格名称 _不是_ 以 "java" 开头。

Class<?> myBufferedReader = classLoader.loadClass(MyBufferedReader.class.getName());
System.out.println(BufferedReader.class.isAssignableFrom(myBufferedReader));
// true,为什么?BufferedReader 由 JAVA 引导类加载器加载
// 你猜对了。因为 BufferedReader 的完全合格名称以 "java" 开头。

也许你对类加载模型有所误解

类加载器采用的模型是父子关系每个类加载器都有一个父类加载器

任何类都是由某个类加载器加载的如果它在其源代码中引用了其他类它将要求自己的类加载器加载它但是该加载器可能将工作推迟给任何其他加载器这很重要你的代码将为任何以 "java" 开头的完全合格名称的类推迟加载甚至不是 "java."这是一个奇怪的选择)。否则它将自己加载被记录为类加载器的 _加载器_ 的类加载器是调用 `defineClass` 的那个在你的代码中如果你通过检查以 "java" 开头的 if你的加载器就不会调用 defineClass因此不是加载器如果没有进入该 if你将始终调用 defineClass从而成为加载器

类加载器的通用模型如下

1. 依次要求父类加载类如果可以很好我们返回该结果__这意味着该类的加载器是父类加载器而不是你__

2. 如果不能那么这个加载器将加载它冲突不太可能发生毕竟系统加载器甚至找不到它现在你 __是__ 加载器

ClassLoader 本身支持这个模型但要通过覆盖 `findClass` 来实现__而不是__ `loadClass`。`loadClass` 的默认实现将与上述完全一致首先调用父类的 loadClass 方法只有在这些方法都找不到时才会调用 findClass 完成工作

我 _强烈_ 建议你遵循这个流程并更新你的代码以扩展 findClass而不是 loadClass

如果你真的想自己加载它而不委托给父加载器那么是的覆盖 loadClass 就是你的方法但现在你必须处理这样一个事实即如果这是一个你的父类也可以找到的类你可能会遇到这样的情况你的加载器加载了 `com.foo.Example`,父加载器也加载了虽然这些类具有完全相同的名称但从 JVM 的角度来看它们是完全不相关的与彼此完全不兼容JVM 不会介意但会导致非常令人困惑的情况例如,`com.foo.Example` 类型的对象无法分配给类型为... `com.foo.Example` 的变量

如果你必须这样做注意检查是否以 "java" 开头是非常不优化的首先询问系统加载器如果它可以加载它就推迟到那里只返回它找到的内容),至少如此

通过编写加载器你想要实现什么目标有了这个了解我可以提供更多关于应该覆盖哪个方法loadClass 还是 findClass的建议
英文:
 // true, why?
System.out.println(Object.class.isAssignableFrom(pClass));

this one should be entirely obvious. Object is java.lang.Object and you rather clumsily call super.loadClass if the fully qualified name starts with java. Which means the loader of Object.class is the system loader, and this is true for all load ops: Whether classLoader loads Parent, or the system loader does, they both work off of the notion that j.l.Object.class is loaded by the system loader: The same type, therefore, compatible.

// false, why? AppleScriptEngine is loaded by JAVA bootstrap-classloader
System.out.println(AppleScriptEngine.class.isAssignableFrom(myAppleScriptEngine));

same reason. In reverse: the fully qualified name of AppleScriptEngine is not starting with "java".

Class&lt;?&gt; myBufferedReader = classLoader.loadClass(MyBufferedReader.class.getName());
// true, why? BufferedReader is loaded by JAVA bootstrap-classlaoder
System.out.println(BufferedReader.class.isAssignableFrom(myBufferedReader));

you guessed it. Because the FQN of BufferedReader starts with "java".

Perhaps you've misunderstood the classloading model.

The model that classloaders employ is a parent/child relationship. A classloader has a parent.

Any class is loaded by some classloader; if it hits any other class in its source code it will ask its own classloader to load it. But that loader may defer the job to any other loader. That's important. Your code will defer for any class whose FQN starts with "java" (and not even "java.", which is a peculiar choice). Otherwise, it loads itself. The classloader that is on record as THE loader of a class is the one that invoked defineClass. In your code, if you go via the if block that checks for starting with "java", your loader does NOT invoke defineClass, and therefore isn't the loader. If that if is not taken, you always end up invoking defineClass, making you the loader.

The common model for classloaders is this:

  1. Ask your parent(s) to load the class, in order. If it can, great. We return that result, and that means the loader of said class is the parent and not you!

  2. If not, then this loader will load it. Conflicts are unlikely; after all, the system loader couldn't even find it. Now you are the loader.

ClassLoader itself supports this model, but you get it by overriding findClass and NOT loadClass. The default impl of loadClass will do precisely as above: First calls the parents' loadClass methods, and only if those can't find it, will it invoke findClass to finish the job.

I strongly recommend you follow this flow, and update your code to extend findClass, not loadClass.

If you really want to load it yourself and NOT delegate to your parent loaders, then, yeah, overriding loadClass is how you do it. But now you have to deal with the fact that if it is a class that your parent can also find, that you can run into the scenario where your loader loaded, say, com.foo.Example, and parent did too, and whilst those classes have exactly the same name, as far as the JVM is concerned, they are completely unrelated and entirely incompatible with each other. The JVM doesn't mind, but it leads to highly confusing scenarios, where an object of type com.foo.Example cannot be assigned to a variable of type... com.foo.Example.

If you must do this, note that checking if it starts with "java" is highly suboptimal. For starters, "java." is a better fit, and for seconds, not all system classes start with "java". Ask the system loader first, if it can load it, defer to that (just return what it found), at the very least.

What are you trying to accomplish by writing a loader? With that insight, I can give more advice on which method (loadClass or findClass) is appropriate to override.

huangapple
  • 本文由 发表于 2020年7月22日 09:51:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/63025580.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定