ClassLoader、双亲委派机制、自定义类加载器
双亲委派模型
如果一个类加载器
收到了要加载某个类的请求,它自己不会立即加载,而是委托给上一级的类加载器,一直委托到顶层,从顶层向下依次加载这个类,加载成功就跳出,否则一直往下,最终加载失败就会抛出ClassNotFoundException
。如果加载过,则不需要再次走这个流程。
注意:有些文章描述的父类加载器
,我觉得表述为上级类加载器
更加贴切,因为他们没有Java里的继承关系,只是人为划分的层级。
- 层级是,或者说加载顺序是
Bootstrap ClassLoader(启动类加载器,或者叫引导类加载器)
|
Extension ClassLoader(扩展类加载器)
|
Application ClassLoader(应用类加载器)
|
User ClassLoader(用户类加载器,或者自定义类加载器,可以有多个,比如MyClassLoader1,MyClassLoader2)
注意:
1、注意断句:启动.类加载器,扩展.类加载器...
2、User ClassLoader是程序员自己定义的,自己写的ClassLoader代码,通过继承`java.lang.ClassLoader`抽象类
3、Bootstrap ClassLoader是获取不到ClassLoader的,得到`null`,因为其是C++写的,即`String.class.getClassLoader()`返回`null`
4、Extension和Application的类加载器,分别是Java类:`sun.misc.Launcher$ExtClassLoader`和`sun.misc.Launcher$AppClassLoader`
Java虚拟机中可以安装多个类加载器,系统默认三个主要的类加载器,每个类负责加载特定位置的类
Bootstrap ClassLoader:/jre/lib/rt.jar,写死了加载名为rt.jar
Extension ClassLoader:/jre/lib/ext/*.jar,该目录所有jar,包括可以把自己的打的jar包丢在这个目录页能加载
Application ClassLoader:加载classpath指定的jar包或目录
User ClassLoader:加载我们指定的目录中的class
加载顺序:
Bootstrap ClassLoader加载是否成功(rt.jar里是否存在指定的类),成功则跳出,否则交给
Extension ClassLoader加载是否成功,成功则跳出,否则交给
Application ClassLoader加载是否成功,成功则跳出,否则交给
User ClassLoader,加载是否成功,成功则跳出,否则抛出ClassNotFoundException
- 举个例子
例如要加载java.lang.String,首先让Bootstrap ClassLoader来加载,找到了,加载成功,跳出;
如果要加载com.some.MyTest,则Bootstrap加载失败,ExtClassLoader失败,APPClassLoader成功;
如果要加载com.wyf.test.Tool,假设这个类打成jar包并丢到`E:\DevFolder\jdk\jdk1.8.0_152\jre\lib\ext`里,则Bootstrap加载失败,ExtClassLoader加载成功,跳出
双亲委派模型的好处
-
安全
比如自己写的java.lang.String类,不能覆盖Java核心的那个,保证了程序运行的稳定性。PS:即使自己定义了类加载器并强行用defineClas()加载java.lang开头的类,也不可能成功,会抛出
java.lang.SecurityException:Prohibited package name:java.lang
-
避免重复加载的混乱
已经加载过的类不会再次被加载,避免了重复加载的混乱。方便管理。
自定义类加载器
要自定义自己的类加载器,必须继承抽象类
java.lang.ClassLoader
,并覆盖findClass(String name)
方法。
为什么要自定义ClassLoader?
- 给.class文件加密,使得无法反编译
通常生成的.class文件很容易被反编译,我们可以利用生成的.class文件(未加密)读成字节流,然后堆字节流做一些转换,然后再写成.class文件,name这个.class文件就是加密的了,无法反编译。但是加密后的文件无法被默认的类加载器加载,就需要自己定义类加载器,在findClass()方法中得到字节流后进行还原
- 需要读取特定目录的class类(例如Tomcat就定义了自己的类加载器)
自定义ClassLoader的详细方法
ClassLoader类:
package com.test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader2 extends ClassLoader {
/**
* 整体思路是传入的name是class文件的位置,例如E:/TestEntity.class, 然后得到文件的字节流后传入`defineClass(byte[] b, int off, int len)`得到Class
*
* 注意这里的name字段传的是class文件的位置,实际上跟父类的方法的name的含义是不一样了
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFileFullpath = name;
FileInputStream fis = null;
byte[] buf = null;
try {
fis = new FileInputStream(new File(classFileFullpath));
int available = fis.available();
buf = new byte[available];
fis.read(buf);// 将文件全部读入buf中
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
if (buf == null) {
return null;
}
return defineClass(buf, 0, buf.length);
}
}
实体类
package com.test;
public class TestEntity {
public String sayGoodBye() {
String msg = "sayonara";
System.out.println("say:" + msg);
return msg;
}
}
测试类
package com.test;
import java.lang.reflect.Method;
public class Test2 {
public static void main(String[] args) {
String classFileLocation = "E:/TestEntity.class";// class文件位置
MyClassLoader2 cl = new MyClassLoader2();
try {
Class clazz = cl.loadClass(classFileLocation);// 网上有些例子直接调用cl.findClass,是不对的,这样就是直接调用了,不会有loadClass委托的逻辑在里面。另外注意这里传qualifiedName是不对的,会被APPClassLoader加载
Object obj = clazz.newInstance();
Method method = clazz.getMethod("sayGoodBye");
Object ret = method.invoke(obj, null);
System.out.println("method_ret:" + ret);
// 查看对象是哪个ClasLoader加载的
System.out.println("ClassLoader:" + TestEntity.class.getClassLoader());// 不能用这个查看,这种方式使用的是AppClassLoader,因为如果不显式指定自己的ClassLoader,就会非自定义的那套(AppClassLoader->Ext->Bootstrap
System.out.println("ClassLoader2:" + obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
将编译出来的TestEntity.class
放到E:/
下即可。这个类加载器使用了过时的方法defineClass(byte[] b, int off, int len)
(@Deprecated)
思考(很重要) 这里的测试类用了loadClass(),传入了E:/TestEntity.class
的值,这里是不能传入com.test.TestEntity
的,否则就轮不到自定义的加载器了,因为其上级AppClassLoader,会通过入参com.test.TestEntity
在类路径下找到E:\DevFolder\workspaces\sts_workspace\test-classloader\target\classes\com\test\TestEntity.class
,然后加载这个类
补充
这里有点不明白替代过时方法的defineClass(String name, byte[] b, int off, int len)
的 name
字段的含义,传name的意义是什么? 似乎是通过该name获取到要加载的class文件的位置,通过类路径,
- name不能随便乱写
com/test/TestEntity
错,不能用/com.test.TestEntity2
错,类名错误,NoClassDefFoundError
异常com.test2.TestEntity
错,包名错误,NoClassDefFoundError
异常com.test.TestEntity.class
错,不能带.class扩展名,native方法抛出异常""
,错,虽然看源码检查这个name这里是可以的,可能是native方法抛出异常了- null,正确,等价于
defineClass(byte[] b, int off, int len)
com.test.TestEntity
,正确
动手实践
可以定义java.lang.Object
吗?
可以定义java.lang.Object
,定义如下
package java.lang;
public class Object {
public void call() {
System.out.println("my own java.lang.Object");
}
}
测试代码
package com.test;
public class Test {
public static void main(String[] args) {
java.lang.Object o = new java.lang.Object();
o.call();
}
}
运行的时候报错,抛出了异常
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Object.call()V
at com.test.Test.main(Test.java:7)
原因:
虽然定义了自己的Object类,包名也是跟rt.jar
里的相同,但是加载的时候不会加载到自己定义的类,因为顶层Bootstrap ClassLoader在rt.jar里找到了这个全限定名的类:java.lang.Object
,于是就用了rt.jar里的,这样的双亲委派模型保证了安全,不会被别人写的相同包名和类名所覆盖。这里也验证了一个问题,Eclipse编译是通过的,因为IDE是按照本地的java.lang.Object进行编译的,所以可以调用call()
可以定义 java.lang.String
吗?
代码:
package java.lang;
public class String {
public void invoke() {
System.out.println("my own java.lang.String");
}
}
// 测试代码
package com.test;
public class Test {
public static void main(String[] args) {
java.lang.String s = new java.lang.String();
s.invoke();
}
}
一样的结论,抛出
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.String.invoke()V
at com.test.Test.main(Test.java:6)
自己写的java.lang.String
依然无法覆盖rt.jar
里头的。这里注意到一个细节,自己写的java.lang.String
继承了自己写的java.lang.Object
,也就是自动获得了call()方法
关于覆盖ext中的类
假设如下代码打包成my-lib.jar并放到E:\DevFolder\jdk\jdk1.8.0_152\jre\lib\ext
中,即有com.wyf.test.Tool类
,有方法sayHelloInJanpanese()
package com.wyf.test;
public class Tool {
public String sayHelloInJanpanese() {
return "konichiwa";
}
}
在项目中定义同包同名类,如下
package com.wyf.test;
public class Tool {
public String sayHelloInJanpanese() {
return "my own com.wyf.test.Tool";
}
}
测试代码是
package com.test;
public class TestExtJar {
public static void main(String[] args) {
com.wyf.test.Tool a = new com.wyf.test.Tool();
String hello = a.sayHelloInJanpanese();
System.out.println(hello);
}
}
问题是会打印出什么? 答案是打印出konichiwa
,因为加载的是jre/lib/ext下的类!
这里有一个细节,假设本地的com.wyf.test.Tool
的方法名和jre/lib/ext/my-lib.jar
中不同,如下
package com.wyf.test;
public class Tool {
public String sayHello() {
return "my own com.wyf.test.Tool";
}
}
则TestExtJar.java
编译不过,因为它认了本地的Tool类,必须改成
package com.test;
public class TestExtJar {
public static void main(String[] args) {
com.wyf.test.Tool a = new com.wyf.test.Tool();
String hello = a.sayHello();
System.out.println(hello);
}
}
改成这样后,编译是没问题,运行的时候抛出
Exception in thread "main" java.lang.NoSuchMethodError: com.wyf.test.Tool.sayHello()Ljava/lang/String;
at com.test.TestExtJar.main(TestExtJar.java:6)
问题跟前面的一样,因为加载的是jre/lib/ext/my-lib.jar
中的com.wyf.test.Tool
而不是本地的Tool,所以运行时找不到sayHello()
方法。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135330.html