实战分享:Tomcat打破双亲委派模型,实现Web应用独立与安全隔离的奥秘

目录

一、JVM 类加载机制

二、Tomcat 类加载器

        2.2 findClass 介绍

        3.2 loadClass 介绍

三、web应用隔离

        3.1 Spring 加载问题


        在开始文章内容之前,先来看三个问题

  1. 假如在 Tomcat 上运行了两个 Web 应用程序,两个 web 应用中有同名的Servlet,比如都叫UserController,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证他们不会冲突,那怎么才能实现隔离?
  2. 假如两个 web 应用都依赖同一个第三方 jar 包,比如spring,那 spring 的 jar 包被加载到内存后,Tomcat 保证这两个 web 应用能共享,也就是说 spring 的 jar 包只被加载一次,否则随着依赖第三方的增多,JVM的内存会爆炸,这时怎么做到的?
  3. 跟JVM一样,怎样隔离 Tomcat 本身和 web 应用类?

        以上三个问题本文会逐一来讲解,下面先来看下 JVM 的类加载机制。

一、JVM 类加载机制

        Java 的类加载就是把字节码格式 “.class” 文件加载到 JVM 方法区,并在 JVM 的堆中建立一个 java.lang.class 对象实例,用来封装 Java 类相关的数据和方法。

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

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

  • BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如 rt.jar、resource.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 对象。

二、Tomcat 类加载器

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

        2.2 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. 如果父类也没找到,抛出ClassNotFoundExceptionif (clazz == null) {throw new ClassNotFoundException(name);}return clazz;
}

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

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

        3.2 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 去加载。

三、web应用隔离

        先回答开头提到的第一个问题,如果我们使用 JVM 的 AppClassLoader 来加载 web 应用,AppClassLoader 只能加载一个 Servlet,再加载第二个同名的 Servlet 时,会返回第一个加载的 Servlet,同名的只被加载一次。Tomcat的解决方案是自定义一个类加载器 WebAppClassLoader,并且给每个 web 应用创建一个类加载器。不同的类加载器加载的类被认为是不同的类,即使名称相同,web 应用通过各自的类加载器来实现隔离。

        在来看第二个问题,多个 web 应用之间需要共享类库,并且不能重复加载相同的类。在双亲委派机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也是通过这种方式共享 JRE 核心类。因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就结局了。

        第三个问题,如何隔离 Tomcat 本身的类和 web 应用的类?要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,他们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。因此 Tomcat 又设计了一个类加载器CatalinaClassLoader,专门来加载 Tomcat 自身的类,这样设计有个问题,那 Tomcat 和 web 需要共享一些类时怎么办呢?

        还是再增加一个 CommonClassLoader,作为 CatalinaClassLoader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

        3.1 Spring 加载问题

        在 JVM 的实现中有一条规则,如果一个类由类加载器 A 加载,那么这个类的依赖类也由相同的类加载器完成。Spring 作为一个 bean 工厂,需要创建业务实体类,并且在创业业务类之前只要创建依赖类。

        前面提到,web 应用之间共享的 JAR 包可以交给 SharedClassLoader 来加载,从而避免了重复,Spring 作为共享的三方 JAR 包,它自己是由 SharedClassLoader 加载的,但是Spring又要去加载业务类,但是业务类不在 SharedClassLoader 对应的目录下,那该怎么办呢?

        Tomcat 使用了线程上下文加载器,它其实是一种类加载传递机制。这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中,就能把这个加载器取出来用。

        Tomcat 为每个 Web 应用创建一个 WebAppClassLoader 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。这样就完成了 SharedClassLoader 创建的 Spring 可以创建 WebAppClassLoader 下的业务类,是不是设计的很精妙呢?

        好了本期内容就介绍到这里。

往期经典推荐:

Tomcat架构究竟是什么?灵魂原来在这里-CSDN博客

你真的了解Tomcat一键启停吗?-CSDN博客

你所不知的Tomcat网络通信的玄机-CSDN博客

决胜高并发战场:Redis并发访问控制与实战解析-CSDN博客

TiDB内核解密:揭秘其底层KV存储引擎如何玩转键值对-CSDN博客

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/713780.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++数据结构与算法——二叉树的属性

C第二阶段——数据结构和算法&#xff0c;之前学过一点点数据结构&#xff0c;当时是基于Python来学习的&#xff0c;现在基于C查漏补缺&#xff0c;尤其是树的部分。这一部分计划一个月&#xff0c;主要利用代码随想录来学习&#xff0c;刷题使用力扣网站&#xff0c;不定时更…

AGI概念与实现

AGI AGI&#xff08;Artificial General Intelligence&#xff09;&#xff0c;中文名为“通用人工智能”或“强人工智能”&#xff0c;是指通过机器学习和数据分析等技术&#xff0c;使计算机具有类似于人类的认知和学习能力的技术. 多模态的大模型 &#xff08;Multimodal…

详细介绍如何用windows自带Hyper-V安装虚拟机(windows11和ubuntu22)

通过系统自带的hyper-v安装windows11&#xff0c;舒服又惬意&#xff0c;相比用第三方虚拟机软件速度快很多。 硬件准备 准备 系统需要符合能安装 Hyper-V 的最低要求windows版本含Hyper-V的功能 电脑空间 电脑要有足够的空间来安装你这个虚拟机。根据自己的磁盘容量情况来规…

2673. 使二叉树所有路径值相等的最小代价

给你一个整数 n 表示一棵 满二叉树 里面节点的数目&#xff0c;节点编号从 1 到 n 。根节点编号为 1 &#xff0c;树中每个非叶子节点 i 都有两个孩子&#xff0c;分别是左孩子 2 * i 和右孩子 2 * i 1 。 树中每个节点都有一个值&#xff0c;用下标从 0 开始、长度为 n 的整…

CloudCanal x Hive 构建高效的实时数仓

简述 CloudCanal 最近对于全周期数据流动进行了初步探索&#xff0c;打通了Hive 目标端的实时同步&#xff0c;为实时数仓的构建提供了支持&#xff0c;这篇文章简要做下分享。 基于临时表的增量合并方式基于 HDFS 文件写入方式临时表统一 Schema任务级的临时表 基于临时表的…

【Linux实践室】Linux初体验

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;Linux 目录结构介绍2.2 &#x1f514;Linux …

MySQL:一行记录如何

1、表空间文件结构 表空间由段「segment」、区「extent」、页「page」、行「row」组成&#xff0c;InnoDB存储引擎的逻辑存储结构大致如下图&#xff1a; 行 数据库表中的记录都是按「行」进行存放的&#xff0c;每行记录根据不同的行格式&#xff0c;有不同的存储结构。 页…

hippy 调试demo运行联调-mac环境准备篇

适用对于终端编译环境不熟悉的人看&#xff0c;仅mac端 hippy 调试文档官网地址 前提&#xff1a;请使用node16 联调预览效果图&#xff1a; 编译iOS Demo环境准备 未跑通&#xff0c;待补充 编译Android Demo环境准备 1、正常安装Android Studio 2、下载Android NDK&a…

Windows系统误删文件恢复

最近很多用户反馈误删文件的场景比较多.下面华仔将讲解数据恢复的原理和过程.以及一些注意事项。 建议的数据恢复软件 1.EaseUS Data Recovery Wizard(易我数据恢复)需要断网使用 2.Wondershare Recoverit(万兴数据恢复)&#xff0c; Windows系统删除文件原理&#xff1a;如果是…

HTTPS是什么,详解它的加密过程

目录 1.前言 2.两种加密解密方式 2.1对称加密 2.2非对称加密 3.HTTPS的加密过程 3.1针对明文的对称加密 3.2针对密钥的非对称加密 3.3证书的作用 1.前言 我们知道HTTP协议是超文本传输协议,它被广泛的应用在客户端服务器上,用来传输文字,图片,视频,js,html等.但是这种传…

java数据结构与算法刷题-----LeetCode572. 另一棵树的子树(经典题,树字符串化KMP)

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 1. 暴力求解&#xff0c;深度优先2. KMP算法进行串匹配 1. 暴力求…

WinForm、Wpf自动升级 AutoUpdater.NET

Github AutoUpdater.NET 目录 一、IIS部署 更新站点 二、创建Winform 一、IIS部署 更新站点 IIS默认站点目录下创建 目录 Downloads、Updates Updates目录创建文件 UpdateLog.html、AutoUpdaterStarter.xml UpdateLog.html&#xff1a; <html><body><h1…

mysql学习--binlog与gtid主从同步

基础环境 基于centOS7-MySQL8.0.35版本 我们先准备一台主服务器两台从服务器来实现我们主从同步的诉求 Master&#xff1a;192.168.75.142 slave1:192.168.75.143 slave&#xff1a;192.168.75.145 binlog主从同步 主库配置 #我们需要在主从库中都需要添加server_id&am…

大龙谈智能内容开通视频号啦

大家好&#xff0c;大龙谈只能内容开通视频号了&#xff0c;欢迎大家扫码关注&#xff1a;

RISC-V特权架构 - 中断与异常概述

RISC-V特权架构 - 中断与异常概述 1 中断概述2 异常概述3 广义上的异常3.1 同步异常3.2 异步异常3.3 常见同步异常和异步异常 本文属于《 RISC-V指令集基础系列教程》之一&#xff0c;欢迎查看其它文章。 1 中断概述 中断&#xff08;Interrupt&#xff09;机制&#xff0c;即…

RocketMQ安装

mq服务端安装配置启动把windows做成服务 mq管理界面安装配置启动 mq服务端 安装 RocketMQ下载地址 配置 ROCKETMQ_HOME D:\google-d\rocketmq-all-5.2.0-bin-release启动 # bin目录cmd输入 start mqnamesrv.cmd把windows做成服务 http://t.csdnimg.cn/qd2RD mq管理界面 …

ubuntu22.04安裝mysql8.0

官网下载mysql&#xff1a;MySQL :: Download MySQL Community Server 将mysql-server_8.0.20-2ubuntu20.04_amd64.deb-bundle.tar上传到/usr/local/src #解压压缩文件 tar -xvf mysql-server_8.0.20-2ubuntu20.04_amd64.deb-bundle.tar解压依赖包依次输入命令 sudo dpkg -i m…

EasyRecovery数据恢复软件2024最新版包括Windows和Mac

EasyRecovery数据恢复软件适用于多种环境和使用场景。首先&#xff0c;它适用于各种操作系统&#xff0c;包括Windows和Mac。无论用户使用的是哪种操作系统&#xff0c;都可以使用该软件进行数据恢复。 其次&#xff0c;EasyRecovery支持从各种存储设备和媒介中恢复数据&#…

自定义BeanNameGenerator生成规则

通过点进ComponentScan注解进入源码可以看到 追随BeanNameGenerator进入源码可以看到该类是个借口且只有一个方法 点击上面黑色箭头出现两个实现方法 点击第一个方法 进入determineBeanNameFromAnnotation方法中 通过上诉自定义一个生成beanName方法 先创建一个CustomeBeanN…

60 个 CSS 选择器,一网打尽!

CSS 选择器用于选择 HTML 元素并将样式应用于它们。使用这些选择器&#xff0c;可以定义特定条件下应用哪些样式。除了普通的选择器外&#xff0c;还有伪类和伪元素&#xff0c;用于选择具有特定状态或特定部分的元素&#xff0c;并将样式应用于它们。本文将通过图文并茂的方式…