英文:
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<?> 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);
}
}
}
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<?> pClass = classLoader.loadClass(Parent.class.getName());
Class<?> 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<?> 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<?> myAppleScriptEngine = classLoader.loadClass(MyAppleScriptEngine.class.getName());
// false, why? AppleScriptEngine is loaded by JAVA bootstrap-classloader
System.out.println(AppleScriptEngine.class.isAssignableFrom(myAppleScriptEngine));
Class<?> 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<?> 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:
-
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!
-
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论