0%

Tomcat类加载器

Tomcat 类加载器:打破双亲委派的隔离机制

Java 类加载器的双亲委派模型通过层级委派确保类加载的安全性和唯一性,但 Tomcat 作为多 Web 应用容器,需要实现应用间类库隔离(避免不同应用的同名类冲突)和类热部署,因此自定义了一套类加载器体系。本文将详细解析 Tomcat 类加载器的结构、加载机制及与双亲委派的差异。

Tomcat 类加载器的结构

Tomcat 打破了 Java 原生的双亲委派模型,设计了多层次的类加载器,实现不同范围类的隔离与共享。其核心类加载器结构如下(从顶层到应用层):

核心类加载器及职责

类加载器 父加载器 配置参数 职责范围
Common 类加载器 System 类加载器 common.loader 加载 Tomcat 内部和所有 Web 应用共享的类(如 Servlet 规范 API、通用工具类)。
Catalina 类加载器 Common 类加载器 server.loader 加载仅 Tomcat 内部可见的类(对 Web 应用隐藏),默认与 Common 共享。
Shared 类加载器 Common 类加载器 shared.loader 加载所有 Web 应用共享但对 Tomcat 不可见的类,默认与 Common 共享。
Web 应用类加载器 Shared 类加载器 无(自动生成) 加载当前 Web 应用私有类(/WEB-INF/classes/WEB-INF/lib 下的类),仅当前应用可见。
Jasper 类加载器 Web 应用类加载器 无(JSP 专用) 加载 JSP 编译后的类,支持 JSP 热部署(修改后重新编译加载)。

配置文件:catalina.properties

Tomcat 通过 conf/catalina.properties 配置类加载器的资源路径:

1
2
3
4
5
6
7
8
# Common 类加载器的资源路径(Tomcat 安装目录下的 lib 目录)
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

# Catalina 类加载器的资源路径(默认空,即复用 Common 加载器)
server.loader=

# Shared 类加载器的资源路径(默认空,即复用 Common 加载器)
shared.loader=
  • server.loadershared.loader 未配置,Catalina 和 Shared 加载器将直接使用 Common 加载器的资源。
  • 自定义路径时,需指定具体目录或 JAR 包(如 shared.loader=/opt/shared/*.jar)。

Tomcat 类加载机制:打破双亲委派

Java 原生双亲委派模型的加载顺序是先委派父加载器加载,父加载器失败后再由当前加载器加载。而 Tomcat 的 Web 应用类加载器为了实现隔离,优先加载自身资源,再委派父加载器,具体流程如下:

Web 应用类加载器的加载流程(WebappClassLoaderBase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;

// 1. 检查当前加载器的缓存(已加载的类)
clazz = findLoadedClass0(name);
if (clazz != null) return resolveClass(clazz);

// 2. 检查系统类加载器的缓存(防止覆盖 Java 核心类)
clazz = findLoadedClass(name);
if (clazz != null) return resolveClass(clazz);

// 3. 优先加载 Java 核心类(Bootstrap/Ext 加载器)
if (isJavaseClass(name)) {
clazz = getJavaseClassLoader().loadClass(name);
if (clazz != null) return resolveClass(clazz);
}

// 4. 尝试自身加载(/WEB-INF/classes 和 /WEB-INF/lib)
try {
clazz = findClass(name); // 核心:先加载自身资源
if (clazz != null) return resolveClass(clazz);
} catch (ClassNotFoundException e) { /* 忽略,继续下一步 */ }

// 5. 委派父加载器加载(Shared -> Common -> System)
if (parent != null) {
clazz = parent.loadClass(name, false);
if (clazz != null) return resolveClass(clazz);
}

throw new ClassNotFoundException(name);
}
}

与双亲委派的核心差异

  • 加载顺序相反
    原生双亲委派是「父优先」,Tomcat Web 应用加载器是「自身优先」(先加载 WEB-INF 下的类,再委派父加载器)。
  • 隔离性
    每个 Web 应用有独立的类加载器,确保同名类(如 com.example.User)在不同应用中互不干扰。
  • 禁止覆盖核心类
    步骤 3 强制优先加载 Java 核心类(如 java.lang.String),防止 Web 应用恶意替换系统类。

启用原生双亲委派

若需强制 Web 应用使用「父优先」模式,可在应用的 META-INF/context.xml 中配置:

1
2
3
4
<Context>
<!-- delegate="true" 启用双亲委派(默认 false) -->
<Loader delegate="true" />
</Context>

此时加载顺序变为:父加载器 → 自身加载器(与 Java 原生一致)。

类热部署的实现原理

Tomcat 支持 Web 应用类的热部署(修改类后无需重启服务器即可生效),核心依赖类加载器的销毁与重建

  1. 监控资源变化
    Tomcat 启动一个后台线程,定期检查 WEB-INF/classesWEB-INF/lib 下文件的修改时间戳。
  2. 触发重新加载
    当检测到类文件或 JAR 包被修改,Tomcat 会:
    • 销毁当前 Web 应用的类加载器及其加载的所有类。
    • 创建新的 Web 应用类加载器。
    • 通过新加载器重新加载所有类(包括修改后的类)。
  3. JSP 热部署
    Jasper 类加载器专门处理 JSP,每次访问 JSP 时会检查是否修改,若修改则重新编译为 .class 并由新的类加载器加载。

常见问题与解决方案

1. 类冲突(ClassCastException)

现象:不同类加载器加载的同名类被视为不同类型,导致转型失败。
原因

  • 同一类被多个加载器加载(如 common.loader 和 Web 应用的 WEB-INF/lib 都包含 xxx.jar)。
    解决
  • 确保类仅在一个位置存在(推荐放在 WEB-INF/libcommon.loader,而非两者皆有)。

2. Servlet API 冲突

现象:启动时报 ClassNotFoundExceptionNoSuchMethodError(涉及 javax.servlet 类)。
原因:Web 应用的 WEB-INF/lib 包含 servlet-api.jar,与 Tomcat 自带的 API 冲突。
解决

  • 移除应用中的 servlet-api.jar,依赖 Tomcat 提供的 Servlet API(由 Common 加载器加载)。

3. 共享类的加载

需求:多个 Web 应用共享某类库(如 common-util.jar)。
方案

  • 将 JAR 包放入 $CATALINA_BASE/lib(由 Common 加载器加载,所有应用可见)。
  • 配置 shared.loader 指向自定义目录(仅 Web 应用可见,Tomcat 不可见)。

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10