Tomcat源码分析——类加载机制

引:相信大家对于Java类加载的认识最开始都是来自于《深入理解Java虚拟机》,可能觉得就一个双亲委派机制嘛,没什么东西!但是具体到实际应用,对于类加载器却有所不同,今天我们就跟着Tomcat源码来分析一下它的类加载机制!

前言

这个说到Tomcat源码,在看源码之前,我们都需要先理解它的架构与一些设计原理,如果图快,可以从最后的参考中快速消化一下Tomcat的架构,但是如果是想系统看一下看Tomcat的全貌,我建议可以好好去看看《深入剖析Tomcat》,虽然他对应的版本对于现在来说有点老了,但是核心思想是不变的,相信很多人看完之后会和我一样有一种恍然大悟的感觉。关于怎么看Tomcat源码也可以在最后的参考中找到相关链接。

Java类加载机制

在说Tomcat的类加载机制之前,我们一定要先看看Java自己的类加载机制,之前自己对《深入理解Java虚拟机》中描述的Java类加载机制一篇总结文章,可以先看看:深入理解JVM9类加载器

Tomcat类加载机制

Tomcat使用类加载器的原因有3条:(待会会在源码中看出)

  1. 为了在载入类指定某些规则
  2. 为了缓存已经载入的类
  3. 为了实现类的预加载,方便使用

设计图

Tomcat类加载机制设计

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
  • JasperLoader: 每一个JSP文件对应一个Jsp类加载器,实现热加载;

源码分析

Tomcat启动

如果看过Tomcat的架构和设计,就应该知道Tomcat的启动时运行main方法作为入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String args[]) {
// daemon就是bootstrap,当初始化完成之后会赋值给daemon
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
try {
// 初始化
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// 设置线程上下文类加载器继续加载,保持一致
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
// 省略...
}

我们需要知道daemon初始化干了什么:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
  public void init() throws Exception {
// Tomcat 自己的类加载器结构
initClassLoaders();
// 将 catalinaLoader 类加载器设置为当前线程上下文类加载器.
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 并设置线程安全类加载器进行类加载
SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 加载启动对象
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();

// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
// 使用反射调用该实例的setParentClassLoader 方法, 参数为 sharedLoader
// 表示该实例的父类加载器为 sharedLoader.
// 这里暗含了后面生成的WebappClassLoader的parent就是sharedLoader
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);
// 设置 catalinaDaemon 为该实例
catalinaDaemon = startupInstance;

}

// 关键来了
private void initClassLoaders() {
try {
// tomcat公用的类加载器 parent为null 违背了java自己的类加载机制
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
// 找不到配置文件中的 key 的时候或者 key 对应的 value 为空的时候回返回 null
// 如果返回 null, 那么就设置默认的类加载器为 common 类加载器.
commonLoader=this.getClass().getClassLoader();
}
// 调试可知:
// 可以找不到配置文件中的 key, 所以他们直接返回父类加载器, 也就是说(默认情况下), 他们三个使用的是同一个类加载器.
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}

// 创建commonLoader
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 拿到资源(要加载类的路径)
// common.loader 对应的 Value=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
String value = CatalinaProperties.getProperty(name + ".loader");
// 如果不存在, 返回 null
if ((value == null) || (value.equals("")))
return parent;
// 使用环境变量对应的目录替换字符串
value = replace(value);
// Repository是ClassLoaderFactory 中的一个静态内部类
// 有2个属性, location, type, 表示某个位置的某种类型的文件
List<Repository> repositories = new ArrayList<>();
// 将路径变成一个数组
String[] repositoryPaths = getPaths(value);

for (String repository : repositoryPaths) {
// Check for a JAR URL repository
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);
}

初始化完三个类加载之后,上面就会让catalinaLoader加载Tomcat所需要的类:

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 static void securityClassLoad(ClassLoader loader) throws Exception {
securityClassLoad(loader, true);
}

static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception {

if (requireSecurityManager && System.getSecurityManager() == null) {
return;
}
// securityClassLoad方法主要加载Tomcat容器所需的class
// Tomcat核心class,即org.apache.catalina.core路径下的class
loadCorePackage(loader);
// Tomcat连接器,即org.apache.coyote路径下的class
loadCoyotePackage(loader);
// Tomcat应用类加载器, org.apache.catalina.loader.WebappClassLoader
loadLoaderPackage(loader);
// org.apache.catalina.realm下的class
loadRealmPackage(loader);
// org.apache.catalina.servlets下的class
loadServletsPackage(loader);
// org.apache.catalina.session下的class;
loadSessionPackage(loader);
// org.apache.catalina.util下的class
loadUtilPackage(loader);
// org.apache.catalina.valves下的class
loadValvesPackage(loader);
// javax.servlet.http.Cookie
loadJavaxPackage(loader);
// org.apache.catalina.connector的class
loadConnectorPackage(loader);
// org.apache.catalina.util路径下的class
loadTomcatPackage(loader);
}

到这里daemon就被创建完成了,启动也就结束了!

WebappClassLoader创建

上面说了Tomcat类加载机制中的三大加载器,但是对于Tomcat来说WebappClassLoader或许是最关键的,因为应用里的类都靠它加载。看过Tomcat架构的都知道WebappClassLoader是和容器StandardContext绑定在一起的。所以我们跟StandardContext生命周期的启动方法startInternal进去看看(该方法比较长,省略很多内容,只看关键):

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
protected synchronized void startInternal() throws LifecycleException {

if (getLoader() == null) {
// 生成一个WebappLoader,parent为sharedClassLoader一直传递下去
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
// 绑定 StandardContext容器
setLoader(webappLoader);
}

try {
if (ok) {
// Start our subordinate components, if any
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
// 这里执行生命的周期的start方法,最后会回调到WebappLoader的startInternal方法
((Lifecycle) loader).start();
}
}

//....
}

// org.apache.catalina.loader.WebappLoader
protected void startInternal() throws LifecycleException {
try {
// 创建WebAppClassLoader,跟进去
classLoader = createClassLoader();
// 设置其资源路径为当前Webapp下某个context的类资源
classLoader.setResources(context.getResources());
classLoader.setDelegate(this.delegate);
// 生命周期钩子
((Lifecycle) classLoader).start();
// ...
}
}
private WebappClassLoaderBase createClassLoader()
throws Exception {
// 通过反射实例化classLoader
// 这里是ParallelWebappClassLoader
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
// 这里父类加载器是Catalina实例中的sharedClassLoader
if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);

return classLoader;
}

到这里,WebappClassLoader就被实例化完成了。

WebappClassLoader类加载

WebappClassLoader就被实例化完成了,接下来我们就需要看看它是如何加载类的?找到他的loaderClass方法,ParallelWebappClassLoaderloadClass是在其父类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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

synchronized (getClassLoadingLock(name)) {

Class<?> clazz = null;

// (0) Check our previously loaded local class cache
// 检查WebappClassLoader中是否加载过此类
// 类中维护了一个resourceEntries的ConcurrentHashMap
clazz = 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
// 检查JVM虚拟机中是否加载过该类
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
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
// 用应用类加载器加载该类(也就是当前JVM的ClassPath),为了防止覆盖基础类实现
String resourceName = binaryNameToPath(name, false);

ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;

if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}

// 先判断是否设置了delegate属性,设置为true,那么就会完全按照JVM的"双亲委托"机制流程加载类
boolean delegateLoad = delegate || filter(name, true);

// (1) Delegate to our parent if requested
// 双亲委派模式,从上到下加载
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}

// (2) Search local repositories
// 若是默认的话,是先使用WebappClassLoader自己处理加载类的
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
// 通过自定义findClass定义处理类加载规则
clazz = findClass(name);
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
// 若是WebappClassLoader在/WEB-INF/classes、/WEB-INF/lib下还是查找不到class
// 那么委托给Common类加载器去查找该类 ,这里满足双亲委派原则
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
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);
}

这里总结一下delegate属性为false,即默认的情况下的加载顺序:

  1. 检查本地缓存
  2. 如果没有,检查虚拟机缓存
  3. 如果没有,从AppClassLoader加载,这里会使用Java自己的类加载体系
  4. 如果没有,则从WebappClassLoader加载(按照WEB-INF/classes、WEB-INF/lib的顺序)
  5. 如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序是Common、Shared

总结

这里用一到面试题结束:Tomcat的类加载机制是否违反了双亲委托原则?

答案:是,具体怎么破坏了看本文理解。

参考

  1. 《深入剖析Tomcat》

  2. 四张图带你了解Tomcat系统架构

  3. 怎么读 Tomcat 源码?
  4. 深入理解JVM9类加载器
  5. 图解Tomcat类加载机制(阿里面试题)
  6. Tomcat源码分析 – Tomcat类加载器
  7. 真正理解线程上下文类加载器(多案例分析)