类加载机制(五):自定义类加载器与深入双亲委托机制


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中放入MyTest1class文件:

  • 传入自定义类加载器的实例与要加载的类的二进制名字。
  • 在方法体里面指定好要加载的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中我们首先调用ClassLoaderloadClass方法,在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直接返回了已经加载过的MyTest1class对象。

深入双亲委托机制

我们通过一些示例代码来进行分析。

示例代码
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的首次主动使用,经历加载连接初始化。

接着,复制一份MySampleclass文件到我们设定的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中没有MySampleclass文件,所以经过双亲委托机制,最终是通过MyClassLoader来加载我们自己的MySample文件。
  • 创建MySample实例时,进行MySample的初始化,执行MySample的构造方法。
  • MySample的构造方法里创建MyCat实例,使用加载MySample的类加载器来加载MyCat
  • MyClassLoader加载器委托系统加载器来加载MyCat.class,加载完成。

再接着,复制一份MyCatclass文件到我们设定的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中没有MySampleclass文件,所以经过双亲委托机制,最终是通过MyClassLoader来加载我们自己的MySample文件。
  • 创建MySample实例时,进行MySample的初始化,执行MySample的构造方法。
  • MySample的构造方法里创建MyCat实例,使用加载MySample的类加载器MyClassLoader来加载MyCat,加载成功。

如果只删除MyCat.class又会怎么样呢?

系统加载器加载MySmple.class,加载MyCat时,同样使用系统加载器来加载MyCat,但classPath中没有MyCat.class文件,最后就会抛出java.lang.NoClassDefFoundError异常。


再再接着,reBuild项目,删除掉MySampleclass文件,在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中的MySampleclass文件,在MySample的构造方法中打印MyCatclass

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)删除掉MySampleclass文件,在MyCat的构造方法里打印MySampleclassMySampleMyClassLoader加载,MyCatAppClassLoader加载,父加载器加载的类是看不到子加载器加载的类,则在MyCat中看不到MySample

(2)删除掉MySampleclass文件,在MySample的构造方法中打印MyCatclassMySampleMycalssLoader加载,MyCatAppClassLoader加载,子加载器能够看见父加载器加载的类,则MySample可以看到MyCatclass

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并不是同一个,并且在执行o1setPath方法时还报错,说无法将MyPerson转换为MyPerson,这就很奇怪了?

其实,想想类加载器的命名空间,还是挺简单的。

一个类在Java虚拟机中的唯一性,是由类与类加载器一起共同决定的,每一个类加载,都有自己独立的命名空间。在此处,loader1loader2虽然都是MyClassLoader的实例,但是它们之间并不存在双亲委托的关系,即是两个不同的类加载器,即存在两个不同的命名空间,clazz1clazz2属于不同的命名空间。使用反射去调用MyPersonserPerson方法,想把o2赋值给o1中的Person属性,但因为clazz1clazz2是属于不同的命名空间,推广开,o1o2也属于不同的命名空间,两者之间是不可见的,所以不能将o2赋值给o1Person属性。

总结

通过这个自定义类加载器,我们深入剖析了类加载器的双亲委托机制,这里再放一遍关于类加载器的双亲委托模型的好处:

  • 可以确保Java核心类库的类型安全:所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由各自的类加载器去加载的话,那系统中会产生多个版本的Object类,这些类位于不同的命名空间中,相互之间不兼容,不可见,应用程序将会变得混乱。而通过双亲委托机制,Java核心类库中的类都由启动加载器来完成加载,从而保证了Java应用使用的都是同一个Java核心类库,它们之间是相互兼容的。
  • 可以确保Java核心类库所提供的类不会被自定义的类所替代。
  • 不同的类加载器可以加载相同名称的类,这些相同名称的类可以并存在Java虚拟机中。不同类加载器所加载的类是不兼容的,这就相当于在Java虚拟机中创建了一个又一个的相互隔离的Java类空间。

最后,提一句,内建于JVM的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstap)。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值