Tomcat打破双亲委派机制

目录

1.JVM类加载器

2.双亲委派机制

3.Tomcat如何打破双亲委派机制

loadClass方法

findClass方法

4.Tomcat如何隔离Web应用

Tomcat类加载器的层次结构

WebAppClassLoader

SharedClassLoader

CatalinaClassLoader

CommonClassLoader

全盘负责委托机制

Spring的加载问题

线程的上下文加载器

1.JVM类加载器

  • Java中有3个类加载器,另外你也可以自定义类加载器
  • 引导(启动)类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载驰骋JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序(系统)类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包

2.双亲委派机制

JVM类加载器是有亲子层级结构的

        双亲委派机制就是优先用父加载器加载,加载不到再回到子加载器去完成加载。具体流程就是先看自身系统类加载器缓存中是否有该类,没有就查找父加载器拓展类加载器缓存中是否拥有,没有找到就再上一层去引导类加载器查看是否有缓存;都没找到的话就先由引导类加载器在自己的加载类路径中查找并加载,没有加载到的话就向下到拓展类加载器中去加载,最后还没加载到就再向下一层到自身系统类加载器中去加载。

为什么要设置双亲委派机制?

  • 沙箱安全机制:防止核心API库被随意篡改,比如自己写的java.lang.String.class类就不会得到加载
  • 避免类的重复加载:当父亲已经加载了该类时,就没必要子ClassLoader再加载一次,保证被加载类的唯一性

3.Tomcat如何打破双亲委派机制

        Tomcat自定义类加载器WebAppClassLoader打破了双亲委派机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载Web应用自己定义的类。具体实现就是重写ClassLoader的两个方法:findClass和loadClass。

loadClass方法

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;
        //1. 先在本地cache查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
        //2. 从系统类加载器的cache中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
        // 3. 尝试用ExtClassLoader类加载器类加载(ExtClassLoader 遵守双亲委派,ExtClassLoader 会使用 Bootstrap ClassLoader 对类进行加载)
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        // 4. 尝试在本地目录搜索class并加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        // 5. 尝试用系统类加载器(AppClassLoader)来加载
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
     }
    //6. 上述过程都加载失败,抛出异常
    throw new ClassNotFoundException(name);
}

Tomcat类加载器的loadClass方法主要有以下6个步骤:

  1. 先在本地Cache查找该类是否已经被加载过。
  2. 如果Tomcat类加载器没有加载过这个类,再看看系统类加载器缓存中是否加载过。
  3. 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的是防止Web应用自己的类覆盖JRE的核心类。这一步比较关键,相当于跳过了AppClassLoader先去加载这一过程,优先让ExtClassLoader去加载,ExtClassLoader会向上委派让BootstrapClassLoader优先去加载该类。
  4. 如果ExtClassLoader加载器加载失败,也就是说JRE核心类中没有这个类,那么就在本地Web应用目录下查找并加载(加载次序优先于系统类加载器)。这一步是直接调用本地findClass()方法,而不会像双亲委派机制那样先去上层AppclassLoader去加载。
  5. 如果本地目录下没有这个类,说明不是Web应用自己定义的类,那么由系统类加载器去加载。这一步是通过Class.forName来调用系统类加载器的,因为Class.forName默认的类加载器就是系统类加载器。
  6. 如果上述加载过程全部失败,抛出ClassNotFound异常。
     

Tomcat中类加载器优先加载类顺序:

(首先调用ExtClassLoader,但ExtClassLoader会向上委派让BootstrapClassLoader优先加载)

BootstrapClassLoader -> ExtClassLoader -> WebappClassLoader -> AppClassLoader

findClass方法

在findClass方法里,主要有三个步骤:

  1. 先在web应用本地目录下查找要加载的类。
  2. 如果没有找到,交给父加载器去查找,它的父加载器就是系统类加载器。
  3. 如果父加载器也没找到这个类,抛出ClassNotFound异常。
public Class<?> findClass(String name) throws ClassNotFoundException {
    // Ask our superclass to locate this class, if possible
    // (throws ClassNotFoundException if it is not found)
    Class<?> clazz = null;
 
    // 先在自己的 Web 应用目录下查找 class
    clazz = findClassInternal(name);
 
    // 找不到 在交由父类来处理
    if ((clazz == null) && hasExternalRepositories) {  
        clazz = super.findClass(name);
    }
    if (clazz == null) {
         throw new ClassNotFoundException(name);
    }
    return clazz;
}

4.Tomcat如何隔离Web应用

        Tomcat作为Servlet容器,它负责加载我们的Servlet类,此外它还负责加载Servlet所依赖的JAR包。并且Tomcat本身也是一个Java程序,因此它需要加载自己的类和依赖的JAR包。首先,让我们思考一下这几个问题:

1)假如我们在Tomcat中运行了两个Web应用程序,两个Web应用中有同名的Servlet,但是功能不同,Tomcat需要同时加载和管理这两个同名的Servlet类,保证它们不会冲突,因此Web应用之间的类需要隔离。

2)加入两个Web应用都依赖同一个第三方JAR包,比如Spring,那Spring的JAR包被加载到内存后,Tomcat要保证这两个Web应用能够共享,也就是说Spring的JAR包只被加载一次,否则随着依赖的第三方JAR包增多,JVM的内存会膨胀。

3)跟JVM一样,我们需要隔离Tomcat本身的类和web应用的类。

Tomcat类加载器的层次结构

为了解决这些问题,Tomcat设计了类加载器的层次结构,它们的关系如图所示:

  • CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • CatalinaClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebAppClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebAppClassLoader,实现相互隔离,比如不同war包应用引入了不同的Spring版本,这样实现就能加载各自的Spring版本。
WebAppClassLoader

        对于第1个问题,Tomcat的解决方案是自定义一个类加载器WebAppClassLoader,并且给每个Web应用创建一个类加载器实例,因此每个Context容器负责创建和维护一个WebAppClassLoader加载器实例,如此一来,就对不同Web应用的类加载起到了隔离作用。

SharedClassLoader

        对于第2个问题,Tomcat为了解决Web应用之间共享类问题,引入一个新的类加载器SharedClassLoader,作为WebAppClassLoader的父加载器,专门来加载Web应用之间共享的类。

CatalinaClassLoader

        对于第3个问题,如何隔离Tomcat本身类和web应用的类?要共享可以通过父子关系,要隔离那就需要兄弟关系了。基于此Tomcat设计了一个CatalinaClassLoader,与SharedClassLoader加载器呈平级兄弟关系。

CommonClassLoader

        为了解决Tomcat和各Web应用之间的共享问题,在它们之上再增加一层父加载器CommonClassLoader。

全盘负责委托机制

        “全盘负责”是指当一个ClassLoader装载一个类时,除非显示地使用另外一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入。

Spring的加载问题

        比如Spring作为一个Bean工厂,它需要创建业务类的实例,并且再创建业务类实例之前需要加载这些类。Spring是通过调用Class.forName来加载业务类的,在forName的函数中,会用调用者也就是Spring的加载器去加载业务类。

        我们前面提到,Web应用之间共享的JAR包可以交给SharedClassLoader来加载,从而避免重复加载。Spring作为共享的第三方JAR包,它本身是由SharedClassLoader来加载的,Spring又要去加载业务类,按照前面那条规则,加载Spring的类加载器也会用来加载业务类,但是业务类在Web应用目录下,不在SharedClassLoader的加载路径下,这该怎么办呢?

线程的上下文加载器

        于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文加载器取出来,用来加载Bean。Spring取线程上下文加载器的代码如下:

cl = Thread.currentThread().getContextClassLoader();

        线程上下文加载器不仅仅可以用在Tomcat和Spring类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的JDBC就是通过上下文类加载器来加载不同的数据驱动的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值