Tomcat是如何打破双亲委派机制?
本文基于Tomcat9.0.55源码分析,源码环境搭建教程。
引言
在学习 Tomcat 的类加载器机制之前,我们先大致了解 JVM 的类加载机制。JVM 采用双亲委派机制来加载类,其核心逻辑是优先使用父加载器尝试加载类:当某个类加载器收到加载请求时,它会先将请求委托给父加载器,只有当父加载器无法完成加载时,自身才会尝试加载。例如,最底层的应用类加载器会先向上委托给扩展类加载器,扩展类加载器再委托给顶层的启动类加载器。这种机制确保 Java 核心类库(如java.lang.Object
)始终由启动类加载器统一加载,避免低层级自定义类覆盖核心类,有效保障了核心类的安全性和唯一性。
但是,Tomcat 作为专业的 Web 服务器,JVM 原生的双亲委派机制并不能满足多应用隔离、热部署等特殊需求。例如,同一 Tomcat 实例部署多个 Web 应用时,若不同应用依赖同一类库的不同版本,按双亲委派机制,先加载的版本会被所有应用共享,导致版本冲突;热部署场景下,若类加载器无法动态替换旧类,新部署的应用也无法正常运行。所以,Tomcat 实现了一套改良的类加载机制——保留类加载器父子层级关系,但反转加载顺序,即每个 Web 应用的类加载器(WebappClassLoader
)优先加载自身WEB-INF/classes
和WEB-INF/lib
路径下的类,失败后再委托给父加载器。这种设计既利用了层级结构管理类库,又通过 “先子后父” 的加载顺序,实现了应用间类库隔离,并支持动态替换类文件,为 Web 应用的灵活部署与高效运行提供了保障。
JVM 的类加载机制
Tomcat 的类加载器层次结构
Tomcat 的类加载器架构是其实现 Web 应用隔离和动态加载的关键所在,它在 JVM 原生类加载器的基础上进行了扩展和定制,构建了一个更加复杂且灵活的类加载体系。在这个体系中,不同类型的类加载器各司其职,共同协作,确保了 Web 应用的高效运行。
Tomcat 的类加载器层次结构主要包含以下几种:
1. Bootstrap ClassLoader(启动类加载器)
层级位置:顶层(C++ 实现,无父加载器)
加载范围:
%JRE_HOME%/lib
下的核心类库(如rt.jar
、tools.jar
),包含 Java 基础类(如java.lang.*
)。作用:为 Tomcat 提供最底层的 JVM 核心类支持,所有其他类加载器的依赖根基。
2. ExtClassLoader(扩展类加载器)
父加载器:Bootstrap ClassLoader
加载范围:
%JRE_HOME%/lib/ext
或-Djava.ext.dirs
指定路径的类库。作用:加载 JVM 扩展类,Tomcat 未直接扩展此加载器,但处于类加载链中。
3. AppClassLoader(应用类加载器,即 System ClassLoader)
父加载器:ExtClassLoader
加载范围:JVM 的
classpath
路径(通过-cp
或环境变量指定)。作用:加载 Tomcat 启动类(如
org.apache.catalina.startup.Bootstrap
)及应用级依赖,是 Tomcat 自定义类加载器的父级基础。
4. CommonClassLoader(Tomcat 公共类加载器)
父加载器:AppClassLoader
加载范围:
$CATALINA_HOME/lib
下的类库(如 Tomcat 核心框架tomcat-embed-core.jar
)。作用:承载 Tomcat 公共依赖,供所有 Web 应用共享(如 JDBC 驱动、日志框架),避免重复加载。
5. WebappClassLoader(Web 应用类加载器)
父加载器:CommonClassLoader(默认配置)
加载范围:单个 Web 应用的
WEB-INF/classes
和WEB-INF/lib/*.jar
。作用:每个 Web 应用独立实例,实现类隔离(如不同应用可依赖不同版本类库),优先加载自身类库(打破原生双亲委派)。
类加载器层级链总结
BootstrapClassLoader(顶层)
↳ ExtClassLoader
↳ AppClassLoader(System ClassLoader)
↳ CommonClassLoader(Tomcat公共加载器)
↳ WebappClassLoader(各Web应用独立加载器)
关键特性
父子关系固定:通过
ClassLoader.parent
属性形成层级,Tomcat 未改变原生父子链结构。加载顺序反转:WebappClassLoader 优先加载自身类库,再委托父加载器,实现应用隔离。
职责分工:顶层加载 JVM 核心类,中间层加载 Tomcat 公共类,底层加载应用私有类,形成 “隔离 - 共享” 的协作体系。
Tomcat 为什么打破双亲委派机制?
解决类隔离问题
在一个 Tomcat 容器中往往需要部署多个 Web 应用,这些 Web 应用可能会依赖同一个第三方类库的不同版本。例如,Web 应用 A 使用spring - core
版本为 5.3.10,而 Web 应用 B 使用spring - core
版本为 5.2.15。在传统的双亲委派机制下,由于类加载器首先会将加载请求委托给父类加载器,若父类加载器已经加载了某个版本的spring - core
类库,那么子类加载器就无法再加载其他版本的相同类库,这就会导致类冲突问题,使得不同 Web 应用无法正常运行。
Tomcat 打破双亲委派机制后,每个 Web 应用都拥有自己独立的 WebApp ClassLoader。当 WebApp ClassLoader 收到类加载请求时,会首先尝试在自己的加载路径(WEB - INF/classes
和WEB - INF/lib
)中加载类。以spring - core
类库为例,Web 应用 A 的 WebApp ClassLoader 会在其对应的WEB - INF/lib
目录下查找并加载版本为 5.3.10 的spring - core
类库,而 Web 应用 B 的 WebApp ClassLoader 会在自己的WEB - INF/lib
目录下查找并加载版本为 5.2.15 的spring - core
类库。这样,不同 Web 应用的类库相互隔离,即使依赖相同类库的不同版本,也不会相互干扰,保证了每个 Web 应用的类库独立性和相互隔离性。
实现热部署
在 Java Web 开发中,热部署是一个非常重要的功能,它允许在不重启 Tomcat 服务器的情况下,动态更新 Web 应用的代码和资源。以 JSP 文件为例,在开发过程中,开发者经常需要对 JSP 文件进行修改和调试。如果每次修改 JSP 文件都需要重启 Tomcat 服务器,将会极大地降低开发效率。
Tomcat 通过打破双亲委派机制来实现热部署功能。每个 JSP 文件都对应一个唯一的 JasperLoader 类加载器。当 Tomcat 检测到 JSP 文件被修改时,会丢弃当前的 JasperLoader 实例,并重新创建一个新的 JasperLoader 来加载修改后的 JSP 文件。这是因为在传统的双亲委派机制下,类加载器一旦加载了某个类,就不会重新加载,即使类文件发生了变化。而 Tomcat 打破双亲委派机制后,JasperLoader 在加载 JSP 文件时,会优先从自身的加载路径中加载,当 JSP 文件被修改,新的 JasperLoader 会重新从文件系统中读取修改后的 JSP 文件并编译成字节码,然后加载到 JVM 中,从而实现了 JSP 文件的热部署。这种方式使得开发者可以实时看到 JSP 文件修改后的效果,大大提高了开发效率。
Tomcat 打破双亲委派机制的源码分析
WebappClassLoaderBase 类介绍
在 Tomcat 的类加载体系中,WebappClassLoaderBase
类占据着核心地位,它是实现 Tomcat 打破双亲委派机制的关键类。WebappClassLoaderBase
类继承自URLClassLoader
,是 Tomcat 中负责 Web 应用类加载的基础类,众多具体的 Web 应用类加载器(如WebappClassLoader
)都继承自它。
WebappClassLoaderBase
类的主要职责是为每个 Web 应用提供独立的类加载环境,实现 Web 应用之间的类隔离。它维护了一系列与 Web 应用相关的资源路径,包括WEB - INF/classes
目录和WEB - INF/lib
目录下的类库,这些路径是它加载 Web 应用类的重要依据。同时,它还具备管理类加载过程中的缓存、权限控制以及资源查找等功能,确保类加载的高效性和安全性。
例如,在一个包含多个 Web 应用的 Tomcat 服务器中,每个 Web 应用都有自己对应的WebappClassLoaderBase
实例。当 Web 应用 A 需要加载某个类时,其对应的WebappClassLoaderBase
会在 Web 应用 A 的WEB - INF/classes
和WEB - INF/lib
目录下查找该类,与 Web 应用 B 的类加载过程相互隔离,避免了不同 Web 应用之间的类冲突。
loadClass 方法重写分析
WebappClassLoaderBase
类通过重写loadClass
方法来打破双亲委派机制,实现 Web 应用类的 “本地优先” 加载策略。下面结合具体的代码示例进行详细分析:
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
Class<?> clazz = null;
// 检查web应用程序是否处于停止状态,停止状态不允许加载新类
checkStateForClassLoading(name);
// 从web应用程序类加载器本地缓存中查找已加载的类
clazz = findLoadedClass0(name);
if (clazz != null) {
return clazz;
}
// 从系统类加载器缓存中查找已加载的类
clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
if (clazz != null) {
return clazz;
}
String resourceName = binaryNameToPath(name, false);
// 获取javaseLoader,即:ExtClassLoader ,确保java核心类由扩展类加载器加载
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
// 通过资源检查,判断需要加载的类是否是JRE核心类,如果是的话,就使用javaseClassLoader加载
...
if (tryLoadingFromJavaseLoader) {
// 使用扩展类加载器加载
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
return clazz;
}
}
// 安全管理器权限检查,确保 Web 应用只能访问被允许的包,防止非法访问敏感包(如java.lang.reflect)
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
throw new ClassNotFoundException(error, se);
}
}
}
// 判断是否需要委托父类AppClassLoader加载,delegate默认为false
// 如果配置为true,则完全按照双亲委派机制加载
// filter过滤逻辑为,根据包名类名来判断,是否是JRE核心类、J2EE类、Tomcat核心类等。如果是,则委托父类加载
boolean delegateLoad = delegate || filter(name, true);
if (delegateLoad) {
// 委托给父加载器(SystemClassLoader)
clazz = Class.forName(name, false, parent);
if (clazz != null) {
return clazz;
}
}
// 重写了findClass方法,在Web应用本地路径查找类
// 这里体现了打破双亲委派,先由webAppClassLoader进行加载类
clazz = findClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// 若未启用委托,并且WebAppClassLoader未加载到,最终委托给父加载器
if (!delegateLoad) {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
return clazz;
}
}
}
throw new ClassNotFoundException(name);
}
上述代码中,loadClass
方法的执行流程如下:
检查类是否已加载:首先通过
findLoadedClass0
和findLoadedClass
方法检查类是否已经在本地缓存或系统类缓存中加载过,如果已加载,则直接返回缓存中的类。尝试使用 ExtClassLoader 加载:获取
ExtClassLoader
类加载器,尝试从ExtClassLoader
的加载路径中加载类。如果加载成功,则返回加载的类。判断是否委托父类加载器加载:根据
delegate
属性或filter
方法的判断结果,决定是否委托父类加载器加载类。如果delegateLoad
为true
,则尝试委托父类加载器通过Class.forName(name, false, parent)
方法加载类。本地目录搜索并加载:如果委托父类加载器加载失败,或者未启用委托加载,则在 Web 应用的本地目录(
WEB - INF/classes
和WEB - INF/lib
)中搜索并通过findClass
方法加载类。再次委托父类加载器加载(如果未启用委托加载):如果本地加载失败且未启用委托加载,则再次委托父类加载器加载类。
抛出异常:如果上述所有步骤都无法加载类,则抛出
ClassNotFoundException
异常。
从这个流程可以看出,WebappClassLoaderBase
的loadClass
方法打破了双亲委派机制中 “先委托父类加载器加载” 的规则,优先尝试在本地目录中加载类,只有在本地无法加载时才委托给父类加载器,从而实现了 Web 应用类的隔离和 “本地优先” 加载。
findClass 方法分析
findClass
方法在WebappClassLoaderBase
类中起着至关重要的作用,它主要负责在 Web 应用的本地目录下查找要加载的类。以下是对findClass
方法的深入剖析:
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
checkStateForClassLoading(name);
// 安全权限检查,阻止 Web 应用定义核心包(如 java.、javax.)的类
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
securityManager.checkPackageDefinition(name.substring(0,i));
}
}
Class<?> clazz = null;
// 安全管理
if (securityManager != null) {
// PrivilegedFindClassByName 内部直接调用 findClassInternal,但绕过部分安全检查
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
// 执行类查找
clazz = findClassInternal(name);
}
// 如果没有找类,是否需要从外部仓库加载。调用父类的findClass加载
if ((clazz == null) && hasExternalRepositories) {
clazz = super.findClass(name);
}
return clazz;
}
在上述代码中,findClass
方法的执行逻辑如下:
权限检查和状态检查:通过
checkStateForClassLoading
方法进行类加载状态检查,确保当前环境适合类加载。同时,如果存在安全管理器,会使用PrivilegedAction
进行权限控制下的类查找操作。本地目录查找:优先调用
findClassInternal
方法在 Web 应用的本地目录(WEB - INF/classes
和WEB - INF/lib
)下查找要加载的类。findClassInternal
方法会遍历 Web 应用的类路径,查找对应的类文件,并将其转换为Class
对象。例如,它会在WEB - INF/classes
目录下直接查找未压缩的类文件,或者在WEB - INF/lib
目录下的 Jar 包中查找类文件。父类查找:如果在本地目录未找到类,并且 Web 应用配置了外部资源库(
hasExternalRepositories
为true
),则调用父类的findClass
方法,委托父类加载器在其加载路径中查找类。这一步体现了 Tomcat 在类加载过程中的灵活性,当本地无法找到类时,仍然可以借助父类加载器的能力来查找类,以满足 Web 应用对类的需求。异常处理和结果返回:如果在整个查找过程中都未找到类,则抛出
ClassNotFoundException
异常;如果找到类,则返回加载的Class
对象。
通过对findClass
方法的分析可以看出,它紧密配合loadClass
方法,在实现 Web 应用类的 “本地优先” 加载策略中发挥了关键作用,确保了 Web 应用能够优先加载自己目录下的类,实现了类的隔离和独立管理。
总结
Tomcat 打破双亲委派机制是为了满足 Web 应用开发中类隔离和热部署的特殊需求,通过自定义类加载器WebappClassLoaderBase
重写loadClass
和findClass
方法来实现。在实际应用中,虽然这种打破机制带来了灵活性,但也引发了诸如ClassNotFoundException
、类冲突和热部署失败等问题,需要开发者通过合理的配置和调试来解决。