自定义类加载器实现及在tomcat中的应用

 2023-01-18
原文作者:summerSunShine 原文地址:https://juejin.cn/post/7016104801712734244

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

了解了类加载器的双亲委派机制, 也知道了双亲委派机制的原理,接下来就是检验我们学习是否扎实了,来自定义一个类加载器

一. 回顾类加载器的原理

202301011541150141.png

还是这张图,类加载器的入口是c++调用java代码创建了JVM启动器,其中的一个启动器是sun.misc.Launcher启动器。这个启动器启动并加载的AppClassLoader和ExtClassLoader。然后调用launcher.getClassLoader()方法获取loader对象, loader对象本质是一个ClassLoader,然后调用了ClassLoader的loadClass("...")方法加载类。也是在loadClass("...")方法里实现了双亲委派机制。

详细原理参考文章:www.cnblogs.com/ITPower/p/1…

二、自定义类加载器分析

对于类加载器, 我们知道他的重点是loadClass(...)方法, 里面的双亲委派机制也是在loadClass方法里面实现的. loadClass方法里面实际上去加载类的是findClass()方法. 对于我们自定义的类加载器来说需要做到两点即可

  1. 这个自定义的类加载器继承自ClassLoader

  2. 这个类加载器要重写ClassLoader类中的findClass()方法

另外我们还可以参考AppClassLoader和ExtClassLoader来写。

三、自定义类加载器实现

下面我自己定义了一个类加载器

第一步:自定义类加载器继承自ClassLoader抽象类,然后定义一个构造方法, 用来接收要加载的类名

202301011541159772.png

第二步:重写核心方法findClass(String name)

202301011541167273.png

这里有两步操作,

第一个是: 从类路径中读取要加载类的文件内容, 自定义

第二个是: 调用构造类的方法, 调用的系统的defineClass

接下来看看自定义的loadByte是如何实现的

202301011541175654.png

这里的实现就是找到类, 并且将类的内容读取出来, 转换成二进制的字节码, 返回

最后一部分就是如何调用了.

202301011541187215.png

用类加载器加载类, 然后实例化, 使用反射机制调用User1 的方法sout

    package com.lxl.jvm;
    ​
    public class User1 {
        public void sout() {
            System.out.println("进入到User1");
        }
    }

这里面System.out.println(clazz.getClassLoader().getClass().getName()); 获取当前类的类加载器, 猜一猜这里打印的会是谁?

202301011541196706.png

看到了么? 是AppClassLoader, 为什么呢?

原因是我的项目里已经有一个类User1了

202301011541202457.png

我们自定义类加载器的父类是AppClassLoader. 而程序代码中的User1刚好是被AppClassLoader加载, 因为找到了,所以就不会再去我们指定的文件夹中查找了

这就是类的双亲委派机制的特点.

那么如果我们将项目中的User1类删除掉, 这是类加载器是谁呢? 当然就是我们自定义的类加载器了. 那么问题来了, 自定义类加载器的父类为什么是AppClassLoader呢?

四. 分析自定义类加载的父类为什么是appClassLoader?

我们来看一下源码

我们自定义的类加载器, 继承自ClassLoader类加载器, 那么在调用自定义类加载器的构造方法之前, 应该先加载父类ClassLoader的无参构造函数.

首先会执行ClassLoader的无参的构造方法.

202301011541209498.png

而无参的构造方法会调用自身的构造方法

202301011541216179.png

里面有一个parent, 我们就是要看看这个parent到底是谁呢. 来看看getSystemClassLoader()方法

2023010115412268510.png

2023010115412385011.png

之前我们已经研究过getClassLoader()这个方法了, 这里面定义的loadClass是谁呢?就是AppClassLoader.

2023010115412520012.png

2023010115412581313.png

这就是为什么自定义class类加载器的父类是AppClassLoader的原因了。

五、打破双亲委派机制

首先,我们要明白,什么是双亲委派机制?为什么要打破双亲委派机制?什么时候需要打破双亲委派机制?

1. 什么是双亲委派机制?

在前面,我们说了什么是双亲委派机制,子类委托父类加载的这个逻辑,就是双亲委派机制。如果还不知道什么是双亲委派机制,可以查看文章:www.cnblogs.com/ITPower/p/1…

2. 如何打破双亲委派机制呢?

我们知道了,双亲委派机制就是类在加载的时候,从自定义类加载器开始查找是否已经加载过这个类,如果没有加载过则加载类,但是不是由自己立刻加载,而是委托上级加载。到了上级,先查找,找不到在加载,然后也不是自己立刻加载,依次类推。。。。这就是双亲委派机制,要打破双亲委派机制,那么就是不让他委托上级类加载器加载,由自己来加载。那么如何实现呢?

2023010115412727714.png

比如, 我现在有一个自定义类加载器, 加载的是~/com/lxl/jvm/User1.class类, 而在应用程序的target目录下也有一个com/lxl/jvm/User1.class, 那么, 最终User1.class这个类将被哪个类加载器加载呢? 根据双亲委派机制, 我们知道, 他一定是被应用程序类加载器AppClassLoader加载, 而不是我们自定义的类加载器, 为什么呢? 因为他要向上寻找, 向下委托. 当找到了以后, 便不再向后执行了.

我们要打破双亲委派机制, 就是要让自定义类加载器来加载我们的User1.class, 而不是应用程序类加载器来加载

双亲委派机制是在ClassLoader类的loadClass(...)方法实现的. 如果我们不想使用系统自带的双亲委派模式, 只需要重新实现ClassLoader的loadClass(...)方法即可. 下面是ClassLoader中定义的loadClass()方法. 里面实现了双亲委派机制

2023010115412842015.png

下面给DefinedClassLoaderTest.java增加一个loadClass方法, 拷贝上面的代码即可. 删除掉中间实现双亲委派机制的部分

2023010115412950616.png

这里需要注意的是, com.lxl.jvm是自定义的类包, 只有我们自己定义的类才从这里加载. 如果是系统类, 依然使用双亲委派机制来加载.

来看看运行结果:

    调用了user1的sout方法
    com.lxl.jvm.DefinedClassLoaderTest

现在User1方法确实是由自定义类加载器加载的了

源码:

    package com.lxl.jvm;
    ​
    import java.io.FileInputStream;
    import java.lang.reflect.Method;
    ​
    /**
     * 自定义的类加载器
     */
    public class DefinedClassLoaderTest extends ClassLoader{
    ​
        private String classPath;
    ​
        public DefinedClassLoaderTest(String classPath) {
            this.classPath = classPath;
        }
    ​
        /**
         * 重写findClass方法
         *
         * 如果不会写, 可以参考URLClassLoader中是如何加载AppClassLoader和ExtClassLoader的
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadBytes(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    ​
    ​
        private byte[] loadBytes(String name) throws Exception {
            // 我们需要读取类的路径
            String path = name.replace('.', '/').concat(".class");
            //String path = "";
            // 去路径下查找这个类
            FileInputStream fileInputStream = new FileInputStream(classPath + "/"  + path);
            int len = fileInputStream.available();
    ​
            byte[] data = new byte[len];
            fileInputStream.read(data);
            fileInputStream.close();
    ​
            return data;
        }
    ​
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    /**
                     * 直接执行findClass()...什么意思呢? 首先会使用自定义类加载器加载类, 不在向上委托, 直接由
                     * 自己执行
                     *
                     * jvm自带的类还是需要由引导类加载器自动加载
                     */
                    if (!name.startsWith("com.lxl.jvm")) {
                        c = this.getParent().loadClass(name);
                    } else {
                        c = findClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    ​
        public static void main(String[] args) throws Exception {
            DefinedClassLoaderTest classLoader = new DefinedClassLoaderTest("/Users/luoxiaoli");
            Class<?> clazz = classLoader.loadClass("com.lxl.jvm.User1");
            Object obj = clazz.newInstance();
            Method sout = clazz.getDeclaredMethod("sout", null);
            sout.invoke(obj, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    ​
    }

六. 打破双亲委派机制案例--tomcat部署多应用?

1. tomcat为何要打破双亲委派机制?

通常,我们在服务器安装的一个tomcat下会部署多个应用。而这多个应用可能使用的类库的版本是不同的。比如:项目A使用的是spring4,项目B使用的是Spring5。Spring4和Spring5多数类都是一样的,但是有个别类有所不同,这些不同是类的内容不同,而类名,包名都是一样的。假如,我们采用jdk向上委托的方式,项目A在部署的时候,应用类加载器加载了他的类。在部署项目B的时候,由于类名相同,这是应用服务器就不会再次加载同包同名的类。这样就会有问题。所以, tomcat需要打破双亲委派机制。不同的war包下的类自己加载,而不向上委托。基础类依然向上委托。

2.tomcat是如何打破双亲委派机制的?

实际上, 我们的tomcat可以加载各种各样类型的war包, 相互之间没有影响. 因为tomcat打破了双亲委派机制, 下面我们就来看看tomcat是如何打破双亲委派机制的?

2023010115413111917.png

如上图, 上面的橙色部门还是和原来一样, 采用双亲委派机制. 而黄色部分是tomcat第一部分自定义的类加载器, 这部分主要是加载tomcat包中的类, 这一部分依然采用的是双亲委派机制, 而绿色部分是tomcat第二部分自定义类加载器, 正事这一部分, 打破了类的双亲委派机制. 先面我们就来详细看看tomcat自定义的类加载器

1. tomcat第一部分自定义类加载器(黄色部分)

这部分类加载器, 在tomcat7及以前是tomcat自定义的三个类加载器, 分别加载不同文件家下的jar包. 而到了tomcat7及以后, tomcat将这三个文件夹合并了, 合并成了一个lib包. 也就是我们现在看到的lib包

2023010115413231818.png

我们来看看这三个类加载器的主要功能.

  • commonClassLoader: tomcat最基本的类加载器, 加载路径中的class可以被tomcat容器本身和各个webapp访问;
  • catalinaClassLoader: tomcat容器中私有的类加载器, 加载路径中的class对于webapp不可见的部分。
  • sharedClassLoader: 各个webapps共享的类加载器, 加载路径中的class对于所有的webapp都可见, 但是对于tomcat容器不可见.

这一部分类加载器, 依然采用的是双亲委派机制, 原因是, 他只有一份. 如果有重复, 那么也是以这一份为准. 这部分主要加载的是tomcat自带的类。

2.tomcat第二部分自定义类加载器(绿色部分)

绿色部分是java项目在打war包的时候, tomcat自动生成的类加载器, 也就是说 , 每一个项目打成一个war包, tomcat都会自动生成一个类加载器, 专门用来加载这个war包. 而这个类加载器打破了双亲委派机制. 我们可以想象一下, 假如这个webapp类加载器没有打破双亲委派机制会怎么样? 之前也说过,如果没有打破, 他就会委托父类加载器去加载, 一旦加载到了, 子类加载器就没有机会在加载了. 那么, spring4和spring5的项目想共存, 那是不可能的了.

所以, 这一部分他打破了双亲委派机制

这样一来, webapp类加载器不需要在让上级去加载, 他自己就可以加载对应war里的class文件. 当然了, 其他的基础项目文件, 还是要委托上级加载的.

下面我们来实现一个自定义的tomcat类加载器

3.自定义tomcat的war包类加载器

如何打破双亲委派机制, 我们在上面已经写过一个demo了.

那么, 现在我有两个war包, 分处于不同的文件夹, tomcat如何使用各自的类加载器加载自己包下的class类呢?

我们来举个例子, 比如: 在我的home目录下有两个文件夹, tomcat-test和tomcat-test1. 用这两个文件夹来模拟两个项目.

2023010115413306519.png

在他们的下面都有一个com/lxl/jvm/User1.class

2023010115413364920.png

2023010115413442821.png

虽然类名和类路径都是一样的,但是他们的内容是不同的

2023010115413519422.png

2023010115413633123.png

这个时候,如果tomcat要同时加载这两个目录下的User1.class文件, 我们如何操作呢?

其实,非常简单, 按照上面的思路, tomcat只需要为每一个文件夹生成一个新的类加载器就可以了.

    public static void main(String[] args) throws Exception {
         // 第一个类加载器
            DefinedClassLoaderTest classLoader = new DefinedClassLoaderTest("/Users/app/tomcat-test");
            Class<?> clazz = classLoader.loadClass("com.lxl.jvm.User1");
            Object obj = clazz.newInstance();
            Method sout = clazz.getDeclaredMethod("sout", null);
            sout.invoke(obj, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
    ​
    ​
         // 第二个类加载器
            DefinedClassLoaderTest classLoader1 = new DefinedClassLoaderTest("/Users/app/tomcat-test1");
            Class<?> clazz1 = classLoader1.loadClass("com.lxl.jvm.User1");
            Object obj1 = clazz1.newInstance();
            Method sout1 = clazz1.getDeclaredMethod("sout", null);
            sout1.invoke(obj1, null);
            System.out.println(clazz1.getClassLoader().getClass().getName());
        }

他们都是只加载自己目录下的文件. 我们来看看执行结果:

    调用了user1的sout方法
    com.lxl.jvm.DefinedClassLoaderTest
    ​
    调用了另外一个项目user1的sout方法, 他们是不同的
    com.lxl.jvm.DefinedClassLoaderTest

虽然上面的代码很简单,但这就是tomcat加载不同war包的原理。不同的是,tomcat实现逻辑会更复杂,他的类加载器都是动态生成的。精髓都是一样的。

4. 思考: tomcat自定义的类加载器中, 有一个jsp类加载器,jsp是可以实现热部署的, 那么他是如何实现的呢?

jsp其实是一个servlet容器, 由tomcat加载. tomcat会为每一个jsp生成一个类加载器. 这样每个类加载器都加载自己的jsp, 不会加载别人的. 当jsp文件内容修改时, tomcat会有一个监听程序来监听jsp的改动. 比如文件夹的修改时间, 一旦时间变了, 就重新加载文件夹中的内容.

具体tomcat是怎么实现的呢? tomcat自定义了一个thread, 用来监听不同文件夹中文件的内容是否修改, 如何监听呢? 就看文件夹的update time有没有变化, 如果有变化了, 那么就会重新加载.

jsp热部署也不是立刻就会看到效果,其他他也是有延迟的,这个延迟就是重新加载的过程。