title: 类加载机制(五):自定义类加载器与深入双亲委托机制
date: 2019-03-17 08:24:05
categories:
我们知道类加载器共分为两大类型,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
文件的搜索与加载过程是由类加载器完成的,而类加载器又是遵循双亲委托机制的,关于这个机制就不多说了,见以前的文章。
类加载机制(三):类的加载与类加载器
类加载机制(四):解析ClassLoader
在MyClassLoader
中我们首先调用ClassLoader
的loadClass
方法,在loadClass
中,最终会调用我们重载的这个findClass
方法,但现在我们重载的findClass
并没有被调用,说明有其他的findClass
调用了。那我们在executeLoad
中打印下加载的这个class
对象的类加载器。
System.out.println("我就是它加载的:" + loadClass.getClassLoader());
输出结果:
我就是它加载的:sun.misc.Launcher$AppClassLoader@18b4aac2
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
对象。我们通过一些示例代码来进行分析。
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
。
复制一份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
类空间。最后,提一句,内建于JVM
的启动类加载器会加载java.lang.ClassLoader
以及其他的Java
平台类,当JVM
启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstap
)。