Tomcat通过重写WebAppClassLoader的loadClass方法实现Web应用隔离:先查本应用/WEB-INF/classes与lib,未找到再委托父加载器;各应用独享类加载器,同名类因加载器不同而互不冲突;共享库由SharedClassLoader统一加载;线程上下文类加载器确保框架可访问应用类。

Tomcat 的 Web 应用隔离不是靠配置或约定实现的,而是靠重写 loadClass 和 findClass 两个方法强行绕过双亲委派逻辑——这是唯一可靠、可验证的机制。
为什么必须重写 loadClass 才算真正破坏双亲委派?
标准 ClassLoader.loadClass 方法内部自带双亲委派逻辑:先查父加载器,再查自己。不覆盖它,就永远走默认流程。
Tomcat 的 WebAppClassLoader 显式重写了该方法,核心改动是:把“先委托父类”改成“先查自己”。具体表现为:
- 调用
findLoadedClass(name)检查本加载器是否已加载该类 - 若未加载,**跳过父加载器调用**,直接执行
findClass(name)尝试从WEB-INF/classes或WEB-INF/lib加载 - 仅当
findClass抛出ClassNotFoundException时,才退回到父加载器(如SharedClassLoader)
这个顺序翻转,就是隔离的起点。不改 loadClass,只改 findClass 是无效的——因为请求根本到不了你写的 findClass。
findClass 的实现细节决定隔离粒度
findClass 不负责委派,只负责“从哪找”。Tomcat 在这里做了三件事:
- 扫描
WEB-INF/classes下的 .class 文件路径,匹配类名转为资源路径 - 遍历
WEB-INF/lib中每个 JAR,用JarURLConnection打开并查找对应 entry - 对找到的字节码调用
defineClass,生成Class对象(注意:不触发初始化)
关键点在于:这些路径是每个 WebAppClassLoader 实例独享的。两个应用即使类名完全相同,因加载器不同,生成的 Class 对象在 JVM 中属于不同类型,无法强制转换,也不会互相污染。
共享类(如 Spring)如何避免重复加载?
共享依赖不靠“同一个类加载器”,而靠“父子加载器链上的位置控制”:
-
SharedClassLoader是WebAppClassLoader的父加载器,但不是它的“委托优先目标” - 只有当
WebAppClassLoader.findClass查不到时,才触发对父加载器的loadClass调用 -
SharedClassLoader自身仍遵循双亲委派,会继续向上委托给CatalinaClassLoader或CommonClassLoader,最终落到 JVM 级别共享路径(如$CATALINA_HOME/lib)
所以,Spring 的 class 只会被 SharedClassLoader 加载一次;两个 Web 应用拿到的是同一个 Class 实例,但它们自己的业务类仍是隔离的——这种“局部打破 + 局部保留”的混合模型,才是实际可行的设计。
容易忽略的兼容性陷阱
真实部署中,以下三点常被低估:
- 以
java.开头的类,无论你怎么重写loadClass,JVM 都会强制由启动类加载器加载,否则抛SecurityException—— 这是硬编码在ClassLoader原生逻辑里的 -
Thread.currentThread().getContextClassLoader()默认继承自主线程,若 Web 应用内手动设置了它,可能意外绕过WebAppClassLoader,导致类加载失败 - 某些 JDBC 驱动(如旧版 MySQL Connector/J)会在静态块中主动调用
Class.forName,若没显式传入WebAppClassLoader,就会走系统默认加载器,引发ClassNotFoundException或跨应用污染
真正理解 Tomcat 类隔离,不在于记住“它打破了双亲委派”,而在于看清:它只在 loadClass 入口处做了一次有节制的翻转,并依靠加载路径隔离 + 父子链回退 + JVM 安全兜底,才让上百个 Web 应用能在同一 JVM 中互不干扰地共存。


















