Tomcat 类加载器的实现
     Tomcat 内部定义了多个 ClassLoader,以便应用和容器访问不同存储库中的类和资源,同时达到应用间类隔离的目的。
 
1. Java 类加载机制

     类加载就是把编译生成的 class 文件,加载到 JVM 内存中(永久代/元空间)。
     类加载器之所以能实现类隔离,是因为两个类相等的前提是它们由同一个类加载器加载,否则必定不相等。
     JVM 在加载时,采用的是一种双亲委托机制,当类加载器要加载一个类时,加载顺序是:
           首先将请求委托给父加载器,如果父加载器找不到要加载的类  
           然后再查找自己的存储库尝试加载
     这个机制的好处就是能够保证核心类库不被覆盖。
     而按照 Servlet 规范的建议,Webapp 加载器略有不同,它首先会在自己的资源库中搜索,而不是向上委托,打破了标准的委托机制,来看下 Tomcat 的设计和实现。

2. Tomcat 类加载器设计
Tomcat 整体类加载器结构如下:


<ignore_js_op>Tomcat 类加载器的实现_Tomcat 
其中 JDK 内部提供的类加载器分别是:

  • Bootstrap - 启动类加载器,属于 JVM 的一部分,加载 <JAVA_HOME>/lib/ 目录下特定的文件
  • Extension - 扩展类加载器,加载 <JAVA_HOME>/lib/ext/ 目录下的类库
  • Application - 应用程序类加载器,也叫系统类加载器,加载 CLASSPATH 指定的类库

Tomcat 自定义实现的类加载器分别是:

  • Common - 父加载器是 AppClassLoader,默认加载 ${catalina.home}/lib/ 目录下的类库
  • Catalina - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 server.loader 配置的资源,一般是 Tomcat 内部使用的资源
  • Shared - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 shared.loader 配置的资源,一般是所有 Web 应用共享的资源
  • WebappX - 父加载器是 Shared 加载器,加载 /WEB-INF/classes 的 class 和 /WEB-INF/lib/ 中的 jar 包
  • JasperLoader - 父加载器是 Webapp 加载器,加载 work 目录应用编译 JSP 生成的 class 文件

在实现时,上图不是继承关系,而是通过组合体现父子关系。Tomcat 类加载器的源码类图:
<ignore_js_op>Tomcat 类加载器的实现_加载_02 
Common、Catalina 、Shared 它们都是 StandardClassLoader 的实例,在默认情况下,它们引用的是同一个对象。其中 StandardClassLoader 与 URLClassLoader 没有区别;WebappClassLoader 则按规范实现以下顺序的查找并加载:

  • 从 JVM 内部的 Bootstrap 仓库加载
  • 从应用程序加载器路径,即 CLASSPATH 下加载
  • 从 Web 程序内的 /WEB-INF/classes 目录
  • 从 Web 程序内的 /WEB-INF/lib 中的 jar 文件
  • 从容器 Common 加载器仓库,即所有 Web 程序共享的资源加载

接下来看下源码实现。

3. 自定义加载器的初始化
common 类加载器是在 Bootstrap 的 initClassLoaders 初始化的,源码如下:


[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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();
    }
    // 指定仓库路径配置文件前缀和父加载器,创建 ClassLoader 实例
    catalinaLoader = createClassLoader("server", commonLoader);
    sharedLoader = createClassLoader("shared", commonLoader);
  } catch (Throwable t) {
    log.error("Class loader creation threw exception", t);
    System.exit(1);
  }
}

 

可以看到分别创建了三个类加载器,createClassLoader 就是根据配置获取资源仓库地址,最后返回一个 StandardClassLoader 实例,核心代码如下:

 

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
 
    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent; // 如果没有配置,则返回传入的父加载器
    ArrayList repositoryLocations = new ArrayList();
    ArrayList repositoryTypes = new ArrayList();
    ...
    // 获取资源仓库路径
    String[] locations = (String[]) repositoryLocations.toArray(new String[0]);
    Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]);
    // 创建一个 StandardClassLoader 对象
    ClassLoader classLoader = ClassLoaderFactory.createClassLoader
            (locations, types, parent);
    ...
    return classLoader;
}


类加载器初始化完毕后,会创建一个 Catalina 对象,最终会调用它的 load 方法,解析 server.xml 初始化容器内部组件。那么容器,比如 Engine,又是怎么关联到这个设置的父加载器的呢?
Catalina 对象有一个 parentClassLoader 成员变量,它是所有组件的父加载器,默认是 AppClassLoader,在此对象创建完毕时,会反射调用它的 setParentClassLoader 方法,将父加载器设为 sharedLoader。
而 Tomcat 内部顶级容器 Engine 在初始化时,Digester 有一个 SetParentClassLoaderRule 规则,会将 Catalina 的 parentClassLoader 通过 Engine.setParentClassLoader 方法关联起来。

4. 如何打破双亲委托机制
答案是使用 Thread.getContextClassLoader() - 当前线程的上下文加载器,该加载器可通过 Thread.setContextClassLoader() 在代码运行时动态设置。

默认情况下,Thread 上下文加载器继承自父线程,也就是说所有线程默认上下文加载器都与第一个启动的线程相同,也就是 main 线程,它的上下文加载器是 AppClassLoader。
Tomcat 就是在 StandardContext 启动时首先初始化一个 WebappClassLoader 然后设置为当前线程的上下文加载器,最后将其封装为 Loader 对象,借助容器之间的父子关系,在加载 Servlet 类时使用。

5. Web 应用的类加载
Web 应用的类加载是由 WebappClassLoader 的方法 loadClass(String, boolean) 完成,核心代码如下:

[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
public synchronized Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
  ...
  Class clazz = null;
  // (0) 检查自身内部缓存中是否已经加载
  clazz = findLoadedClass0(name);
  if (clazz != null) {
    if (log.isDebugEnabled())
      log.debug("  Returning class from cache");
    if (resolve) resolveClass(clazz);
    return (clazz);
  }
  // (0.1) 检查 JVM 的缓存中是否已经加载
  clazz = findLoadedClass(name);
  if (clazz != null) {
    if (log.isDebugEnabled())
      log.debug("  Returning class from cache");
    if (resolve) resolveClass(clazz);
    return (clazz);
  }
  // (0.2) 尝试使用系统类加载加载,防止覆盖 J2SE 类
  try {
    clazz = system.loadClass(name);
    if (clazz != null) {
      if (resolve) resolveClass(clazz);
      return (clazz);
    }
  } catch (ClassNotFoundException e) {// Ignore}
  // (0.5) 使用 SecurityManager 检查是否有此类的访问权限
  if (securityManager != null) {
    int i = name.lastIndexOf('.');
    if (i >= 0) {
      try {
        securityManager.checkPackageAccess(name.substring(0,i));
      } catch (SecurityException se) {
        String error = "Security Violation, attempt to use " +
            "Restricted Class: " + name;
        log.info(error, se);
        throw new ClassNotFoundException(error, se);
      }
    }
  }
  boolean delegateLoad = delegate || filter(name);
  // (1) 是否委托给父类,这里默认为 false
  if (delegateLoad) {
      ...
  }
  // (2) 尝试查找自己的存储库并加载
  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) {}
  // (3) 如果此时还加载失败,那么将加载请求委托给父加载器
  if (!delegateLoad) {
    if (log.isDebugEnabled())
      log.debug("  Delegating to parent classloader at end: " + parent);
    ClassLoader loader = parent;
    if (loader == null)
      loader = system;
    try {
      clazz = loader.loadClass(name);
      if (clazz != null) {
        if (log.isDebugEnabled())
          log.debug("  Loading class from parent");
        if (resolve) resolveClass(clazz);
        return (clazz);
      }
    } catch (ClassNotFoundException e) {}
  }
  // 最后加载失败,抛出异常
  throw new ClassNotFoundException(name);
}

 

     在防止

覆盖 J2SE 类

的时候,版本 Tomcat 6,使用的是 AppClassLoader,rt.jar 核心类库是由 Bootstrap Classloader 加载的,但是在 Java 代码是获取不了这个加载器的,在高版本做了以下优化:

 

[Java] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
ClassLoader j = String.class.getClassLoader();
if (j == null) {
  j = getSystemClassLoader();
  while (j.getParent() != null) {
    j = j.getParent();
  }
}
this.javaseClassLoader = j;

 

也就是使用尽可能接近 Bootstrap 加载器的类加载器。