Tomcat打破双亲委派

2023-11-11

复习复习JVM类加载机制,再谈谈 Tomcat 的类加载器如何打破 Java 的双亲委托机制。

JVM 的类加载器

Java 的类加载,就是把字节码格式“.class”文件加载到 JVM 的方法区,并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法。那 Class 对象又是什么呢?你可以把它理解成业务类的模板,JVM 根据这个模板来创建具体业务类对象实例。

JVM 并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM 类加载是由类加载器来完成的,JDK 提供一个抽象类 ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。


public abstract class ClassLoader {

    //每个类加载器都有个父加载器
    private final ClassLoader parent;
    
    public Class<?> loadClass(String name) {
  
        //查找一下这个类是不是已经加载过了
        Class<?> c = findLoadedClass(name);
        
        //如果没有加载过
        if( c == null ){
          //先委托给父加载器去加载,注意这是个递归调用
          if (parent != null) {
              c = parent.loadClass(name);
          }else {
              // 如果父加载器为空,查找Bootstrap加载器是不是加载过了
              c = findBootstrapClassOrNull(name);
          }
        }
        // 如果父加载器没加载成功,调用自己的findClass去加载
        if (c == null) {
            c = findClass(name);
        }
        
        return c;
    }
    
    protected Class<?> findClass(String name){
       //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
          ...
          
       //2. 调用defineClass将字节数组转成Class对象
       return defineClass(buf, off, len)}
    
    // 将字节码数组解析成一个Class对象,用native方法实现
    protected final Class<?> defineClass(byte[] b, int off, int len){
       ...
    }
}

从上面的代码我们可以得到几个关键信息:

  • JVM 的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个 parent 字段,指向父加载器。
  • defineClass 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。
  • findClass 方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。
  • loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。

JDK 中有哪些默认的类加载器?它们的本质区别是什么?为什么需要双亲委托机制?JDK 中有 3 个类加载器,另外你也可以自定义类加载器,它们的关系如下图所示。

在这里插入图片描述

  • BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如rt.jar、resources.jar等。
  • ExtClassLoader 是扩展类加载器,用来加载\jre\lib\ext目录下 JAR 包。
  • AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。
  • 自定义类加载器,用来加载自定义路径下的类。

这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

这里请你注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有,如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制。

Tomcat 的类加载器

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

findClass 方法

我们先来看看 findClass 方法的实现,为了方便理解和阅读,我去掉了一些细节:


public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    
    Class<?> clazz = null;
    try {
            //1. 先在Web应用目录下查找类 
            clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
           throw e;
       }
    
    if (clazz == null) {
    try {
            //2. 如果在本地目录没有找到,交给父加载器去查找
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
       }
    
    //3. 如果父类也没找到,抛出ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }

    return clazz;
}

在 findClass 方法里,主要有三个步骤:
1、先在 Web 应用本地目录下查找要加载的类。
2、如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
3、如何父加载器也没找到这个类,抛出 ClassNotFound 异常。

loadClass 方法

接着我们再来看 Tomcat 类加载器的 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类加载器类加载,为什么?
        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);
}

loadClass 方法稍微复杂一点,主要有六个步骤:
1、先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。
2、如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
3、如果都没有,就让 ExtClassLoader 去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
4、如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
5、如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。
6、如果上述加载过程全部失败,抛出 ClassNotFound 异常。

从上面的过程我们可以看到,Tomcat 的类加载器打破了双亲委托机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。那为什么不先用系统类加载器 AppClassLoader 去加载?很显然,如果是这样的话,那就变成双亲委托机制了,这就是 Tomcat 类加载器的巧妙之处。

总结

我想打破双亲委托机制,能保证不同版本的类共存,就像一个tomcat下多个工程,使用了不同版本的spring,各加载各的互不影响。如果不打破双亲委托机制,都交由AppClassLoader去加载,那么相同包名相同类名的类就被判定已经加载过了,达不到加载不同版本的功能。由于自定义了类加载器,即使包名与类名相同,但类加载器不同依然被判断为不同的类

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Tomcat打破双亲委派 的相关文章

随机推荐

  • frida-dexdump工具实现APK脱壳

    目录 准备python3环境 安装firda 安装frida server 安装frida dexdump APP脱壳操作 准备python3环境 下载地址 python 3 9 6 amd64 exe
  • 开发新的开始---我的MacBook Pro生活

    第一次使用Mac 真的很新鲜 买的时候也是很咬牙的 毕竟才毕业辛辛苦苦工作了两个月全搭在这台电脑上了 现在穷的当当响 不过还是很开心 毕竟作为一个开发人员拥有一台Mac是个梦想吧 哈哈 虽然Mac不是什么大梦想啦 周末一直在配环境装软件 发
  • Java实现生成和校验图片验证码(详细)

    验证码校验是日常中很常见的场景 工作中难免会遇到了生成和校验验证码这样的需求 下面我们就来用java实现它 验证码生成和校验我用到了Hutool工具类 我们先引入他的依赖
  • 计算机操作系统--UNIX操作系统

    UNIX操作系统 UNIX操作系统是一种多用户 多任务的分时操作系统 它由最内层的硬件提供基本服 务 内核提供全部应用程序所需的各种服务 UNIX文件系统 UNIX文件系统采用树形带交叉勾连的目录结构 根目录即为 非叶节点是目录 文件 叶节
  • 字符编码 unicode 及其在javascript 中的使用

    一 javascript 使用 unicode16 字符集 可以使用中文变量名和函数名计算机使用 8 位 bit 二进制表示一个字节 Byte 计算机内存最小寻址单位就是 1 字节 早期为了在计算机上使用同一的方式使用字符 使用无符号整数来
  • jquery父页面和子页面之间的取元素

    一 父页面得到iframe中子页面的元素 iframe1 contents find form1 html 二 子页面获取父页面的元素 父页面元素id parent document 转载于 https my oschina net u 1
  • 计算机科学与技术专业选题推荐

    同学们好 这里是海浪学长的毕设系列文章 对毕设有任何疑问都可以问学长哦 大四是整个大学期间最忙碌的时光 一边要忙着准备考研 考公 考教资或者实习为毕业后面临的就业升学做准备 一边要为毕业设计耗费大量精力 近几年各个学校要求的毕设项目越来越难
  • MARS: Markov Molecular Sampling for Multi-Objective Drug Discovery

    本方法用于进行多目标的分子优化 原文链接在这 以下内容均为我自己的理解 如有理解不正确的地方欢迎大家斧正 整体方法 采样思路 将多个目标综合成一个score 也就是文中提到的公式 其中 的具体实现就是各个部分打分的求和 文章中选择了总共四个
  • java集成测试_到底什么是集成测试?

    你的问题其实要分两块儿来说 因为现在用的是手机所以先简要回答一二 不明白的话再补充 单元测试就是最小代码单元的针对性测试 可以是对象的一个属性 检查是否存在或值是否有效等等 也可以是一个函数或方法 检查其行为或输出是否如预期或者代码执行效能
  • 解析H264的SPS信息

    原文链接 在做音视频开发的时候 存在不解码视频帧的前提下需要获取视频宽高 帧率等信息 而H 264中的SPS数据可为我们提供这些相关的信息 在此之前 我们需要对一些协议和算法有一定的初步了解 后文中有完整的代码展示 H 264协议 我们在此
  • 分布式版本控制工具——git

    lt 1 gt 主页 我的代码爱吃辣 lt 2 gt 知识讲解 Linux git lt 3 gt 开发环境 Centos7 lt 4 gt 前言 git是一个开源的分布式版本控制系统 可以有效 高速地处理从很小到非常大的项目版本管理 也是
  • PRD 使用Pentaho Metadata Editor(PME)生成的metadata做数据源(5)

    使用Pentaho Metadata Editor PME 生成的metadata做数据源 Pentaho Report Designer PRD 可以支持多种数据源输入方式 Pentaho Metadata Editor作为自家平台中的一
  • GBDT去预测时序数据

    最近有一个需求 需要用到GBDT算法去实现对时序数据进行预测 回归任务 数据是从2011年1月到2020年4月份6个不同城市的房地产交易数据 由于在网上没有找到对应的基于时序数据来用GBDT算法的博客或者资料 我也去github上面找个 出
  • 组装数组对象

    1 创建数组let arryList 2 创建数组内的元素 JSON对象 let item 3 给元素赋值item value1 num1 item value2 num2 4 把元素放入数组arryList push item 5 多元素
  • linux中的9个权限位

    首先 我们通过linux的ls命令操作获得每个文件的权限 如下图 1 表示连接的文件数 admin 表示用户 admin表示用户所在的组 5572 表示文件大小 字节 Feb 20 11 43 表示最后修改日期 2 20 表示文件名 由上图
  • Python爬虫获取10页的图片、文本数据并传入linux上的mysql数据库中

    一 任务需求 爬取网址之家的网站排行信息 共获取6个指标 2张图片和4个文本字符串 观察发现每个网页共30个 一共需要爬取10页 并把图片存入PNG目录下 文本信息存入info txt文件中 最后上传到linux上的Mysql数据库中 二
  • SpringBoot+mybatisPlus + dynamic-datasource实现真正的动态切换数据源(附核心代码)

    文章目录 前言 创建主库 生成mapper等代码 定义新数据源 创建初始化runner类 创建Mybatis配置类 拦截器实现动态切换 前言 系统要调整为S A S S版实现多 租 户功能 首先想到的两个解决方案就是 1 通过表字段隔离租户
  • 大数据可视化界面截图(三)

    电商销售 购买力 舆情 集团数据 品牌电商
  • Vijava 学习笔记之(VirtualMachine 更改虚拟机系统磁盘大小)

    源代码 package com vmware client import com vmware util Session import com vmware vim25 import com vmware vim25 mo Created
  • Tomcat打破双亲委派

    复习复习JVM类加载机制 再谈谈 Tomcat 的类加载器如何打破 Java 的双亲委托机制 JVM 的类加载器 Java 的类加载 就是把字节码格式 class 文件加载到 JVM 的方法区 并在 JVM 的堆区建立一个java lang