当前位置: 首页 > news >正文

深入解析Tomcat类加载器:为何及如何打破Java双亲委派模型

引言:Java类加载的"家规"与现实需求

在Java世界中,类加载器的双亲委派模型就像一套严格的"家规",规定了类加载的层级秩序。这套机制保证了Java核心库的安全性和稳定性,但在复杂的现实应用场景中,有时却显得力不从心。本文将通过深入分析Tomcat的类加载器设计,揭示为何以及如何打破这一模型,并在专业解释中穿插生动比喻,帮助读者更好地理解这一核心机制。

在标准的Java应用中,这套“家规”运行得井然有序——每当一个类需要被加载时,它必须先“向上请示”,由父类加载器尝试加载,只有当父类无法完成时,才轮到子类出手。这种双亲委派模型(Parent Delegation Model)就像家族中的长幼尊卑:晚辈不得越权,凡事必须先问长辈。

然而,当Java应用服务器如Tomcat登场时,这套“家规”就面临了严峻挑战。想象一下:一个公司里同时运行着多个独立项目(Web应用),每个项目都自带“家规”(依赖库),甚至有些项目还希望“自立门户”(使用不同版本的Servlet API)。如果继续死守“长辈优先”的规矩,就会出现以下问题:

  • 缺乏灵活性:在像Tomcat这样的Web容器中,需要同时部署多个Web应用(WAR包)。这些应用可能依赖于不同版本的同一个第三方库(例如,一个应用用Spring 4,另一个用Spring 5)。如果严格遵循双亲委派,父加载器(如Shared加载器)加载了一个库后,子加载器(Webapp加载器)就无法再加载自己版本不同的同一个库了,这会导致库版本冲突。

  • 隔离性要求:Web应用A不应该能访问Web应用B的类,这是出于安全和稳定性的考虑。如果所有Web应用都共用同一个类加载路径,就无法实现应用级别的隔离。

一、标准Java类加载器的“家规”回顾

在普通Java程序中,类加载器遵循严格的层次结构和双亲委派:
在这里插入图片描述

// 查看类加载器的示例代码
public class ClassLoaderView {public static void main(String[] args) {// 查看当前类的类加载器 (默认是AppClassLoader)System.out.println("ClassLoader of this class: " + ClassLoaderView.class.getClassLoader());// 查看扩展类加载器System.out.println("Extension ClassLoader: " + ClassLoaderView.class.getClassLoader().getParent());// 查看启动类加载器 (输出为null)System.out.println("Bootstrap ClassLoader: " + ClassLoaderView.class.getClassLoader().getParent().getParent());// 查看String类的加载器 (由Bootstrap加载,输出为null)System.out.println("ClassLoader of String: " + String.class.getClassLoader());}
}

二、Tomcat的类加载器层次结构(以Tomcat 9为例)

Tomcat构建了一个多层、定制化的类加载器体系:

BootstrapSystem (Bootstrap加载)Common[ WebappClassLoader for App1 ]
[ WebappClassLoader for App2 ]
[ WebappClassLoader for App3 ]JasperLoader (JSP编译类,可选)

在这里插入图片描述
关键点:每个Web应用都有自己的WebappClassLoader实例,实现类空间隔离。

三、Tomcat的类加载流程(打破点详解)

当一个Web应用请求加载类com.example.Service时,WebappClassLoader执行如下步骤:

1. 检查是否已加载该类(缓存)
2. 如果是Java核心类(java.*, javax.*, sun.*等) → 委派给父类加载器(遵循双亲委派)
3. 否则 → 先在本应用内查找:- 查找 WEB-INF/classes/- 查找 WEB-INF/lib/*.jar
4. 如果本应用找不到 → 再委派给父类加载器(Common → System → Bootstrap)

✅ 打破点:第3步“先自己查”是关键,违背了“先问长辈”的双亲委派原则。

四、WebAppClassLoader

Tomcat使用WebAppClassLoader对应每个Context容器下的Loader,来进行容器间类的隔离

而如果容器间需要共享相同的类,再增加个共享的类加载器SharedClassLoader作为WebAppClassLoader的父类

五、源码解析

 1. 判断当前运用是否已经启动, 未启动, 则直接抛异常2. 调用 findLocaledClass0 从 resourceEntries 中判断 class 是否已经加载 OK3. 调用 findLoadedClass(内部调用一个 native 方法) 直接查看对应的 WebappClassLoader 是否已经加载过4. 调用 binaryNameToPath 判断是否 当前 class 是属于 J2SE 范围中的, 若是的则直接通过 ExtClassLoader, BootstrapClassLoader 进行加载 (这里是双亲委派)5. 在设置 JVM 权限校验的情况下, 调用 securityManager 来进行权限的校验(当前类是否有权限加载这个类, 默认的权限配置文件是 ${catalina.base}/conf/catalina.policy)6. 判断是否设置了双亲委派机制 或 当前 WebappClassLoader 是否能加载这个 class (通过 filter(name) 来决定), 将最终的值赋值给 delegateLoad7. 根据上一步中的 delegateLoad 来决定是否用 WebappClassloader.parent(也就是 sharedClassLoader) 来进行加载, 若加载成功, 则直接返回8. 上一步若未加载成功, 则调用 WebappClassloader.findClass(name) 来进行加载9. 若上一还是没有加载成功, 则通过 parent 调用 Class.forName 来进行加载10. 若还没加载成功的话, 那就直接抛异常
 public synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {if (log.isDebugEnabled())log.debug("loadClass(" + name + ", " + resolve + ")");Class<?> clazz = null;// Log access to stopped classloader         // 1.  判断程序是否已经启动了, 未启动 OK, 就进行加载, 则直接抛异常if (!started) {try {throw new IllegalStateException();} catch (IllegalStateException e) {log.info(sm.getString("webappClassLoader.stopped", name), e);}}// (0) Check our previously loaded local class cache// 2. 当前对象缓存中检查是否已经加载该类, 有的话直接返回 Classclazz = findLoadedClass0(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Returning class from cache");if (resolve)resolveClass(clazz);return (clazz);}// (0.1) Check our previously loaded class cache// 3. 是否已经加载过该类 (这里的加载最终会调用一个 native 方法, 意思就是检查这个 ClassLoader 是否已经加载过对应的 class 了哇)clazz = findLoadedClass(name);if (clazz != null) {if (log.isDebugEnabled())log.debug("  Returning class from cache");if (resolve)resolveClass(clazz);return (clazz);}// (0.2) Try loading the class with the system class loader, to prevent // 代码到这里发现, 上面两步是 1. 查看 resourceEntries 里面的信息, 判断 class 是否加载过, 2. 通过 findLoadedClass 判断 JVM 中是否已经加载过, 但现在 直接用 j2seClassLoader(Luancher.ExtClassLoader 这里的加载过程是双亲委派模式) 来进行加载//       the webapp from overriding J2SE classes                        // 这是为什么呢 ? 主要是 这里直接用 ExtClassLoader 来加载 J2SE 所对应的 class, 防止被 WebappClassLoader 加载了String resourceName = binaryNameToPath(name, false);                    // 4. 进行 class 名称 转路径的操作 (文件的尾缀是 .class)if (j2seClassLoader.getResource(resourceName) != null) {                // 5. 这里的 j2seClassLoader 其实就是 ExtClassLoader, 这里就是 查找 BootstrapClassloader 与 ExtClassLoader 是否有权限加载这个 class (通过 URLClassPath 来确认)try {clazz = j2seClassLoader.loadClass(name);if (clazz != null) {if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {// Ignore}}// (0.5) Permission to access this class when using a SecurityManager   // 6. 这里的 securityManager 与 Java 安全策略是否有关, 默认 (securityManager == null), 所以一开始看代码就不要关注这里if (securityManager != null) {int i = name.lastIndexOf('.');if (i >= 0) {try {securityManager.checkPackageAccess(name.substring(0,i));   // 7. 通过 securityManager 对 是否能加载 name 的权限进行检查 (对应的策略都在 ${catalina.base}/conf/catalina.policy 里面进行定义)} catch (SecurityException se) {String error = "Security Violation, attempt to use " +"Restricted Class: " + name;log.info(error, se);throw new ClassNotFoundException(error, se);}}}boolean delegateLoad = delegate || filter(name);   // 8. 读取 delegate 的配置信息, filter 主要判断这个 class 是否能由这个 WebappClassLoader 进行加载 (false: 能进行加载, true: 不能被加载)// (1) Delegate to our parent if requested// 如果配置了 parent-first 模式, 那么委托给父加载器      // 9. 当进行加载 javax 下面的包 就直接交给 parent(sharedClassLoader) 来进行加载 (为什么? 主要是 这些公共加载的资源统一由 sharedClassLoader 来进行加载, 能减少 Perm 区域的大小)if (delegateLoad) {   // 10. 若 delegate 开启, 优先使用 parent classloader( delegate 默认是 false); 这里还有一种可能, 就是 经过 filter(name) 后, 还是返回 true, 那说明 WebappClassLoader 不应该进行加载, 应该交给其 parent 进行加载if (log.isDebugEnabled())log.debug("  Delegating to parent classloader1 " + parent);try {clazz = Class.forName(name, false, parent);   // 11. 通过 parent ClassLoader 来进行加载 (这里构造函数中第二个参数 false 表示: 使用 parent 加载 classs 时不进行初始化操作, 也就是 不会执行这个 class 中 static 里面的初始操作 以及 一些成员变量ed赋值操作, 这一动作也符合 JVM 一贯的 lazy-init 策略)if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from parent");if (resolve)resolveClass(clazz);return (clazz);   // 12. 通过 parent ClassLoader 加载成功, 则直接返回}} catch (ClassNotFoundException e) {// Ignore}}// (2) Search local repositoriesif (log.isDebugEnabled())log.debug("  Searching local repositories");try {// 从 WebApp 中去加载类, 主要是 WebApp 下的 classes 目录 与 lib 目录clazz = findClass(name);       // 13. 使用当前的 WebappClassLoader 加载if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from local repository");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {// Ignore}// (3) Delegate to parent unconditionally// 如果在当前 WebApp 中无法加载到, 委托给 StandardClassLoader 从 $catalina_home/lib 中去加载if (!delegateLoad) {                                                 // 14. 这是在 delegate = false 时, 在本 classLoader 上进行加载后, 再进行操作这里if (log.isDebugEnabled())log.debug("  Delegating to parent classloader at end: " + parent);try {clazz = Class.forName(name, false, parent);// 15. 用 WebappClassLoader 的 parent(ExtClassLoader) 来进行加载if (clazz != null) {if (log.isDebugEnabled())log.debug("  Loading class from parent");if (resolve)resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {// Ignore}}throw new ClassNotFoundException(name);   // 16. 若还是加载不到, 那就抛出异常吧
}
http://www.dtcms.com/a/392510.html

相关文章:

  • 基于BP神经网络的PID控制器matlab参数整定和性能仿真
  • RabbitMQ死信队列与幂等性处理的性能优化实践指南
  • 基于python全国热门景点旅游管理系统的设计与实现
  • 鸿蒙Next ArkTS卡片生命周期:深入理解与管理实践
  • 荣耀手机(安卓)快速传数据换机到iPhone17 Pro
  • Linux的线程池
  • [bitcoin白皮书_1] 时间戳服务器 | 简化支付验证
  • OAuth 认证在电商 API 中的实现与安全
  • Linux 是什么?初学者速查表
  • openharmony之AV_CodeC音视频编解码模块驱动实现原理详解(三)
  • Llamaindex-Llama_indexRAG进阶_Embedding_model与ChromaDB-文档切分与重排序
  • 如何使用WordToCard自动拆分文章制作小红书卡片
  • RTX 4090重塑数字内容创作:4K视频剪辑与3D渲染的效率革命
  • Spring AI开发指导-MCP
  • C++/操作系统
  • 动手学深度学习(pytorch版):第八章节—循环神经网络(4)循环神经网络
  • Jenkins与Arbess,CICD工具一文全面对比分析
  • 矩阵、线性代数
  • react常用的hooks
  • 重构的艺术:从‘屎山’恐惧到优雅掌控的理性之旅
  • 在c++中,怎么理解把析构函数设置为virtual呢?
  • CUDA性能优化 ---- 通过矢量化内存访问提高性能
  • 【序列晋升】39 Spring Data REST 的优雅实践,让数据交互更符合 REST 规范
  • 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL 凭什么这么香?
  • 【2025PolarCTF秋季个人赛】WEB方向wp
  • Go基础:Go语言函数和方法详解
  • Redis 遍历指定格式的所有key
  • 插入mathtype/latex公式在word中行间距变高了
  • 设计模式学习(四)代理模式、适配器模式
  • ​​[硬件电路-279]:DRV8818PWP功能概述、管脚定义