Poison

Tomcat 的类加载器层次

关于 Tomcat 的类加载器层次,我之前也只是看过文档中的介绍,并未深入理解,直到一次问题排查,对其有了更深入的理解,本文作简要记录。

在一次处理 java.sql.DriverManager 相关的问题时,看到 DriverManager 文档中有以下描述:

Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.

于是果断把一个数据源工具类中的 Class.forName() 方法调用进行了移除。原实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.tianshuang.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DataSourceUtil {

public static Connection getConnection(String url, String driverClassName, String username, String password) {
try {
Class.forName(driverClassName);
return DriverManager.getConnection(url, username, password);
} catch (SQLException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

}

移除对 Class.forName() 的显式调用后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package me.tianshuang.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DataSourceUtil {

public static Connection getConnection(String url, String driverClassName, String username, String password) {
try {
return DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

}

然后在本地跑了个单元测试,验证了该工具类没有问题,随即进行发布,发现 未接入过数据源 的 web 应用使用该工具类获取连接时抛出如下异常:

1
java.sql.SQLException: No suitable driver found for jdbc:mysql://...

让我感到困惑的是,这明显和文档说的不一致,同样的代码,明明在本地通过单元测试的,在 Tomcat 容器中使用就不行了。于是经过查询文档及 DEBUG,发现与 Tomcat 的类加载器层次有关。

首先我们看看 Servlet 规范 Servlet Specification 2.4 中关于 web 应用类加载器的描述,位于 SRV.9.7.2 Web Application Class Loader:

The class loader that a container uses to load a servlet in a WAR must allow the developer to load any resources contained in library JARs within the WAR following normal J2SE semantics using getResource. As described in the J2EE license agreement, servlet containers that are not part of a J2EE product should not allow the application to override J2SE platform classes, such as those in the java.* and javax.* namespaces, that J2SE does not allow to be modified. Also, servlet containers that are part of a J2EE product should not allow the application to override J2SE or J2EE platform classes, such as those in java.* and javax.* namespaces, that either J2SE or J2EE do not allow to be modified. The container should not allow applications to override or access the container’s implementation classes. It is recommended also that the application class loader be implemented so that classes and resources packaged within the WAR are loaded in preference to classes and resources residing in container-wide library JARs.

规范提到不允许应用覆盖 J2SE 及 J2EE 平台的类,比如 java.*javax.* 命名空间中的类,不允许应用覆盖或访问容器实现的类,推荐将 web 应用类加载器实现为加载 WAR 包中的类优先于容器范围的 JAR。

通过查阅 Apache Tomcat 8 (8.5.72) - Class Loader How-To 中关于类加载器的描述,我们知道,在 Java 环境中,类加载器以父子树形的模式被组合。当一个类加载器被要求加载一个类或者资源时,它将把这个加载请求委托给父类加载器,然后仅在父类加载器找不到请求的类或资源时才自行加载。需要注意的是,web 应用类加载器的模型与此稍有不同,但是主要原理是相同的。

当 Tomcat 启动时,它会创建一组类加载器,这些类加载器组织成以下父子关系,在下图中,父类加载器位于子类加载器上方,层次结构如图所示:

1
2
3
4
5
6
7
    Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ...

上面的说法有一点偏差,准确地说,Bootstrap ClassLoader 是由 JVM 创建,System ClassLoader 是 sun.misc.Launcher 加载时由静态字段实例化创建,根据 Tomcat 文档中对 Bootstrap ClassLoader 的描述,其中提到:

Bootstrap — This class loader contains the basic runtime classes provided by the Java Virtual Machine, plus any classes from JAR files present in the System Extensions directory ($JAVA_HOME/jre/lib/ext). Note: some JVMs may implement this as more than one class loader, or it may not be visible (as a class loader) at all.

对于 Oracle JVM,Tomcat 文档中的 Bootstrap ClassLoader 其实对应的 JVM 中的 Bootstrap ClassLoader 与 Ext ClassLoader,我们以上的图补充为:

1
2
3
4
5
6
7
8
9
    Bootstrap
|
Ext
|
System
|
Common
/ \
Webapp1 Webapp2 ...

对于 System ClassLoader,通常使用 CLASSPATH 环境变量的值作为搜索路径,而 Tomcat 的启动脚本 $CATALINA_HOME/bin/catalina.sh 完全忽略了原 CLASSPATH 环境变量的值,重新指定了 CLASSPATH 环境变量的值,具体实现可以参考:catalina.sh。比如在我的电脑上,使用默认的 Tomcat 配置,通过脚本启动 Tomcat,在 Tomcat 容器运行环境下,使用 sun.misc.Launcher.AppClassLoader#getAppClassLoader 初始化系统类加载器时,调用 System.getProperty("java.class.path") 获取到的值为:

1
/opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar

扩展类加载器与系统类加载器的创建逻辑可以参见:Launcher.java

剩下的 Common ClassLoader 与 Webapp ClassLoader 才是由 Tomcat 代码进行的创建,其中 Common ClassLoader 创建部分的源码位于 org.apache.catalina.startup.Bootstrap#initClassLoaders

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
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", 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 {

String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals(""))) {
return parent;
}

// omitted

return ClassLoaderFactory.createClassLoader(repositories, parent);
}

ClassLoaderFactory.createClassLoader(repositories, parent) 的源码位于 ClassLoaderFactory.java

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
/**
* Create and return a new class loader, based on the configuration
* defaults and the specified directory paths:
*
* @param repositories List of class directories, jar files, jar directories
* or URLS that should be added to the repositories of
* the class loader.
* @param parent Parent class loader for the new class loader, or
* <code>null</code> for the system class loader.
* @return the new class loader
*
* @exception Exception if an error occurs constructing the class loader
*/
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {

// omitted

return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null) {
return new URLClassLoader(array);
} else {
return new URLClassLoader(array, parent);
}
}
});
}

创建 Common ClassLoader 时,传入 createClassLoaderparent 参数为 null,我们跟随调用链,先看 return new URLClassLoader(array);

1
2
3
4
5
6
7
8
9
10
public URLClassLoader(URL[] urls) {
super();
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}

跟随 super()java.security.SecureClassLoader#SecureClassLoader()

1
2
3
4
5
6
7
8
9
protected SecureClassLoader() {
super();
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
initialized = true;
}

再跟随 super()java.lang.ClassLoader#ClassLoader()

1
2
3
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

可以看出,Common ClassLoader 实际为实例化了一个 URLClassLoader,其 parent 为系统类加载器,与我们上面描述的层次关系相符合。

根据上面的代码,完成对 commonLoader 的创建后,会创建名为 servershared 的类加载器,这两个类加载器是根据 catalina.properties 配置文件中的 server.loadershared.loader 的配置决定是否创建,默认配置可以参考:catalina.properties,可以看出默认配置中这两个选项的值为空,故默认配置下不会进行创建,直接引用的 commonLoader, 所以 org.apache.catalina.startup.Bootstrap#initClassLoaders 方法执行后,commonLoadercatalinaLoadersharedLoader 均指向的 commonLoader。随后将这个 commonLoader 设置为了当前线程的上下文类加载器。代码位于 org.apache.catalina.startup.Bootstrap#init()

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
/**
* Initialize daemon.
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {

initClassLoaders();

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");
}
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 = startupInstance;
}

自此,我们明确了 commonLoader 的创建过程,但是这个和开始提到的 java.sql.DriverManager 的问题有什么关系呢,此处不得不引出另一篇文档:Apache Tomcat 8 (8.5.72) - JNDI Datasource How-To - DriverManager, the service provider mechanism and memory leaks,根据该文档的描述,java.sql.DriverManager 是支持 Java SPI 服务提供者机制的,该特性能够自动发现、加载和注册通过 META-INF/services/java.sql.Driver 声明的 JDBC 驱动,以使你无需在获取 JDBC 连接前显式加载数据库驱动程序。但是,该实现在所有 Java 版本的 Servlet 容器环境中都被破坏了。原因是 java.sql.DriverManager 只会扫描一次驱动。

以上文档描述是什么意思呢?首先查看 JDK 源码,我们可以知道 java.sql.DriverManager 位于 <JAVA_HOME>/jre/lib/rt.jar,该目录被 Bootstrap ClassLoader 所加载,且该类只会被加载一次,我们跟随 java.sql.DriverManager 中 SPI 部分的源码来明确相关逻辑,在 java.sql.DriverManager 被加载时,会触发其静态代码块:

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
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

在以上代码中,最为关键的 SPI 部分的方法调用为 ServiceLoader.load(Driver.class);,该调用将生成一个懒迭代器,我们简要看下相关实现:

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
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

最为关键的部分即为以上代码,其中,从当前线程获取了上线文类加载器用于后续服务实现的加载。那么,我们知道触发 java.sql.DriverManager 时当前线程的上下文类加载器会被用于服务实现的加载,在 Tomcat 启动过程中会去触发 java.sql.DriverManager 的加载吗?根据文档可知,JreMemoryLeakPreventionListener 会触发 java.sql.DriverManager 的加载,其中源码位于 JreMemoryLeakPreventionListener.java

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
/**
* The first access to {@link DriverManager} will trigger the loading of
* all {@link java.sql.Driver}s in the the current class loader. The web
* application level memory leak protection can take care of this in most
* cases but triggering the loading here has fewer side-effects.
*/
private boolean driverManagerProtection = true;

@Override
public void lifecycleEvent(LifecycleEvent event) {
// Initialise these classes when Tomcat starts
if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {

/*
* First call to this loads all drivers visible to the current class
* loader and its parents.
*
* Note: This is called before the context class loader is changed
* because we want any drivers located in CATALINA_HOME/lib
* and/or CATALINA_HOME/lib to be visible to DriverManager.
* Users wishing to avoid having JDBC drivers loaded by this
* class loader should add the JDBC driver(s) to the class
* path so they are loaded by the system class loader.
*/
if (driverManagerProtection) {
DriverManager.getDrivers();
}

// omitted
}
}

注释对该部分代码也进行了解释,触发 java.sql.DriverManager 加载的线程的上下文类加载器就是我们之前提到的 commonLoader,在根据服务接口查询服务的实现时,是调用的 java.util.ServiceLoader.LazyIterator#hasNextService 去进行服务实现类配置文件的查询,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}

所以,根据以上代码中的注释及 catalina.properties 中对 commmon.loader 的默认配置,我们知道 commonLoader 默认扫描如下路径:

  • ${catalina.base}/lib
  • ${catalina.base}/lib/*.jar
  • ${catalina.home}/lib
  • ${catalina.home}/lib/*.jar

回到之前的举例,在 web 应用中,我们的 MySQL 驱动程序的 jar 被打入了 WAR 包,不存在于 commonLoader 可以扫描的路径中,也不存在于父类加载器层次上可以扫描的路径中,所以在 web 应用启动后,通过 JreMemoryLeakPreventionListener 触发 java.sql.DriverManager 加载后注册的驱动只包含以上目录中的服务实现,并不包含 /WEB-INF/lib 中的驱动程序,所以在我们未显式调用 Class.forName("com.mysql.jdbc.Driver") 就进行连接的获取就触发了找不到驱动的异常。

那么,如果我们显式调用了 Class.forName("com.mysql.jdbc.Driver") 又是如何解决驱动的加载的呢,我们以 MySQL 驱动为例,在 web 应用中我们使用 Class.forName("com.mysql.jdbc.Driver") 加载 MySQL 驱动,触发的源码为:

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
/**
* Returns the {@code Class} object associated with the class or
* interface with the given string name. Invoking this method is
* equivalent to:
*
* <blockquote>
* {@code Class.forName(className, true, currentLoader)}
* </blockquote>
*
* where {@code currentLoader} denotes the defining class loader of
* the current class.
*
* <p> For example, the following code fragment returns the
* runtime {@code Class} descriptor for the class named
* {@code java.lang.Thread}:
*
* <blockquote>
* {@code Class t = Class.forName("java.lang.Thread")}
* </blockquote>
* <p>
* A call to {@code forName("X")} causes the class named
* {@code X} to be initialized.
*
* @param className the fully qualified name of the desired class.
* @return the {@code Class} object for the class with the
* specified name.
* @exception LinkageError if the linkage fails
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails
* @exception ClassNotFoundException if the class cannot be located
*/
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

// Returns the class's class loader, or null if none.
static ClassLoader getClassLoader(Class<?> caller) {
// This can be null if the VM is requesting it
if (caller == null) {
return null;
}
// Circumvent security check since this is package-private
return caller.getClassLoader0();
}

易知 caller 为 web 应用中调用 Class.forName() 的类,该类是由什么类加载器加载的呢?此时就引入了 Webapp ClassLoader,我们查看 Tomcat 部署 webapps 目录下应用的源码 HostConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Deploy applications for any directories or WAR files that are found
* in our "application root" directory.
*/
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}

跟随以上代码的调用链,可以看出会为每个部署的应用创建一个 Webapp ClassLoader,源码位于 WebappLoader.java

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
/**
* The Java class name of the ClassLoader implementation to be used.
* This class should extend WebappClassLoaderBase, otherwise, a different
* loader implementation must be used.
*/
private String loaderClass = ParallelWebappClassLoader.class.getName();

/**
* Create associated classLoader.
*/
private WebappClassLoaderBase createClassLoader()
throws Exception {

Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;

if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
} else {
context.setParentClassLoader(parentClassLoader);
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);

return classLoader;
}

其中的 parentClassLoader 为我们之前的 commonLoader,通过反射实例化一个 ParallelWebappClassLoader,自此,就与 Tomcat 官方文档中 ClassLoader 的层次图完全一致了,继续跟随源码的话,可以看出会为该 Webapp ClassLoader 设置资源、是否委托加载、类路径等属性,然后在 StandardContext.java 将创建的 Webapp ClassLoader 绑定到当前线程,此处不再粘贴代码。

到此我们知道自行编写的代码的类由 Webapp ClassLoader 加载,该类加载器能够扫描对应应用下以下路径的类及资源:

  • /WEB-INF/classes
  • /WEB-INF/lib

因为我们的 MySQL 驱动程序的 jar 包位于我们 web 应用的 /WEB-INF/lib 目录下,所以,当我们在 web 应用中显式调用 Class.forName("com.mysql.jdbc.Driver") 时,当前的上下文类加载器为 Webapp ClassLoader,所以 MySQL 驱动程序能被正确加载,然后触发后续的驱动注册逻辑 Driver.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

由以上代码可知,在加载 com.mysql.jdbc.Driver 类时,执行了静态代码块,将该驱动实例化并进行了注册,从而保证了后续获取连接的正常运行。

根据以上的分析,我们知道,在默认配置下,Tomcat 触发 java.sql.DriverManager 类加载时使用的 commonLoader,仅会注册该类加载器及父级层次可见范围中的驱动程序,打包在 web 应用中的驱动程序默认不会被自动注册。因此,在其 WEB-INF/lib 目录中具有数据库驱动程序的 web 应用程序不能依赖于 SPI 机制,而应显式注册驱动程序。

最后,我们再看看 Webapp ClassLoader 加载类的顺序,根据 Tomcat 的文档,从 web 应用的角度来看,在默认配置下,类或资源按照以下顺序进行搜索:

  • Bootstrap classes of your JVM
  • /WEB-INF/classes of your web application
  • /WEB-INF/lib/*.jar of your web application
  • System class loader classes
  • Common class loader classes

排在首位的是 JRE 的一些基础类,如 java.javax. 开头的类,因为这些类不应该被覆盖,随后才是 web 应用自身的类及依赖的 jar 中的类,然后才是 System ClassLoader 和 Common ClassLoader 中的类,我们结合代码看看,其中 Webapp ClassLoader 加载类的方法位于:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* Load the class with the specified name, searching using the following
* algorithm until it finds and returns the class. If the class cannot
* be found, returns <code>ClassNotFoundException</code>.
* <ul>
* <li>Call <code>findLoadedClass(String)</code> to check if the
* class has already been loaded. If it has, the same
* <code>Class</code> object is returned.</li>
* <li>If the <code>delegate</code> property is set to <code>true</code>,
* call the <code>loadClass()</code> method of the parent class
* loader, if any.</li>
* <li>Call <code>findClass()</code> to find this class in our locally
* defined repositories.</li>
* <li>Call the <code>loadClass()</code> method of our parent
* class loader, if any.</li>
* </ul>
* If the class was found using the above steps, and the
* <code>resolve</code> flag is <code>true</code>, this method will then
* call <code>resolveClass(Class)</code> on the resulting Class object.
*
* @param name The binary name of the class to be loaded
* @param resolve If <code>true</code> then resolve the class
*
* @exception ClassNotFoundException if the class was not found
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled()) {
log.debug("loadClass(" + name + ", " + resolve + ")");
}
Class<?> clazz = null;

// Log access to stopped class loader
checkStateForClassLoading(name);

// (0) Check our previously loaded local class cache
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
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 bootstrap class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);

ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
// details of how this may trigger a StackOverflowError
// Given these reported errors, catch Throwable to ensure any
// other edge cases are also caught
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}

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

// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = sm.getString("webappClassLoader.restrictedPackage", name);
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}

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
if (log.isDebugEnabled()) {
log.debug(" Searching local repositories");
}
try {
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
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);
}

其中注释 (0)(0.1) 首先是检查类是否已经加载过,如果已经加载过则直接返回加载过的类,注释 (0.2) 在 Oracle JVM 实现中是使用 javaseLoader 即 Ext ClassLoader 去加载 JRE 的基类,因为默认配置的情况下 delegatefalse,所以如果之前没加载到类会执行注释 (2) 的逻辑加载 web 应用的类,若仍然未加载到则执行注释 (3) 委托给当前 ParallelWebappClassLoader 的 parent 即 Common ClassLoader 进行类的加载,而 Common ClassLoader 又会先委托给其父类加载器 System ClassLoader 进行加载,所以加载顺序如文档所示,如果以上逻辑执行完都没有加载到类,则最后抛出 ClassNotFoundException

类似的实现在 Flink 中也存在,在 Flink 中被称为 child-first,可以参考文档:Debugging Classloading | Apache Flink

References

java - Difference between JVM and HotSpot? - Stack Overflow
Apache Tomcat 8 Configuration Reference (8.5.72) - The Loader Component
java - How Tomcat Classloader separates different Webapps object scope in same JVM? - Stack Overflow
About the comment of org.apache.tomcat.util.threads.TaskQueue