title: 类加载机制(五):自定义类加载器与深入双亲委托机制
date: 2019-03-17 08:24:05
categories:
- Java虚拟机
tags: - 类加载机制
- 自定义类加载器
引言
我们知道类加载器共分为两大类型,Java虚拟机自带的类加载器和自定义类加载器。Java虚拟机自带的类加载器分别加载了不同路径下的class文件,而有时我们需要加载一些特殊的class文件,如这个class文件是被加密的,我们就需要自己定义类加载器去解密加载它,又比如我们需要从网络或者直接从数据库中读取class文件,我们也需要自己定义类加载。
上文(类加载机制(四):解析ClassLoader)我们介绍分析了ClassLoader类,知道这个类是一个抽象类,除了Java虚拟机内建的启动类加载器以为,所有的类加载器都继承于它,并且要重载它的一个方法findClass去搜寻指定名字的class文件,并且如果在一个类中,又有其他类的引用,也是先通过调用类的类加载器先尝试去加载。在此篇文章,我们自定义一个类加载器去加载本地文件系统中的class文件来深入剖析双亲委托机制。
自定义类加载器
首先来看看代码。
public class MyClassLoader extends ClassLoader{
//定义一个className,表示自定义类加载器的名字
private String className;
//定义一个path,表示class文件所在目录
private String path;
public void setPath(String path) {
this.path = path;
}
//同其父类ClassLoader一样,有两个构造方法
public MyClassLoader(ClassLoader parent, String className) {
super(parent);
this.className = className;
}
public MyClassLoader(String className) {
super();
this.className = className;
}
//重载的findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//测试自定义类加载器是否执行成功
System.out.println("自定义class loader name: " + this.className);
//调用MyLoadClass获取字节数组
byte[] bytes = this.MyLoadClass(name);
//调用
return defineClass(null,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private byte[] MyLoadClass(String className) throws IOException {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream bis = null;
//将传入的类的二进制名转换为类的全限定名(包名+类名)
String replace = className.replace(".", File.separator);
try {
//将这个class文件转换成字节数组
is = new FileInputStream(this.path + replace + ".class");
bis = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())){
bis.write(ch);
}
data = bis.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
is.close();
bis.close();
}
return data;
}
}
如上代码所示,这个简易的自定义类加载器同样有两个构造方法,没有父类加载器传入的构造方法会调用
ClassLoader的无参的构造方法,将系统类加载器设置为这个自定义类加载器的父类加载器;有父类类加载器传入的构造函数,也会调用ClassLoader的构造方法,只不过调用的是有参的构造方法,将传入的这个类加载器设置为这个自定义类加载器的父类加载器。
测试
具体的实现,在注释中,就不过多阐述了。我们在findClass中写了句测试语句System.out.println("自定义class loader name: " + this.className);,来测试自定义类加载器是否执行成功。到这我们就可以在启动类中进行测试了。
public static void executeLoad(MyClassLoader loader,String className) throws Exception{
loader.setPath(***********);
Class<?> loadClass = loader.loadClass(className);
//打印出Class对象的hash码
System.out.println(className + "的class对象的hashCode:" + loadClass.hashCode());
//创建一个示例
Object o = loadClass.newInstance();
System.out.println("--------------");
}
写了一个执行方法,减少代码,并且在指定的
path中放入MyTest1的class文件:
- 传入自定义类加载器的实例与要加载的类的二进制名字。
- 在方法体里面指定好要加载的
class文件目录。- 调用父类的
loadClass方法进行加载(ClassLoader具体怎么加载,见类加载机制(四):解析ClassLoader)。
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader.executeLoad(loader1,"classLoader.MyTest1");
}
输出结果:
classLoader.MyTest1的class对象的hashCode:1163157884
--------------
竟然只输出了
MyTest1的class对象的hashCode,意思是我们的自定义类加载未执行(System.out.println("自定义class loader name: " + this.className);)(⊙o⊙),怎么回事?
结果分析
我们知道关于类的加载也就是class文件的搜索与加载过程是由类加载器完成的,而类加载器又是遵循双亲委托机制的,关于这个机制就不多说了,见以前的文章。
在MyClassLoader中我们首先调用ClassLoader的loadClass方法,在loadClass中,最终会调用我们重载的这个findClass方法,但现在我们重载的findClass并没有被调用,说明有其他的findClass调用了。那我们在executeLoad中打印下加载的这个class对象的类加载器。
System.out.println("我就是它加载的:" + loadClass.getClassLoader());
输出结果:
我就是它加载的:[email protected]
classLoader.MyTest1的class对象的hashCode:1163157884
--------------
结果显示
MyTest1是由系统类加载器加载的。
现在水落石出了,原来我们想要加载的MyTest1被系统类加载器给加载了,那为什么呢,其实联想下双亲委托机制就明白了。MyClassLoader收到要加载某个类的请求,就往其父类加载器(系统类加载器)传递,然后,一层层传递,导启动类加载器后,又往下传回来,传到系统类加载器后,系统类加载器发现自己能加载这个类,然后就截胡了,MyTest1.class就被系统类加载器加载到内存中去了。
我们知道,系统类加载器是从classPath或者java.class.path系统属性中去加载class文件和jar包的,那我们把classPath中的MyTest1.class给删除掉,结果又会怎么样呢?
输出结果:
自定义class loader name: loader1
我就是它加载的:classLoader.MyClassLoader@4554617c
classLoader.MyTest1的class对象的hashCode:356573597
--------------
输出结果显示:我们的
MyClassLoader起作用啦,注意这里hashCode不一样哦(⊙x⊙;)。
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader.executeLoad(loader1,"classLoader.MyTest1");
MyClassLoader loader2 = new MyClassLoader("loader2");
MyClassLoader.executeLoad(loader2,"classLoader.MyTest1");
//loader2是loader3的类加载器
MyClassLoader loader3 = new MyClassLoader(loader2,"loader3");
MyClassLoader.executeLoad(loader3,"classLoader.MyTest1");
}
再创建两个
MyClassLoader的实例。loader2–>loader3
输出结果:
自定义class loader name: loader1
我就是它加载的:classLoader.MyClassLoader@4554617c
classLoader.MyTest1的class对象的hashCode:356573597
--------------
自定义class loader name: loader2
我就是它加载的:classLoader.MyClassLoader@677327b6
classLoader.MyTest1的class对象的hashCode:2133927002
--------------
我就是它加载的:classLoader.MyClassLoader@677327b6
classLoader.MyTest1的class对象的hashCode:2133927002
--------------
输出结果显示:
loader2加载获得的class对象和loader3加载获得的class是一样的。
这个结果其实ClassLoader类中的loadClass很清楚:
- 类只会被加载一次(
findLoadedClass(String)),返回的class对象都一样。若没有class文件,则会调用当前加载器的findClass方法去查找class文件。 - 双亲委托机制是包含关系,实例化
loader3时可以让loader2作为自己的父加载器,创建loader3去加载MyTest1时,因为loader2已经加载过了(findLoadedClass(String)),所以使用loader3加载时,loader3直接返回了已经加载过的MyTest1的class对象。
深入双亲委托机制
我们通过一些示例代码来进行分析。
示例代码
NO.1
public class MyCat {
public MyCat(){
public MyCat(){
//打印出MyCat的类加载器器
System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
}
}
}
--------------------------------------------------------
public class MySample {
public MySample() {
//打印出MySample的类加载器器
System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
System.out.println("--------------");
//在MySample的构造方法中创建一个MyCat的实例
new MyCat();
}
}
--------------------------------------------------------
public class MyTest13 {
public static void main(String[] args) throws Exception {
//加载MySample类
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader.executeLoad(loader1,"refenLoad.MySample");
}
}
输出结果:
refenLoad.MySample的class对象的hashCode:1956725890
MySample is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
具体过程如下:
- 调用
MyClassLoader加载MySample类。classPath中有MySample类的class文件,系统类加载器将其加载到内存中。- 然后因为在
executeLoad方法中创建了对象实例,MySample被首次主动使用,即进行初始化,调用构造函数完成初始化。- 在
MySample的构造函数中new MyCat(),即对MyCat的首次主动使用,经历加载连接初始化。
接着,复制一份MySample的class文件到我们设定的path中,删除到classPath中的那份,结果怎么样呢。
输出结果:
自定义class loader name: loader1
refenLoad.MySample的class对象的hashCode:1735600054
MySample is loaded by:classLoader.MyClassLoader@74a14482
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
为什么,两个类的类加载器又不一样呢?
- 因为
classPath中没有MySample的class文件,所以经过双亲委托机制,最终是通过MyClassLoader来加载我们自己的MySample文件。 - 创建
MySample实例时,进行MySample的初始化,执行MySample的构造方法。 -
MySample的构造方法里创建MyCat实例,使用加载MySample的类加载器来加载MyCat。 -
MyClassLoader加载器委托系统加载器来加载MyCat.class,加载完成。
再接着,复制一份MyCat的class文件到我们设定的path中,删除到classPath中的那份,结果又怎么样呢。
输出结果:
自定义class loader name: loader1
refenLoad.MySample的class对象的hashCode:1735600054
MySample is loaded by:classLoader.MyClassLoader@74a14482
--------------
自定义class loader name: loader1
MyCat is loaded by:classLoader.MyClassLoader@74a14482
--------------
它们的类加载器又都是
MyClassLoader了。
- 因为
classPath中没有MySample的class文件,所以经过双亲委托机制,最终是通过MyClassLoader来加载我们自己的MySample文件。 - 创建
MySample实例时,进行MySample的初始化,执行MySample的构造方法。 -
MySample的构造方法里创建MyCat实例,使用加载MySample的类加载器MyClassLoader来加载MyCat,加载成功。
如果只删除MyCat.class又会怎么样呢?
系统加载器加载MySmple.class,加载MyCat时,同样使用系统加载器来加载MyCat,但classPath中没有MyCat.class文件,最后就会抛出java.lang.NoClassDefFoundError异常。
再再接着,reBuild项目,删除掉MySample的class文件,在MyCat的构造方法里打印MySample的class`。
public class MyCat {
public MyCat(){
System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
System.out.println("from MyCat:" + MySample.class);
}
}
输出结果:
自定义class loader name: loader1
refenLoad.MySample的class对象的hashCode:1735600054
MySample is loaded by:classLoader.MyClassLoader@74a14482
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
我们想在
MyCat中调用MySample,竟然报错了,找不到MySample类,这里涉及到类的命名空间问题。
最后,只删除classPath中的MySample的class文件,在MySample的构造方法中打印MyCat的class。
public class MyCat {
public MyCat(){
System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
}
}
-----------------------------------------------
public class MySample {
public MySample() {
System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
System.out.println("from MyCat:" + MyCat.class);
System.out.println("--------------");
new MyCat();
}
}
输出结果:
refenLoad.MySample的class对象的hashCode:1956725890
MySample is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
from MyCat:class refenLoad.MyCat
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
可以看到,这里就打印成功,也就是说,在
MySample中调用MyCat成功,这里同样也是命名空间的问题。
命名空间
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
命名空间之间的关系
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间。因此由只加载器加载的类能看见父加载器加载的类。例如系统类加载器可以看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类互不可见。
了解了命名空间后,就明白前面代码的输出结果了。
(1)删除掉MySample的class文件,在MyCat的构造方法里打印MySample的class。MySample由MyClassLoader加载,MyCat由AppClassLoader加载,父加载器加载的类是看不到子加载器加载的类,则在MyCat中看不到MySample。
(2)删除掉MySample的class文件,在MySample的构造方法中打印MyCat的class,MySample由MycalssLoader加载,MyCat由AppClassLoader加载,子加载器能够看见父加载器加载的类,则MySample可以看到MyCat的class。
NO.3
复制一份MyPerson.class到指定的路径下。
public class MyPerson {
//内部维护一个MyPerson的类型的属性
private MyPerson person;
public MyPerson() {
}
//传进来一个对象,强制转换为MyPerson
public void setPerson(Object o) {
this.person = (MyPerson)o;
}
}
-----------------------------------------
public class MyTest14 {
public static void main(String[] args) throws Exception{
//创建两个MyClassLoader的实例,都去加载位于path路径下的MyPerson.class文件
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader loader2 = new MyClassLoader("loader2");
loader1.setPath("C:\\Users\\Administrator\\Desktop\\jvmTest\\");
loader2.setPath("C:\\Users\\Administrator\\Desktop\\jvmTest\\");
//分别去加载MyPerson.class,得到其class对象
Class<?> clazz1 = loader1.loadClass("classLoader.MyPerson");
Class<?> clazz2 = loader2.loadClass("classLoader.MyPerson");
//比较两个class对象是否相等
System.out.println(clazz1 == clazz2);
//通过class对象,创建实例
Object o1 = clazz1.newInstance();
Object o2 = clazz2.newInstance();
//使用反射的方式去调用MyPerson的setPerson方法
Method setPerson = clazz1.getMethod("setPerson", Object.class);
//调用o1的setPerson方法,将o2传进去。
setPerson.invoke(o1,o2);
}
}
输出结果:
true
通过系统类加载器加载,没什么问题。
从classPath中删除掉MyPerson.class文件,再运行程序。
从结果可以看出,两个
class对象最终都是由MyClassLoader来加载得到的,但是得到的class并不是同一个,并且在执行o1的setPath方法时还报错,说无法将MyPerson转换为MyPerson,这就很奇怪了?
其实,想想类加载器的命名空间,还是挺简单的。
一个类在Java虚拟机中的唯一性,是由类与类加载器一起共同决定的,每一个类加载,都有自己独立的命名空间。在此处,loader1与loader2虽然都是MyClassLoader的实例,但是它们之间并不存在双亲委托的关系,即是两个不同的类加载器,即存在两个不同的命名空间,clazz1和clazz2属于不同的命名空间。使用反射去调用MyPerson的serPerson方法,想把o2赋值给o1中的Person属性,但因为clazz1和clazz2是属于不同的命名空间,推广开,o1和o2也属于不同的命名空间,两者之间是不可见的,所以不能将o2赋值给o1的Person属性。
总结
通过这个自定义类加载器,我们深入剖析了类加载器的双亲委托机制,这里再放一遍关于类加载器的双亲委托模型的好处:
- 可以确保
Java核心类库的类型安全:所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由各自的类加载器去加载的话,那系统中会产生多个版本的Object类,这些类位于不同的命名空间中,相互之间不兼容,不可见,应用程序将会变得混乱。而通过双亲委托机制,Java核心类库中的类都由启动加载器来完成加载,从而保证了Java应用使用的都是同一个Java核心类库,它们之间是相互兼容的。 - 可以确保Java核心类库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以加载相同名称的类,这些相同名称的类可以并存在
Java虚拟机中。不同类加载器所加载的类是不兼容的,这就相当于在Java虚拟机中创建了一个又一个的相互隔离的Java类空间。
最后,提一句,内建于JVM的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstap)。