前几天跟同事聊怎么用自定义类加载器加载java.lang.String的问题,正好又遇到一个类加载器的问题,决定花点时间研究一下。
在查看源码研究的过程中,我发现很多人都有个误区:双亲委派机制不能被打破,不能使用自定义类加载器加载java.lang.String,也是由于这个原因。
但是事实上并不是,只要重写ClassLoader的loadClass()方法,就能打破了。
如下是我写的一个简单的自定义类加载器:
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls) {
super(urls);
}
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
//只对MyClassLoader和String使用自定义的加载,其他的还是走双亲委派
if(name.equals("MyClassLoader") || name.equals("java.lang.String")) {
return super.findClass(name);
} else {
return getParent().loadClass(name);
}
}
public static void main(String[] args) throws Exception {
//urls指定自定义类加载器的加载路径
URL url = new File("J:/apps/demo/target/classes/").toURI().toURL();
URL url3 = new File("C:/Program Files/Java/jdk1.8.0_191/jre/lib/rt.jar").toURI().toURL();
URL[] urls = {
url
, url3
};
MyClassLoader myClassLoader = new MyClassLoader(urls);
Class> c1 = MyClassLoader.class.getClassLoader().loadClass("MyClassLoader");
Class> c2 = myClassLoader.loadClass("MyClassLoader");
System.out.println(c1 == c2); //false
System.out.println(c1.getClassLoader()); //AppClassLoader
System.out.println(c2.getClassLoader()); //MyClassLoader
System.out.println(myClassLoader.loadClass("java.lang.String")); //Exception
}
}
输出结果如下:
false
sun.misc.Launcher$AppClassLoader@18b4aac2
MyClassLoader@3feba861
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at MyClassLoader.loadClass(MyClassLoader.java:14)
at MyClassLoader.main(MyClassLoader.java:35)
加载同一个类MyClassLoader,使用的类加载器不同,说明我这里是打破了双亲委派机制的,但是尝试加载String类的时候报错了。
看代码是ClassLoader类里面的限制,只要加载java开头的包就会报错。所以真正原因是JVM安全机制,并不是因为双亲委派。
https://www.cnblogs.com/idea360/p/12377464.html
这篇文章比较详细的讲解了类加载器的一些机制,但是最后在JVM安全机制这里就没下文了。难道真的就没办法了吗?
看代码既然是ClassLoader里面的代码做的限制,那把ClassLoader.class修改了不就好了吗。
抱着试一试的心态,我自己写了个java.lang.ClassLoader,把preDefineClass()方法里那段if直接删掉,再用编译后的class替换rt.jar里面的,直接通过命令jar uvf rt.jar java/lang/ClassLoader/class即可。
不过事与愿违,修改之后还是报错:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at com.example.demo.mini.test.MyClassLoader.loadClass(MyClassLoader.java:17)
at com.example.demo.mini.test.MyClassLoader.main(MyClassLoader.java:31)
仔细看报错和之前的不一样了,这次是native方法报错了。
这就比较难整了,看来要自己重新编译个JVM才行了。理论上来说,编译JVM的时候把校验的代码去掉就行了。
正好之前自己编译过JVM,很多东西都是现成的,说干就干。找到defineClass1的native代码,进一步定位到systemDictionary.cpp的resolve_from_stream()方法:
直接把这里的if整个干掉。
顺手找到jdk/src/share/classes/java/lang/ClassLoader.java类,把这个里面的if也干掉。然后重新编译。
定位代码的过程比较繁琐,这里推荐一篇文章:
https://www.dazhuanlan.com/2019/11/19/5dd2e596bc024/
使用新编译的jdk测试MyClassLoader:
可以看到String类的classLoader是MyClassLoader了,验证成功。
结论:自定义类加载器加载java.lang.String,必须修改jdk的源码,自己重新编译个JVM才行。
顺便说下编译JVM过程中遇到的一个坑
编译的时候报错:
## Starting jdk
Compiling 9417 files for BUILD_JDK
Killed
CompileJavaClasses.gmk:316: recipe for target '/data/github/jdk-jdk8-b120/build/linux-x86_64-normal-server-release/jdk/classes/_the.BUILD_JDK_batch' failed
make[2]: *** [/data/github/jdk-jdk8-b120/build/linux-x86_64-normal-server-release/jdk/classes/_the.BUILD_JDK_batch] Error 137
BuildJdk.gmk:64: recipe for target 'classes-only' failed
make[1]: *** [classes-only] Error 2
/data/github/jdk-jdk8-b120//make/Main.gmk:115: recipe for target 'jdk-only' failed
make: *** [jdk-only] Error 2
本来我前几次编译都成功了,后面开始编译就一直报这个错,根据信息完全看不出是哪里问题。
http://freebsd.1045724.x6.nabble.com/Failure-compiling-java-openjdk8-td6093672.html
然后看到这篇文章说可能是内存不足,试着把虚拟机里面的vscode关掉,果然就编译通过了。