Tomcat-类加载器
以问题切入,深入剖析了commonLoader与WebappClassLoader各自的定位与设计初衷,重点分析了WebappClassLoader如何在保证JVM核心类安全的前提下,部分打破双亲委派机制,实现Web应用之间的真正隔离。
以当前8.5.x版本为例,在具体分析之前,先想一个问题,带着这个问题再去思考为什么这么设计:
当我们把一个 Web 项目部署到 Tomcat 里后,项目里用到的各种 class 文件,Tomcat 到底是从哪些地方去加载它们的?以及为什么要用这些不同的类加载器去加载?
整体上可以简单归类为三部分来源:
- Tomcat 自己的依赖(由commonLoader加载):安装目录里lib目录下所有jar,这些都是 Tomcat 启动和运行时所需要的核心库。
- Web 应用本身的依赖(由WebappClassLoader加载),以context为user举例。
webapps/user/WEB-INF/classes/
:存放应用自身编译后的 class 文件webapps/user/WEB-INF/lib/
:存放应用所依赖的第三方 jar 包
- JDK依赖(由 BootstrapClassLoader 加载):比如
rt.jar
(JDK 8 及以下)或java.base
(JDK 9 及以后),这些属于 JVM 层面的标准库,
Web 应用在运行时所依赖的 class,基本都来自于上述几个目录。而 Tomcat 为了支持不同context之间的隔离、避免类冲突、支持热部署等需求,专门设计了一套层级化的类加载器体系,来负责加载这些不同来源的 class。
commonLoader
conf/catalina.properties相关配置
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
初始化代码
调用路径为:Bootstrap#main -> Bootstrap#start -> Bootstrap#init -> Bootstrap#initClassLoaders
默认的conf/catalina.properties配置里server.loader和shared.loader都为空,所以catalinaLoader和sharedLoader都被赋值为了commonLoader
/**
* 初始化三层类加载器:common、server(catalina)、shared
*/
private void initClassLoaders() {
try {
// commonLoader:公共类加载器,供 Tomcat 内部和所有 Web 应用共享
// 默认搜索目录:
// ${catalina.base}/lib/
// ${catalina.base}/lib/*.jar
// ${catalina.home}/lib/
// ${catalina.home}/lib/*.jar
//
// 说明:
// - ${catalina.base}:运行时指定的工作目录,可以实现多实例共享一个 Tomcat 安装
// - ${catalina.home}:Tomcat 的安装目录
//
// 如果未在 catalina.properties 里配置 common.loader,则默认使用当前类的 ClassLoader
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
commonLoader = this.getClass().getClassLoader();
}
// 默认为上面的commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
// 默认为上面的commonLoader
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 从 catalina.properties 读取对应的 loader 配置
String value = CatalinaProperties.getProperty(name + ".loader");
// value为空,则使用parent
if ((value == null) || (value.equals(""))) {
return parent;
}
// value解析和占位符替换
value = replace(value);
List<Repository> repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
// 根据class所在的位置,创建一个URLClassLoader
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
classLoader引用设置
- 在Bootstrap#init()方法里,会将catalinaLoader绑定到当前线程的contextClassLoader里,同时,通过反射将sharedLoader设置到Catalina.parentClassLoader字段里
public void init() throws Exception {
initClassLoaders();
// 用catalinaLoader作为当前线程上下文的contextClassLoader,用于加载tomcat class
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// 使用反射将sharedLoader设置到Catalina.parentClassLoader字段
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
}
在 Catalina 中创建用于解析
server.xml
的 Digester 时,涉及到类加载器的一些关键配置。这里仅整理与类加载器相关的部分逻辑设置Digester.useContextClassLoader=true
这一配置让 Digester 在解析
server.xml
并实例化各个 Tomcat 组件时,优先使用当前线程的contextClassLoader
(即catalinaLoader
,本质上就是commonLoader
)来加载类。这样可以确保 Tomcat 核心组件统一通过 common 层加载设置 Engine 的
parentClassLoader
在实例化
Engine
时,会将其内部的ContainerBase.parentClassLoader
设置为sharedLoader
(其实也是commonLoader
)。这一步非常关键,因为后续每个WebappClassLoader
的父加载器就是从这里继承而来
protected Digester createStartDigester() {
long t1=System.currentTimeMillis();
// Initialize the digester
Digester digester = new Digester();
// 使用当前Thead的contextClassLoader加载指定的class
digester.setUseContextClassLoader(true);
// 将sharedLoader设置到Engine里的parentClassLoader字段
digester.addRule("Server/Service/Engine",
new SetParentClassLoaderRule(parentClassLoader));
return digester;
}
关键要点
通过上诉的配置解析和设置,可总结出如下关键要点,对后续的WebappClassLoader分析有很大作用:
- sharedLoader 和 catalinaLoader 基本都被废弃了,最终它们都被统一设置为了
commonLoader
,即默认只使用 common 层的类加载器。 - Tomcat 主线程的
contextClassLoader
被设置为了catalinaLoader
(也就是commonLoader
),主要用于加载 Tomcat 自身运行所需的类(即lib
目录下的 jar 或 class 文件)。 StandardEngine
的parentClassLoader
被设置为了sharedLoader
(实际上还是commonLoader
),用于作为后续每个WebappClassLoader
(即 web 应用类加载器)的父加载器
WebappClassLoader
通过之前分析的文章,StandardContext#start会触发WebappLoader#start,需要注意的是,WebappLoader
本身并不是一个类加载器,而是通过实现 Loader
和 Lifecycle
接口,负责在各个生命周期阶段对真正的 WebappClassLoader
进行管理和控制。其创建真正的WebappClassLoader在WebappLoader#start阶段,核心如下
protected void startInternal() throws LifecycleException {
try {
// 创建真正的 Web 应用类加载器 WebappClassLoader
classLoader = createClassLoader();
classLoader.setResources(context.getResources());
// 设置类加载委托模型,决定优先使用父加载器还是自身加载器(默认为false,即不优先使用父加载器加载class)
classLoader.setDelegate(this.delegate);
setClassPath();
setPermissions();
// 启动 WebappClassLoader(缓存/WEB-INF/classes和/WEB-INF/lib下的资源)
((Lifecycle) classLoader).start();
} catch (Throwable t) {
// ...省略
}
setState(LifecycleState.STARTING);
}
private WebappClassLoaderBase createClassLoader()
throws Exception {
// loader为org.apache.catalina.loader.ParallelWebappClassLoader
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
// 内部会递归向上查找,也就是最终会使用Engine内部的parentClassLoader,即sharedLoader
parentClassLoader = context.getParentClassLoader();
} else {
context.setParentClassLoader(parentClassLoader);
}
// 使用反射将sharedLoader作为构造参数,实例化出ParallelWebappClassLoader加载器
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
ParallelWebappClassLoader
start
相关字段和方法如下,主要是将当前项目里的/WEB-INF/classes和/WEB-INF/lib目录下的class资源缓存到localRepositories(可以理解为classpath),以供后续来实际加载class
// 当前 Web 应用的本地 classpath 资源(/WEB-INF/classes 以及 /WEB-INF/lib 下的所有 jar)
private List<URL> localRepositories = new ArrayList<>();
/**
* 生命周期的start方法,缓存class目录资源
*/
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
// 1. 扫描 /WEB-INF/classes 目录,将其加入本地 classpath
WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
}
// 2. 扫描 /WEB-INF/lib 目录下所有可读的 jar 包,将其加入本地 classpath,同时记录最后修改时间
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
loadClass
核心字段和方法如下(已删除不重要的部分),通过如下源码可总结出loadClass的步骤:
- 缓存检查
- 先检查当前 WebappClassLoader 是否已加载过指定的类,如果已加载,直接返回已加载的类,避免重复加载
- 检查是否应尝试使用 JVM 内部类加载器(类似传统的类加载器加载方式)
- 若JVM内部类加载器能找到指定class资源,则由其加载指定的class
- 根据委派规则(delegateLoad)决定加载顺序,delegateLoad一般都为false(若为true则颠倒顺序)
- 先本地加载(当前 Web 应用内),尝试从/WEB-INF/classes、/WEB-INF/lib文件夹下加载指定class
- 否则交由父加载器(sharedLoader)兜底,即从tomcat的lib目录里的jar加载指定class
- 以上都没找到,则抛出ClassNotFoundException
public abstract class WebappClassLoaderBase extends URLClassLoader
implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
// 绑定的 Web 资源接口,实际提供 /WEB-INF/classes、/WEB-INF/lib 等资源访问能力
protected WebResourceRoot resources = null;
// 当前加载过的class的缓存(/WEB-INF/classes目录下)
protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();
// 是否强制委派给父类加载器优先加载。默认为false,表示优先当前类加载器先加载类
protected boolean delegate = false;
// /WEB-INF/lib 目录下jar包的修改时间(热加载使用)
private final HashMap<String, Long> jarModificationTimes = new HashMap<>();
// tomcat的sharedLoader(URLClassLoader)
protected final ClassLoader parent;
// 标识是否有外部 repository 被注册(即 localRepositories 是否有值)
private boolean hasExternalRepositories = false;
// 当前 Web 应用的本地 classpath 资源(/WEB-INF/classes 以及 /WEB-INF/lib 下的所有 jar)
private List<URL> localRepositories = new ArrayList<>();
/**
* 把 webapp 的所有 classpath 资源(包括 /WEB-INF/classes 和 /WEB-INF/lib/*.jar)整理成 URL
* 数组,
* 供自己这个URLClassLoader实现去加载class。hasExternalRepositories也会被设置为true
*/
@Override
public URL[] getURLs() {
ArrayList<URL> result = new ArrayList<>();
result.addAll(localRepositories);
result.addAll(Arrays.asList(super.getURLs()));
return result.toArray(new URL[0]);
}
/**
* 父类URLClassLoader的方法,配合getURLs()方法,设置hasExternalRepositories为true
*/
@Override
protected void addURL(URL url) {
super.addURL(url);
hasExternalRepositories = true;
}
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 先检查 Web 应用是否已停止,防止在已停止的 webapp 中加载类
checkStateForClassLoading(name);
// 1、从缓存中resourceEntries查找当前类加载器是否已经加载了这个class
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// 2、从当前类加载器的native中查找是否已经缓存过(范围比findLoadedClass0更大)
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// class名转换为路径。比如:site.shanzhao.Demo => /site/shanzhao/Demo.class
String resourceName = binaryNameToPath(name, false);
// 检查jvm的类加载器是否加载过当前class
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url = javaseLoader.getResource(resourceName);
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
tryLoadingFromJavaseLoader = true;
}
// 3. jvm类加载器加载过,直接从jvm类加载器获取这个Class
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 强制委派父加载器加载 or 一些特定核心类 :都先走父类加载器进行加载
// 一般情况都应为false
boolean delegateLoad = delegate || filter(name, true);
// 4. 设置了delegate为true,则尝试使用父类加载器(默认即tomcat的common类加载器加载)
if (delegateLoad) {
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// ================== 走到这,才开始尝试使用本类加载加载指定的class
try {
// 5. 开始使用当前类加载器加载(即 /WEB-INF/classes 与 /WEB-INF/lib)
clazz = findClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
if (!delegateLoad) {
try {
// 6. delegateLoad为false,再使用父类加载器(为sharedLoader,默认也是commonLoader)尝试加载指定的class
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
/**
* 从当前项目的/WEB-INF/classes 与 /WEB-INF/lib中加载指定class
*/
public Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
try {
try {
// 1. 从/WEB-INF/classes文件夹下加载class(如果找到,还会缓存到resourceEntries字段里)
clazz = findClassInternal(name);
} catch (Exception e) {
// ... 异常处理
}
if ((clazz == null) && hasExternalRepositories) {
try {
// 2. 利用父类URLClassLoader的功能,加载/WEB-INF/lib文件夹下jar里的class
clazz = super.findClass(name);
} catch (Exception e) {
// ... 异常处理
}
}
// =========== 到这说明/WEB-INF/classes 和/WEB-INF/lib 里都没找到指定的class,直接抛异常 =========
if (clazz == null) {
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
throw e;
}
return clazz;
}
}
总结和思考
commonLoader存在的意义?
Tomcat 里其实只有一个 JVM,但可以部署多个 web 应用(多个 context)。这些应用共用 Tomcat 核心组件,比如:Server、Connector、Valve、Pipeline 等。这些 Tomcat 核心组件的 class 没必要每个应用都各自加载一份,只需 JVM 里统一加载一次就行。
commonLoader 就起这个作用,它其实就是一个 URLClassLoader,负责加载
$CATALINA_HOME/lib
目录下的 jar,把 Tomcat 自己的核心代码加载进来,供所有 Web 应用共享。WebappClassLoader的意义?
这里就是隔离问题的核心:全限定类名是类加载器判定是否已加载的唯一依据。
假设有多个 web 应用,它们各自依赖了同一个类
site.shanzhao.Demo
,但版本不同。如果 JVM 里只有一份类加载器,那只能加载一份,版本冲突就没法解决了。 所以 Tomcat 设计了 WebappClassLoader:每个 context 都有自己的类加载器,各自加载自己的
/WEB-INF/classes
和/WEB-INF/lib
目录,互不影响。这样同一个类名的 class 文件可以在不同应用里加载出不同版本,实现真正的隔离。 另外,有独立的 WebappClassLoader 也方便支持应用的热部署和卸载:当某个 context 重新加载或被移除时,直接销毁这个 WebappClassLoader,连带其加载的所有 class、资源、线程都能一起回收,减少内存泄露风险。
为什么不使用jdk的
AppClassLoader
替换commonLoader
?从表面看,AppClassLoader
和 Tomcat 自定义的commonLoader
都继承自URLClassLoader
,两者在能力上类似,都可以加载来自指定classpath
的类或资源。但 Tomcat 并没有使用AppClassLoader
来加载自身核心组件,为什么呢?- AppClassLoader 的 classpath 是在 JVM 启动阶段静态确定的(通过系统属性:java.class.path),而Tomcat 的类加载器的classpath支持占位符(如${catalina.base}和${catalina.home}),即允许用户扩展 classpath,更加灵活
- Tomcat 在早期设计中区分了commonLoader,catalinaLoader和sharedLoader这三个类加载器用以实现模块隔离,AppClassLoader无法实现
- WebappClassLoader 更不可能使用 AppClassLoader。单是启动时无法确定需要部署哪些Context以及各自classpath这条理由,就不可能用AppClassLoader,更别提Tomcat还支持了Web环境隔离和热加载这种高级特性了
哪个类加载器打破了双亲委派机制?
其实只有 WebappClassLoader 部分打破了双亲委派。
commonLoader、sharedLoader 本身都是标准的 URLClassLoader,没有动过 loadClass 逻辑,完全遵守双亲委派。
WebappClassLoader 才重写了 loadClass,实现了一个 可控双亲委派:
- 没完全打破:JVM 自带的类(JDK 标准库等)依然优先交给 JVM 自己的类加载器加载,保证不会被项目里的同名 class 覆盖,比如你项目里搞个自定义
java.lang.String
,Tomcat 也不会用你这份 - 部分打破:对于非 JVM 内部类,WebappClassLoader 可以根据 delegate 参数决定是否优先本地加载,而不强行一律交给父加载器sharedLoader
- 没完全打破:JVM 自带的类(JDK 标准库等)依然优先交给 JVM 自己的类加载器加载,保证不会被项目里的同名 class 覆盖,比如你项目里搞个自定义