弄懂 Java 的 ClassLoader
2024-03-28 10:32:53 # Technical # JavaBase

ClassLoader 是什么

官方介绍:

☞ A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.

Every Class object contains a reference to the ClassLoader that defined it.

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

概括来说就是:

  • ClassLoader 是一个负责加载类的对象,用于将字节码(.class)文件加载到 JVM 中
  • 每个 Java 类都有加载它的 ClassLoader 引用
  • 数组不是通过 ClassLoader 加载的(数组类没有对应的二进制字节流),是由 JVM 直接生成的

被加载的字节码文件可以是通过 javac 编译而来的,也可以是通过其他工具生成的,或者是从网络下载下来的,只要是标准的字节码文件

另:ClassLoader 不仅可以加载类,还可以加载 Java 应用所需的资源,如文本、图像、视频等

初探 ClassLoader

根据官方文档所说的,尝试获取 ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClassLoaderTest {

private static class Test {}

public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());

System.out.println(int.class.getClassLoader());

String[] array = new String[]{};
System.out.println(array.getClass().getClassLoader());
}
}

Output:

1
2
3
sun.misc.Launcher$AppClassLoader@18b4aac2
null
null

1,3 的输出符合预期,但 int.class 的 ClassLoader 却为 null ?

JDK 提供的 ClassLoader

JDK 内置了三个 ClassLoader:

  • BootstrapClassLoader:最顶层的类加载器,由 C++ 实现,用来加载 %JRE_HOME%/lib 下的 rt.jarresources.jarcharsets.jar 等 JDK 内部的核心类库,以及 -Xbootclasspath 所配置的路径下的类
  • ExtensionClassLoader:主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,以及 java.ext.dirs 所配置的路径下的类
  • AppClassLoader:面向用户的类加载器,负责加载 classpath 下所有的 jar 包和类
graph BT
AppClassLoader --> ExtensionClassLoader
ExtensionClassLoader --> BootstrapClassLoader

rt.jar:「rt」表示「RunTime」,rt.jar 是 Java 基础类库,包含 Java doc 里面所看到的所有类,如常用的 java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.* 等等。所以上面的 int.class 的 ClassLoader 其实是 BootstrapClassLoader 只不过因为是 C++ 实现的,所以为 null

Java9 引入了模块系统,ExtensionClassLoader 改名为 PlatformClassLoader

双亲委派模型

还是先上官方介绍:

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine’s built-in class loader, called the “bootstrap class loader”, does not itself have a parent but may serve as the parent of a ClassLoader instance.

简而言之:

  • ClassLoader 使用委托模型(delegation model)来搜索类和资源
  • 委托模型要求除了顶级启动类(BootstrapClassLoader)外,其他的类加载器都应有自己的父级类加载器
  • ClassLoader 实例会在自己尝试查找类和资源之前,将任务委托给它的父级类加载器

「双亲委派」这个翻译很具有迷惑性,容易望文生义理解为「父」+「母」两个上级,但实际只有一个,个人觉得应该叫做「父级委派」

sequenceDiagram
    participant A as class or resource
    participant B as xxxClassLoader
    participant C as AppClassLoader
    participant D as ExtensionClassLoader
    participant E as BootstrapClassLoader
    
    A ->> +B: 请求加载
    B ->> B: 已加载
    B -->> A: 返回
    B ->> C: 委托父级类加载器
    C ->> C: 已加载
    C -->> B: 返回
    C ->> D: 委托父级类加载器
    D ->> D: 已加载
    D -->> C: 返回
    D ->> E: 委托父级类加载器
    E ->> E: 已加载
    E -->> D: 返回
    
    Note over B,E: —>—>自下而上委托加载—>—>
    
    E ->> E: 尝试加载
    E -->> D: 加载成功,返回
    E -x D: 父级无法加载,子级自行加载
    D ->> D: 尝试加载
    D -->> C: 加载成功,返回
    D -x C: 父级无法加载,子级自行加载
    C ->> C: 尝试加载
    C -->> B: 加载成功,返回
    C -x B: 父级无法加载,子级自行加载
    B ->> B: 尝试加载
    B -->> A: 加载成功,返回
    B -x A: 加载失败,抛出ClassNotFoundException异常
    
    Note over E,B: <-<-自上而下尝试加载<-<-

class or resource 为需要加载的类或资源

xxxClassLoader 为自定义类加载器,可能有多个

图看起来可能会觉得很复杂,看代码可以很清晰的知道整个过程

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
// 如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
// 当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
// 当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 非空父类的类加载器无法找到相应的类,则抛出异常
}

if (c == null) {
// 当父类加载器无法加载时,则调用findClass方法来加载该类
// 用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);

// 用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 解析Class
resolveClass(c);
}
return c;
}
}

步骤概要:

  1. 执行 findLoadedClass(String) 方法检测是否以及加载过这个 class
  2. 如果没有加载过,则委托父级加载器去查找/加载这个 class
  3. 如果父级加载器没找到也没法加载,则调用 findClass(String) 来加载这个 class
  4. 如果最后加载出来,并且 resolve = true 的话,调用 resolveClass(Class) 来解析 Class 对象

findLoadedClass(String) 方法底层调用的是一个本地方法 findLoadedClass0(String),主要内容是查找已经加载的类。当一个类被加载后,JVM 会维护一个 ClassLoader 所加载的类的缓存,以便后续的类加载请求中能迅速找到已经加载的类,避免重复加载

resolveClass(Class) 方法底层也是一个本地方法 resolveClass0(Class),主要用来解析指定的 Class。ClassLoader 加载类的字节码文件并创建好 Class 对象后,由此方法解析 Class 对象的父类、字段和方法等。解析的过程包括验证类的格式、符号引用转换为直接引用等操作,确保类的正确性和完整性。

判断两个类是否相同,不仅要比较两个类的全类名,还要看这两个类的类加载器是否相同。即使同一个 .class 文件,但由两个不同的类加载器加载,那么加载出来的两个类也是不同的

为什么采用双亲委派模型

  • 安全性:双亲委派模型可以防止恶意类的加载。当一个类需要被加载时,首先会委托给父类加载器去加载,如果父类加载器找不到该类,子类加载器才会尝试加载。这种机制确保了核心类库不会被恶意修改,提高了系统的安全性
  • 一致性:双亲委派模型可以确保类库的一致性。如果一个类已经被加载,那么它就会被缓存起来,不会被重复加载
  • 隔离性:每个类加载器都有自己命名空间,相同类名的类可以被不同的类加载器加载,从而实现了类的隔离

注:这里个「一致性」和「隔离性」看似冲突,其实不然,「隔离性」强调的是 不同实例,同一个类加载器的不同实例加载的同名类,被认为是两个不同的类

缺点:

  • 性能损耗:双亲委派模型可能引入一些性能开销,因为在加载类时需要逐级委托给父类加载器。对于一些特殊场景,如动态生成类,可能会带来一些额外的开销
  • 灵活性受限:在一些特殊情况下,可能需要打破双亲委派模型的限制,例如一些应用服务器需要在不同的应用程序中加载相同类的不同版本。这就需要采用一些独立的类加载机制,而双亲委派模型限制了这种灵活性

总体而言,双亲委派模型的设计更适用于绝大多数场景,尤其是在提高系统安全性和一致性方面。然而,在一些特殊需求下,可能需要对类加载机制进行定制

修改双亲委派模型

双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式

从前面的双亲委派过程和源码可知,双亲委派的核心在于 loadClass(String) 方法中先判断是否加载,再委托父级加载器加载,最后自己加载

自定义一个类加载器,然后重写 loadClass(String) 方法就可以修改整个流程。比如可以先尝试自己加载,然后委托父级加载器(如:Tomcat 自定义的 WebAppClassLoader)

还可以重写 findClass(String) 方法,从指定位置加载类

自定义类加载器

这里测试从网络获取一个 .class 文件然后使用自定义的类加载器加载并执行

先创建一个 MyTest.java

1
2
3
4
5
6
public class MyTest {

public void test() {
System.out.println("hello world");
}
}

将其编译成 class 文件,然后上传到 GitHub 上

https://github.com/vvvenom24/my-test/raw/main/MyTest.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class ClassLoaderTest {

public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("https://github.com/vvvenom24/my-test/raw/main/MyTest.class");

Class<?> myTestClass = classLoader.loadClassAndResolve("temp.core.util.MyTest");
myTestClass.getMethod("test").invoke(myTestClass.newInstance(), null);
System.out.println("MyTest is loaded by" + myTestClass.getClassLoader());
}

}

@Slf4j
class MyClassLoader extends ClassLoader {

private final URL url;

public MyClassLoader(String urlStr) throws MalformedURLException {
this.url = new URL(urlStr);
}

public Class<?> loadClassAndResolve(String name) throws ClassNotFoundException {
return loadClass(name, true);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
log.info("My ClassLoader findClass:{}", name);

String className = name.substring(name.lastIndexOf(".") + 1);
File file = new File(name.replace(".", "/").concat(".class"));
try {
FileUtils.copyURLToFile(this.url, file);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!file.exists()) {
throw new ClassNotFoundException("Class " + className + " not found");
}
log.info("downloaded class: {} successfully", className);

byte[] fileBytes;
try {
fileBytes = FileUtils.readFileToByteArray(file);
} catch (IOException e) {
throw new RuntimeException(e);
}
return defineClass(name, fileBytes, 0, fileBytes.length);
}
}

Output:

1
2
3
4
2024-01-12 14:39:34.445  INFO   --- [           main] temp.service.MyClassLoader               : My ClassLoader findClass:temp.core.util.MyTest
2024-01-12 14:39:37.427 INFO --- [ main] temp.service.MyClassLoader : downloaded class: MyTest successfully
hello world
MyTest is loaded bytemp.service.MyClassLoader@703580bf

此时,在 temp.core.util 下创建一个 MyTest

1
2
3
4
5
6
public class MyTest {

public void test() {
System.out.println("Haaaa My test");
}
}

重新执行 ClassLoaderTest

Output:

1
2
Haaaa My test
MyTest is loaded bysun.misc.Launcher$AppClassLoader@18b4aac2

可以发现,加载出来的是后面新建的 MyTest,并且 ClassLoader 为 AppClassLoader

如果仍要加载 GitHub 上的类,就需要打破双亲委派模型,重写 loadClass 方法

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
public class ClassLoaderTest {

public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("https://github.com/vvvenom24/my-test/raw/main/MyTest.class");

Class<?> myTestClass = classLoader.loadClassAndResolve("temp.core.util.MyTest");
myTestClass.getMethod("test").invoke(myTestClass.newInstance(), null);
System.out.println("MyTest is loaded by" + myTestClass.getClassLoader());
MyTest myTest = new MyTest();
myTest.test();
System.out.println("MyTest[new] is loaded by" + myTest.getClass().getClassLoader());
}

}

@Slf4j
class MyClassLoader extends ClassLoader {

private final URL url;

public MyClassLoader(String urlStr) throws MalformedURLException {
this.url = new URL(urlStr);
}

public Class<?> loadClassAndResolve(String name) throws ClassNotFoundException {
// 先判断是否已加载过
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
log.info("Class has been loaded: {} by: {}", name, loadedClass.getClassLoader());
return loadedClass;
}

// 先自己加载
loadedClass = findClass(name);
if (loadedClass != null) {
return loadedClass;
}

// 自己无法加载,交给系统类加载器加载
ClassLoader sysClassLoader = this.getClass().getClassLoader();
if (sysClassLoader != null) {
loadedClass = sysClassLoader.loadClass(name);
}

// 最后仍未加载,抛出异常
if (loadedClass == null) {
throw new ClassNotFoundException(name);
}
return loadedClass;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
log.info("My ClassLoader findClass:{}", name);

String className = name.substring(name.lastIndexOf(".") + 1);
File file = new File(name.replace(".", "/").concat(".class"));
try {
FileUtils.copyURLToFile(this.url, file);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!file.exists()) {
throw new ClassNotFoundException("Class " + className + " not found");
}
log.info("downloaded class: {} successfully", className);

byte[] fileBytes;
try {
fileBytes = FileUtils.readFileToByteArray(file);
} catch (IOException e) {
throw new RuntimeException(e);
}
return defineClass(name, fileBytes, 0, fileBytes.length);
}
}

此处仅为简单实现,许多方面尚欠考虑,如:与 JDK 中核心类的冲突问题等

Output:

1
2
3
4
5
6
2024-01-12 15:04:26.495  INFO   --- [           main] temp.service.MyClassLoader               : My ClassLoader findClass:temp.core.util.MyTest
2024-01-12 15:04:29.271 INFO --- [ main] temp.service.MyClassLoader : downloaded class: MyTest successfully
hello world
MyTest is loaded bytemp.service.MyClassLoader@703580bf
Haaaa My test
MyTest[new] is loaded bysun.misc.Launcher$AppClassLoader@18b4aac2

可以发现加载的是 GitHub 上的类,打破了双亲委派模型,并且实现了相同名称不同版本的类的共存

Tomcat 打破双亲委派模型

Tomcat 的自定义类加载器 WebAppClassLoader 重写了 findClass(String) 方法与 loadClass(String) 方法,打破了双亲委派模型

重写 findClass 方法

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
public Class<?> findClass(String name) throws ClassNotFoundException {
...

Class<?> clazz = null;
try {
//1. 先在Web应用目录下查找类
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}

if (clazz == null) {
try {
//2. 如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}

//3. 如果父类也没找到,抛出ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}

return clazz;
}

重写的 findClass 方法主要逻辑:

  1. 先在 Web 应用的本地目录下找要加载的类
  2. 如果没找到,就交给父级类加载器(URLClassLoader)去找
  3. 父级也没找到则抛出异常

重写 loadClass 方法

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
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

synchronized (getClassLoadingLock(name)) {

Class<?> clazz = null;

//1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}

//2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}

// 3. 尝试用ExtClassLoader类加载器类加载,为什么?
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}

// 4. 尝试在本地目录搜索class并加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}

// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}

//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}

重写的 loadClass 方法主要包含以下内容:

  1. 先查找自己是否已经加载过这个类(这里的 findLoadedClass0(String) 方法并不是前面提到的那个本地方法,而是 Tomcat 自己的一个方法)
  2. 如果 Tomcat 的类加载器没加载过,查找系统的类加载器是否加载过(AppClassLoader)
  3. 如果都没,就让 ExtensionClassLoader 去加载
  4. 如果 ExtensionClassLoader 加载失败,也就是说 JRE 的核心类中没有这个类,那就尝试在本地的 Web 应用目录下查找并加载
  5. 如果还没有,说明这个类不是 Web 应用自己定义的,那就 交由系统类加载器 去加载
  6. 加载失败,抛出异常

第三步:之所以让 ExtensionClassLoader 去加载,按照前面说的自上而下的加载原则,这里会交由 BootstrapClassLoader 去加载,BootstrapClassLoader 加载的是 JRE 的核心类,这样可以确保这个类不会与核心类重名,即使重名也会优先核心类,从而避免了覆盖 JRE 核心类的问题。如果是从 AppClassLoader 去加载,也可以达到上述的目的(自上而下加载),但是 AppClassLoader 会存在一个问题,如果这个 web 应用的类与 classpath 下的类同名,此时会优先加载 classpath 下的,也正因此,选择了 ExtensionClassLoader 去加载

第五步:这里是通过 Class.forName 调用交给系统的类加载器的,因为 Class.forName 的默认加载器就是系统的类加载器

这里 Tomcat 打破双亲委派模型的最大目的是为了 使 web 应用下的类优先于 classpath 下的类,保证不同版本的类可以共存

Tomcat 中类加载器的隔离与共享

Tomcat 作为 Servlet 容器,它负责加载 Servlet 类,此外还负责加载 Servlet 所依赖的 jar 包。

它需要考虑以下几个问题:

  1. 如果一个 Tomcat 中运行多个 Web 应用,那这些 Web 应用就可能存在同名的 Servlet,但他们的功能不同,Tomcat 需要保证他们不会被相同的类加载器加载,需要确保不同 Web 应用间的隔离性
  2. 同样的,如果多个 Web 应用都依赖了相同的第三方包,如:Spring,Tomcat 需让他们共享相同的依赖,避免没必要的资源浪费
  3. 最后,同 JVM 一样,Tomcat 需要隔离自身与 Web 应用的类,避免覆盖

Tomcat 多层类加载器设计

为解决第一个问题,Tomcat 给每个 Web 应用都绑定了一个类加载器。一个 Web 应用对应着一个 Context 容器,一个 Context 容器对应着一个 WebAppClassLoader 实例。如此便能保证即使是同名,但也是不同类加载器加载的,确保隔离性

SharedClassLoader

针对第二个问题,本质是多个 WebAppClassLoader 间重复加载的问题。双亲委派模型的一个优点就是避免重复加载,所以处理方式就是给这些 WebAppClassLoader 加一个父级类加载器 —— SharedClassLoader。这样每个 WebAppClassLoader 加载前都会先去到 SharedClassLoader 搜索,加载也是从上自下,先由 SharedClassLoader 尝试加载,加载不成再交给 WebAppClassLoader 处理。

CatalinaClassLoader

第三个问题,要避免 Tomcat 自身的类被同名类覆盖,只要确保 Tomcat 自身的类加载器与 Web 应用的类加载器不同即可。共享类可以通过父子关系 解决,而 隔离类就可以通过兄弟关系 解决。所以增加一个与 SharedClassLoader 平级的 CatalinaClassLoader

CommonClassLoader

CatalinaClassLoader 可以解决 Tomcat 自身类与 Web 应用类之间的隔离问题,但是有些 Tomcat 的类是可以与 Web 应用共享的,可以避免一些重复的加载。同上,解决共享,所以加一个 CatalinaClassLoaderSharedClassLoader 的父级类加载器 —— CommonClassLoader

graph BT
CatalinaClassLoader --> CommonClassLoader
WebAppClassLoader --> SharedClassLoader
SharedClassLoader --> CommonClassLoader
CommonClassLoader --> AppClassLoader
AppClassLoader --> ExtensionClassLoader
ExtensionClassLoader --> BootstrapClassLoader

WebAppClassLoader 加载 WEB-INF/libWEB-INF/classes 目录

SharedClassLoader 加载目录由 %CATALINA_HOME%/conf/catalina.properties 中的 shared.loader 指定,默认为空

CatalinaClassLoader 加载目录由 %CATALINA_HOME%/conf/catalina.properties 中的 server.loader 指定,默认为空

CommonClassLoader 加载目录由 %CATALINA_HOME%/conf/catalina.properties 中的 common.loader 指定,默认为:"${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

ThreadContextClassLoader

单纯的依靠自定义类加载器没法满足某些场景的要求,例如,有些情况下,高层的类加载器依赖底层类加载器加载的类

拿前面的共享加载 Spring 具体来说,多个 Web 应用使用 SharedClassLoader 加载共享的 Spring jar,Spring 中的核心类可以这样,但是 Spring 还管理着业务类,这些业务类都在 WEB-INFO/class 下的,这是 WebAppClassLoader 加载目录,而 SharedClassLoader 是 WebAppClassLoader 的父级加载器。这时就出现了 高层类加载器依赖底层类加载器 的情况了,就需要利用到 ThreadContextClassLoader

不仅是 Spring,还有 SPI,SPI 的接口(如 java.sql.Driver)是由 Java 核心库提供的,是由 BootstrapClassLoader 加载的,而 SPI 的具体实现(如 com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,它们是由 AppClassLoader 加载的。默认情况下,一个类及其依赖类是由同一个类加载器加载 的,而正常 BootstrapClassLoader 是无法加载 AppClassLoader 加载的类的

ThreadContextClassLoader 其实是一种类加载器的 传递机制,并不是一个实质的类加载。类似 ThreadLocal key 为 Thread,value 为类加载器,一旦设置了 ThreadContextClassLoader,在线程后续的执行过程中就能将前面设置的类加载器取出来。Tomcat 为每个应用创建了一个 WebAppClassLoader,并且在启动时将 WebAppClassLoader 设置为 ThreadContextClassLoader,这样 Spring 就能通过 ThreadContextClassLoader 获取到 WebAppClassLoader 来加载业务类

获取 ThreadContextClassLoader ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

设置 ThreadContextClassLoader Thread.currentThread().setContextClassLoader(classLoader);

如果没有设置过 ThreadContextClassLoader 的话,将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那就默认是 AppClassLoader

classpath 与 web 应用目录

启动一个 jvm 进程可以使用 java -cp xxxjava -classpath,这里的 cp 就是 classpath的缩写,用于指定classpath

Tomcat 的 bin 目录下有个 catalina.sh 脚本,这个是 Tomcat 的启动脚本,从脚本中就可以看到 Tomcat 指定了哪些 classpath,包括 bin/bootstrap.jar lib/catalina.jar

web 应用目录则是指 Tomcat 的 webapps 目录

让 Tomcat 遵循双亲委派模型

通过参数:<Loader delegate="true" /> 可以让 Tomcat 遵循双亲委派模型