Tomcat源码分析WebappClassLoader分析(基于8.0.5)

1、疑惑

在刚接触 Tomcat 中的ClassLoader时心中不免冒出的疑惑: "Tomcat 里面是怎么样设计ClassLoader的, 这样设计有什么好处?"; 我们先把这个问题留着, 到最后在看 !

2、java中ClassLoader类别

1. BootstrapClassLoader
    加载路径: System.getProperty("java.class.path") 或直接通过 -Xbootclasspath 指定
    特性: 用C语言写的
    手动获取加载路径: sun.misc.Launcher.getBootstrapClassPath().getURLs()

2. ExtClassLoader
    加载路径: System.getProperty("java.ext.dirs") 或直接通过 -Djava.ext.dirs 指定
    特性: 继承 URLClassLoader
    手动获取加载路径:((URLClassLoader)App.class.getClassLoader().getParent()).getURLs()


3. AppClassLoader
    加载路径: System.getProperty("sun.boot.class.path") 或直接通过 -cp, -classpath 指定
    特性: 继承 URLClassLoader
    手动获取加载路径: ((URLClassLoader)App.class.getClassLoader()).getURLs()
    通过 ClassLoader.getSystemClassLoader() 就可以获取 AppClassLoader, 自己写的程序中写的 ClassLoader(继承 URLClassLoader), 若不指定 parent, 默认的parent就是 AppClassLoader

PS:
AppClassLoader.getparent() = ExtClassLoader
ExtClassLoader.getParent() == null, 则直接通过 BootstrapClassLoader 来进行加载

3、java中ClassLoader主要方法

1. loadClass    方法 实现双亲委派模型
2. findClass    方法 根据Class名称获取Class路径, 然后调用 defineClass 进行加载到JVM 内存中
3. defineClass  方法 加Class文件的二进制字节码加载到JVM内存生成Class对象
4. resolveClass 方法 JVM规范里面指连接操作中的第三步操作, 实际上我们的平时使用的JDK并没有按照JVM的这个规范进行设计, 你在进行debug时, 发现这个 resolveClass 永远是 false

4、ClassLoader.loadClass()方法

ClassLoader的双亲委派模式主要体现在 loadClass 方法上, 直接看代码

synchronized (getClassLoadingLock(name)) {              // 1. 通过一个ClassName对应一个 Object, 放到 ConcurrentHashMap 中, 最终通过 synchronized 实现并发加载
    Class<?> c = findLoadedClass(name);                 // 2. 查看本 ClassLoader 是否加载过
    if (c == null) {
        try {
            if (parent != null) {                       // 4. parent != null, 则通过父ClassLoader来进行加载 (加载的原则是: class 一定要在 URLClassPath 中)
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);     // 5. parent == null, 则说明当前ClassLoader是ExtClassLoader, 直接通过 BootstrapClassLoader 来进行加载 (加载的原则是: class 一定要在 URLClassPath 中)
            }
        } catch (ClassNotFoundException e) {}
        if (c == null) {                                // 6. delegate 父 ClassLoader 还没加载成功, 则用当前ClassLoader 来进行加载
            c = findClass(name);                        // 7. 通过 findClass 在本 ClassLoader 的path 上进行查找 class, 转化成 byte[], 通过 defineClass 加载到内存中 (加载的原则是: class 一定要在 URLClassPath 中)
        }
    }
    if (resolve) {                                      // 8. 永远的 resolve = false, JVM规范指定是通过 resolveClass 方法实现 链接 操作的第三步, 实际我们的JVM上并没有实现这个操作
        resolveClass(c);
    }
    return c;
}

5、ClassLoader加载模式

下面通过一个简单的Demo加深一下理解ClassLoader

Class A {
    public void doSomething(){
        B b = new B();
        b.doSomething();
    }

    public static void main(String[] args){
        A a = new A();
        a.doSomething()
    }
}

执行命令 java -classpath: test.jar A

操作步骤
1. AClass = AppClassLoader.loadClass(A)                                 # 通过 AppClassLoader 加载类A
2. BClass = AClass.getClassLoader().loadClass(B)                        # 其中通过 AClass.getClassLoader.getResource("/" + B.class.getName().replace(".", "/") + ".class") 查找 B 的Resource
3. BClass.getDeclaredMethod("doSomething").invoke(BClass.newInstance()) # 直接激活方法 doSomething

从中我们可以得知 在默认方法内进行 new 出对象, 其实是用的 Thread.currentThread().getContextClassloader() 来进行加载的 (A.class.getClassLoader() = B.class.getClassLoader()),有了上面的知识后我们再来看看 Tomcat 中的 ClassLoader。

6、Tomcat中ClassLoader的种类

1. BootstrapClassLoader : 系统类加载器
2. ExtClassLoader       : 扩展类加载器
3. AppClassLoader       : 普通类加载器
#下面是 这几个 Classloader 是 Tomcat 对老版本的兼容
4. commonLoader         : Tomcat 通用类加载器, 加载的资源可被 Tomcat 和 所有的 Web 应用程序共同获取
5. catalinaLoader       : Tomcat 类加载器, 加载的资源只能被 Tomcat 获取(但 所有 WebappClassLoader 不能获取到 catalinaLoader 加载的类)
6. sharedLoader         : Tomcat 各个Context的父加载器, 这个类是所有 WebappClassLoader 的父类, sharedLoader 所加载的类将被所有的 WebappClassLoader 共享获取
7. WebappClassLoader    : 每个Context 对应一个 WebappClassloader, 主要用于加载 WEB-INF/lib 与 WEB-INF/classes 下面的资源

这个版本 (Tomcat 8.x.x) 中, 默认情况下 commonLoader = catalinaLoader = sharedLoader
(PS: 为什么这样设计, 主要这样这样设计 ClassLoader 的层级后, WebAppClassLoader 就能直接访问 tomcat 的公共资源, 若需要tomcat 有些资源不让 WebappClassLoader 加载, 则直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 加载路径就可以了)

在看看下面的 UML 图, 加深一下理解:

从新再来看一下 ClassLoader 的初始化:

/**
 * 1. BootstrapClassLoader  : 系统类加载器
 * 2. ExtClassLoader        : 扩展类加载器
 * 3. AppClassLoader        : 普通类加载器
 #下面是 这几个 Classloader 是 Tomcat 对老版本的兼容
 * 4. commonLoader      : Tomcat 通用类加载器, 加载的资源可被 Tomcat 和 所有的 Web 应用程序共同获取
 * 5. catalinaLoader    : Tomcat 类加载器, 加载的资源只能被 Tomcat 获取(但 所有 WebappClassLoader 不能获取到 catalinaLoader 加载的类)
 * 6. sharedLoader      : Tomcat 各个Context的父加载器, 这个类是所有 WebappClassLoader 的父类, sharedLoader 所加载的类将被所有的 WebappClassLoader 共享获取
 *
 * 这个版本 (Tomcat 8.x.x) 中, 默认情况下 commonLoader = catalinaLoader = sharedLoader
 * (PS: 为什么这样设计, 主要这样这样设计 ClassLoader 的层级后, WebAppClassLoader 就能直接访问 tomcat 的公共资源, 若需要tomcat 有些资源不让 WebappClassLoader 加载, 则直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 加载路径就可以了)
 */
private void initClassLoaders() {
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    try {                                                               // 1. 补充: createClassLoader 中代码最后调用 new URLClassLoader(array) 来生成 commonLoader, 此时 commonLoader.parent = null,  则采用的是默认的策略 Launcher.AppClassLoader
        commonLoader = createClassLoader("common", null);               // 2. 根据 catalina.properties 指定的 加载jar包的目录, 生成对应的 URLClassLoader( 加载 Tomcat 中公共jar包的 classLoader, 这里的 parent 参数是 null, 最终 commonLoader.parent 是 URLClassLoader)
        if( commonLoader == null ) {                                    // 3. 若 commonLoader = null, 则说明在 catalina.properties 里面 common.loader 是空的
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);     // 4. 将 commonClassLoader 作为父 ClassLoader, 生成 catalinaLoader,这个类就是加载 Tomcat bootstrap.jar, tomcat-juli.jar 包的 classLoader (PS; 在 catalina.properties 里面 server.loader 是空的, 则代码中将直接将 commonLoader 赋值给 catalinaLoader)
        sharedLoader = createClassLoader("shared", commonLoader);       // 5. 将 commonClassLoader 作为父 ClassLoader, 生成 sharedLoader, 这个类最后会作为所有 WebappClassLoader 的父类 ( PS: 因为 catalina.properties 里面 shared.loader 是空的, 所以代码中直接将 commonLoader 赋值给 sharedLoader)
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

额, 漏了, 漏了一个 JasperLoader, 这个classLoader 直接继承 URLClassLoader, 当程序将 JSP 编译成 servlet 的class之后, 通过这个 JasperLoader 进行加载(PS: 这个 JasperLoader 其实没有什么太多的功能); 接下来我们主要看 WebappClassLoader

7、WebappClassLoader常见属性

protected final Matcher packageTriggersDeny = Pattern.compile(                          // 在 delegating = false 的情况下, 被这个正则匹配到的 class 不会被 WebappClassLoader 进行加载 (其实就是 Tomcat 中的代码不能被 WebappClassLoader 来加载)
        "^javax\\.el\\.|" +
        "^javax\\.servlet\\.|" +
        "^org\\.apache\\.(catalina|coyote|el|jasper|juli|naming|tomcat)\\."
        ).matcher("");

protected final Matcher packageTriggersPermit =                                         // 在 delegating = false 的情况下, 下面正则匹配到的类会被 WebappClassLoader 进行加载
        Pattern.compile("^javax\\.servlet\\.jsp\\.jstl\\.").matcher("");

protected final ClassLoader parent;                                                     // WebappClassLoader 的父 parent(在这里 Tomcat 8.x.x, parent  其实就是 commonClassloader)
protected final ClassLoader j2seClassLoader;                                            // 这个 classLoader 其实就是 ExtClassLoader (PS: 所有的 WebappClassLoader 出发到加载 J2SE 的类时, 直接通过 ExtClassLoader / BootstrapClassLoader 来进行加载 )
                                                                                
protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>(); // 加载资源的时候会将 文件缓存在这个 Map 里面, 下次就可以根据 ResourceEntry.lastModified 来判断是否需要热部署

protected WebResourceRoot resources = null;                                             // 这个 WebappClassLoader 加载的资源(PS: 其实就是 StandardRoot, 在WebappClassLoader 启动时, 会载入 WEB-INF/lib 与 WEB-INF/classes 下的资源de URL加入 WebAppClassLoader的 URLClassPath 里面)

private final HashMap<String,Long> jarModificationTimes = new HashMap<>();              // 保存每个加载的资源, 上次修改的时间 (后台定时任务检查这个修改时间, 决定是否需要 reload)

8、WebappClassLoader构造函数

parent: WebappClassLoader 的父classLoader, j2seClassLoader: ExtClassLoader, 所有WebappClassLoader 加载 J2SE 的类时, 需通过 ExtClassLoader 或 BootstartpClassLoader 来进行加载。

public WebappClassLoader(ClassLoader parent) {              // 1. 在 Tomcat 8.x.x 中运行时, 会发现 parent 就是 commonClassLoader

    super(new URL[0], parent);

    ClassLoader p = getParent();                            // 2. 这里做个检查, 若构造函数传来的 parent 是 null, 则 将 AppClassLoader 赋值给 WebAppClassLoader 的 parent
    if (p == null) {
        p = getSystemClassLoader();
    }
    this.parent = p;
                                                            // 3. 下面几步是 获取 Launcher.ExtClassLoader 赋值给 j2seClassLoader (主要是在类加载时会被用到)
    ClassLoader j = String.class.getClassLoader();
    if (j == null) {
        j = getSystemClassLoader();
        while (j.getParent() != null) {
            j = j.getParent();
        }
    }
    this.j2seClassLoader = j;                               // 4. 这里进行赋值的就是 Launcher.ExtClassLoader

    securityManager = System.getSecurityManager();          // 5. 这里的操作主要是判断 Java 程序是否启动安全策略
    if (securityManager != null) {
        refreshPolicy();
    }
}

9、WebappClassLoader start方法

/**
 * Start the class loader.
 *
 * @exception LifecycleException if a lifecycle error occurs
 * 将 /WEB-INF/classes 及 /WEB-INF/lib 封装成 URL 加入到 ClassLoader 的 URLClassPath 里面
 */
@Override
public void start() throws LifecycleException {
                                                                            // 下面的 resources 其实就是  StandardRoot
                                                                            // WebappClassLoader 进行资源/类 URL 的加载操作 (/WEB-INF/classes  与 WEB-INF/lib 下面资源的 URL)
    WebResource classes = resources.getResource("/WEB-INF/classes");        // 1. 加入 /WEB_INF/classes 的 URL
    if (classes.isDirectory() && classes.canRead()) {
        addURL(classes.getURL());
    }                                                                       // 2. 加入 /WEB_INF/lib 下面的 jar 的URL 加入 URLClassPath
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
            addURL(jar.getURL());                                           // 3. 这一步就是将 ClassLoader需要加载的 classPath 路径 加入到 URLClassLoader.URLClassPath 里面
            jarModificationTimes.put(                                       // 4. 放一下 jar 文件的 lastModified
                    jar.getName(), Long.valueOf(jar.getLastModified()));
        }
    }
}

这个方法其实就是将 /WEB-INF/classes 及 /WEB-INF/lib 封装成 URL 加入到 ClassLoader 的 URLClassPath 里面(PS: 当WebappClassloader在加载Class时, 通过这个URLs来决定是否加载 class )

10、WebappClassLoader modified方法

Tomcat 后来会启用定时任务, 来检查已经加载的资源是否有修改/增加/删减, 来触发 StandardContext 的 reload; 见代码

/**
 * Have one or more classes or resources been modified so that a reload
 * is appropriate?
 */
// 校验 WebappClassLoader 加载的资源是否有修改过, 若有文件修改过, 则进行热部署
public boolean modified() {

    if (log.isDebugEnabled())
        log.debug("modified()");

    for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) {       // 1. 遍历已经加载的资源
        long cachedLastModified = entry.getValue().lastModified;
        long lastModified = resources.getClassLoaderResource(
                entry.getKey()).getLastModified();                                  // 2. 对比 file 的 lastModified的属性
        if (lastModified != cachedLastModified) {                                   // 3. 若修改时间不对, 则说明文件被修改过, StandardContext 需要重新部署
            if( log.isDebugEnabled() )
                log.debug(sm.getString("webappClassLoader.resourceModified",
                        entry.getKey(),
                        new Date(cachedLastModified),
                        new Date(lastModified)));
            return true;
        }
    }

    // Check if JARs have been added or removed
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    // Filter out non-JAR resources

    int jarCount = 0;
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {      // 4. 比较 /WEB-INF/lib 下的 jar 包是否有修改/增加/减少
            jarCount++;                                                              // 5. 记录 /WEB-INF/lib 下的 jar 的个数
            Long recordedLastModified = jarModificationTimes.get(jar.getName());
            if (recordedLastModified == null) {
                // Jar has been added
                log.info(sm.getString("webappClassLoader.jarsAdded",
                        resources.getContext().getName()));
                return true;
            }
            if (recordedLastModified.longValue() != jar.getLastModified()) {        // 6. 比较一下这次的文件修改时间 与 上次文件的修改时间是否一样, 不一样的话, 直接返回 true, StandardContext 需要重新部署
                // Jar has been changed
                log.info(sm.getString("webappClassLoader.jarsModified",
                        resources.getContext().getName()));
                return true;
            }
        }
    }

    if (jarCount < jarModificationTimes.size()){                                 // 7. 判断 WebappClassloader文件是够有增加/减少, 若有变化的话, 直接返回 true, StandardContext 需要重新部署
        log.info(sm.getString("webappClassLoader.jarsRemoved",
                resources.getContext().getName()));
        return true;
    }


    // No classes have been modified
    return false;
}

11、WebappClassLoader loadClass方法

双亲委派模式的开关: WebappClassLoader 的loadClass有一个标识(delegateLoad) 用来控制是否启用双亲委派模式;
下面来看方法的主要步骤:

 1. 判断当前运用是否已经启动, 未启动, 则直接抛异常
 2. 调用 findLocaledClass0 从 resourceEntries 中判断 class 是否已经加载 OK
 3. 调用 findLoadedClass(内部调用一个 native 方法) 直接查看对应的 WebappClassLoader 是否已经加载过
 4. 调用 binaryNameToPath 判断是否 当前 class 是属于 J2SE 范围中的, 若是的则直接通过 ExtClassLoader, BootstrapClassLoader 进行加载 (这里是双亲委派)
 5. 在设置 JVM 权限校验的情况下, 调用 securityManager 来进行权限的校验(当前类是否有权限加载这个类, 默认的权限配置文件是 ${catalina.base}/conf/catalina.policy)
 6. 判断是否设置了双亲委派机制 或 当前 WebappClassLoader 是否能加载这个 class (通过 filter(name) 来决定), 将最终的值赋值给 delegateLoad
 7. 根据上一步中的 delegateLoad 来决定是否用 WebappClassloader.parent(也就是 sharedClassLoader) 来进行加载, 若加载成功, 则直接返回
 8. 上一步若未加载成功, 则调用 WebappClassloader.findClass(name) 来进行加载
 9. 若上一还是没有加载成功, 则通过 parent 调用 Class.forName 来进行加载
 10. 若还没加载成功的话, 那就直接抛异常

直接看代码

 public synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {

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

    // Log access to stopped classloader                                     // 1.  判断程序是否已经启动了, 未启动 OK, 就进行加载, 则直接抛异常
    if (!started) {
        try {
            throw new IllegalStateException();
        } catch (IllegalStateException e) {
            log.info(sm.getString("webappClassLoader.stopped", name), e);
        }
    }

    // (0) Check our previously loaded local class cache
                                                                             // 2. 当前对象缓存中检查是否已经加载该类, 有的话直接返回 Class
    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
                                                                             // 3. 是否已经加载过该类 (这里的加载最终会调用一个 native 方法, 意思就是检查这个 ClassLoader 是否已经加载过对应的 class 了哇)
    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 // 代码到这里发现, 上面两步是 1. 查看 resourceEntries 里面的信息, 判断 class 是否加载过, 2. 通过 findLoadedClass 判断 JVM 中是否已经加载过, 但现在 直接用 j2seClassLoader(Luancher.ExtClassLoader 这里的加载过程是双亲委派模式) 来进行加载
    //       the webapp from overriding J2SE classes                        // 这是为什么呢 ? 主要是 这里直接用 ExtClassLoader 来加载 J2SE 所对应的 class, 防止被 WebappClassLoader 加载了
    String resourceName = binaryNameToPath(name, false);                    // 4. 进行 class 名称 转路径的操作 (文件的尾缀是 .class)
    if (j2seClassLoader.getResource(resourceName) != null) {                // 5. 这里的 j2seClassLoader 其实就是 ExtClassLoader, 这里就是 查找 BootstrapClassloader 与 ExtClassLoader 是否有权限加载这个 class (通过 URLClassPath 来确认)
        try {
            clazz = j2seClassLoader.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   // 6. 这里的 securityManager 与 Java 安全策略是否有关, 默认 (securityManager == null), 所以一开始看代码就不要关注这里
    if (securityManager != null) {
        int i = name.lastIndexOf('.');
        if (i >= 0) {
            try {
                securityManager.checkPackageAccess(name.substring(0,i));   // 7. 通过 securityManager 对 是否能加载 name 的权限进行检查 (对应的策略都在 ${catalina.base}/conf/catalina.policy 里面进行定义)
            } 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);                      // 8. 读取 delegate 的配置信息, filter 主要判断这个 class 是否能由这个 WebappClassLoader 进行加载 (false: 能进行加载, true: 不能被加载)

    // (1) Delegate to our parent if requested
    // 如果配置了 parent-first 模式, 那么委托给父加载器                      // 9. 当进行加载 javax 下面的包 就直接交给 parent(sharedClassLoader) 来进行加载 (为什么? 主要是 这些公共加载的资源统一由 sharedClassLoader 来进行加载, 能减少 Perm 区域的大小)
    if (delegateLoad) {                                                   // 10. 若 delegate 开启, 优先使用 parent classloader( delegate 默认是 false); 这里还有一种可能, 就是 经过 filter(name) 后, 还是返回 true, 那说明 WebappClassLoader 不应该进行加载, 应该交给其 parent 进行加载
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        try {
            clazz = Class.forName(name, false, parent);                   // 11. 通过 parent ClassLoader 来进行加载 (这里构造函数中第二个参数 false 表示: 使用 parent 加载 classs 时不进行初始化操作, 也就是 不会执行这个 class 中 static 里面的初始操作 以及 一些成员变量ed赋值操作, 这一动作也符合 JVM 一贯的 lazy-init 策略)
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);                                           // 12. 通过 parent ClassLoader 加载成功, 则直接返回
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    // (2) Search local repositories
    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    try {
        // 从 WebApp 中去加载类, 主要是 WebApp 下的 classes 目录 与 lib 目录
        clazz = findClass(name);                                         // 13. 使用当前的 WebappClassLoader 加载
        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
    // 如果在当前 WebApp 中无法加载到, 委托给 StandardClassLoader 从 $catalina_home/lib 中去加载
    if (!delegateLoad) {                                                 // 14. 这是在 delegate = false 时, 在本 classLoader 上进行加载后, 再进行操作这里
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        try {
            clazz = Class.forName(name, false, parent);                 // 15. 用 WebappClassLoader 的 parent(ExtClassLoader) 来进行加载
            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);                            // 16. 若还是加载不到, 那就抛出异常吧
}

在上面步骤中, WebappClassLoader首选会在本地资源来获取 class, 见方法 findLoadedClass0

protected Class<?> findLoadedClass0(String name) {                  // 1. 根据加载的 className 来加载 类

    String path = binaryNameToPath(name, true);                     // 2. 将 类名转化成 类的全名称

    ResourceEntry entry = resourceEntries.get(path);                // 3. resourceEntries 是 WebappClassLoader 加载好的 class 存放的地址
    if (entry != null) {
        return entry.loadedClass;                                   // 4. 将 加载好的 class 直接返回
    }
    return null;
}

12、WebappClassLoader findClassInternal方法

WebappClassLoader 作为ClassLoader 的子类, 其实现了自己的一套资源查找方法, 具体的逻辑在 findClassInternal 中

protected Class<?> findClassInternal(String name)
    throws ClassNotFoundException {

    if (!validate(name))                                    // 1. 对于 J2SE 下面的 Class, 不能通过这个 WebappClassloader 来进行加载
        throw new ClassNotFoundException(name);

    String path = binaryNameToPath(name, true);             // 2. 将类名转化成路径名称

    ResourceEntry entry = null;

    if (securityManager != null) {
        PrivilegedAction<ResourceEntry> dp =
            new PrivilegedFindResourceByName(name, path);
        entry = AccessController.doPrivileged(dp);
    } else {
        entry = findResourceInternal(name, path);          // 3. 调用 findResourceInternal  返回 class 的包装类 entry
    }

    if (entry == null)
        throw new ClassNotFoundException(name);

    Class<?> clazz = entry.loadedClass;                    // 4. 若程序已经生成了 class, 则直接返回
    if (clazz != null)
        return clazz;

    synchronized (this) {
        clazz = entry.loadedClass;
        if (clazz != null)
            return clazz;

        if (entry.binaryContent == null)
            throw new ClassNotFoundException(name);

        // Looking up the package
        String packageName = null;
        int pos = name.lastIndexOf('.');
        if (pos != -1)
            packageName = name.substring(0, pos);         // 5. 获取包名

        Package pkg = null;

        if (packageName != null) {
            pkg = getPackage(packageName);                // 6. 通过 包名 获取对应的 Package 对象
            // Define the package (if null)
            if (pkg == null) {                            // 7. 若还不存在, 则definePackage
                try {
                    if (entry.manifest == null) {
                        definePackage(packageName, null, null, null, null,
                                null, null, null);
                    } else {
                        definePackage(packageName, entry.manifest,
                                entry.codeBase);
                    }
                } catch (IllegalArgumentException e) {
                    // Ignore: normal error due to dual definition of package
                }
                pkg = getPackage(packageName);            // 8. 获取 Package
            }
        }

        if (securityManager != null) {                    // 9. 若程序运行配置了 securityManager, 则进行一些权限方面的检查

            // Checking sealing
            if (pkg != null) {
                boolean sealCheck = true;
                if (pkg.isSealed()) {
                    sealCheck = pkg.isSealed(entry.codeBase);
                } else {
                    sealCheck = (entry.manifest == null)
                        || !isPackageSealed(packageName, entry.manifest);
                }
                if (!sealCheck)
                    throw new SecurityException
                        ("Sealing violation loading " + name + " : Package "
                         + packageName + " is sealed.");
            }

        }

        try {                                            // 10 最终调用 ClassLoader.defineClass 来将 class 对应的 二进制数据加载进来, 进行 "加载, 连接(解析, 验证, 准备), 初始化" 操作, 最终返回 class 对象
            clazz = defineClass(name, entry.binaryContent, 0,                       
                    entry.binaryContent.length,
                    new CodeSource(entry.codeBase, entry.certificates));
        } catch (UnsupportedClassVersionError ucve) {
            throw new UnsupportedClassVersionError(
                    ucve.getLocalizedMessage() + " " +
                    sm.getString("webappClassLoader.wrongVersion",
                            name));
        }
        // Now the class has been defined, clear the elements of the local
        // resource cache that are no longer required.
        entry.loadedClass = clazz;
        entry.binaryContent = null;
        entry.codeBase = null;
        entry.manifest = null;
        entry.certificates = null;
        // Retain entry.source in case of a getResourceAsStream() call on
        // the class file after the class has been defined.
    }

    return clazz;                                         // 11. return 加载了的 clazz
}

13、 WebappClassLoader findResourceInternal方法

在Tomcat中, 其资源的查找都是通过 JNDI(具体存储在了 StandardRoot里面), WebappClassLoader 的资源查找, 并且将找到的资源转化成 byte[] 就是在 findResourceInternal 里面实现

protected ResourceEntry findResourceInternal(final String name, final String path) {

    if (!started) {
        log.info(sm.getString("webappClassLoader.stopped", name));
        return null;
    }

    if ((name == null) || (path == null))
        return null;

    ResourceEntry entry = resourceEntries.get(path);        // 1. resourceEntries 里面会存储所有已经加载了的 文件的信息
    if (entry != null)
        return entry;

    boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX);

    WebResource resource = null;

    boolean fileNeedConvert = false;

    resource = resources.getClassLoaderResource(path);      // 2. 通过 JNDI 来进行查找 资源 (想知道 resources 里面到底是哪些资源, 可以看 StandardRoot 类)

    if (!resource.exists()) {                               // 3. 若资源不存在, 则进行返回
        return null;
    }

    entry = new ResourceEntry();                            // 4. 若所查找的 class 对应的 ResourceEntry 不存在, 则进行构建一个
    entry.source = resource.getURL();
    entry.codeBase = entry.source;
    entry.lastModified = resource.getLastModified();

    if (needConvert) {
        if (path.endsWith(".properties")) {
            fileNeedConvert = true;
        }
    }

    /* Only cache the binary content if there is some content
     * available and either:
     * a) It is a class file since the binary content is only cached
     *    until the class has been loaded
     *    or
     * b) The file needs conversion to address encoding issues (see
     *    below)
     *
     * In all other cases do not cache the content to prevent
     * excessive memory usage if large resources are present (see
     * https://issues.apache.org/bugzilla/show_bug.cgi?id=53081).
     */
    if (isClassResource || fileNeedConvert) {                               // 5. 获取对应资源的二进制字节流, 当需要进行转码时, 进行相应的转码操作
        byte[] binaryContent = resource.getContent();
        if (binaryContent != null) {
             if (fileNeedConvert) {
                // Workaround for certain files on platforms that use
                // EBCDIC encoding, when they are read through FileInputStream.
                // See commit message of rev.303915 for details
                // http://svn.apache.org/viewvc?view=revision&revision=303915
                String str = new String(binaryContent);
                try {
                    binaryContent = str.getBytes(StandardCharsets.UTF_8);   // 6. 进行资源转码为 UTF-8
                } catch (Exception e) {
                    return null;
                }
            }
            entry.binaryContent = binaryContent;                           // 7. 获取资源对应的 二进制数据信息
            // The certificates and manifest are made available as a side
            // effect of reading the binary content
            entry.certificates = resource.getCertificates();               // 8. 获取资源的证书
        }
    }
    entry.manifest = resource.getManifest();

    if (isClassResource && entry.binaryContent != null &&
            this.transformers.size() > 0) {
        // If the resource is a class just being loaded, decorate it
        // with any attached transformers
        String className = name.endsWith(CLASS_FILE_SUFFIX) ?
                name.substring(0, name.length() - CLASS_FILE_SUFFIX.length()) : name;
        String internalName = className.replace(".", "/");

        for (ClassFileTransformer transformer : this.transformers) {
            try {
                byte[] transformed = transformer.transform(
                        this, internalName, null, null, entry.binaryContent
                );
                if (transformed != null) {
                    // 设置 二进制设置到 ResourceEntry
                    entry.binaryContent = transformed;
                }
            } catch (IllegalClassFormatException e) {
                log.error(sm.getString("webappClassLoader.transformError", name), e);
                return null;
            }
        }
    }

    // Add the entry in the local resource repository
    synchronized (resourceEntries) {                                        // 9. 将生成的 entry 放入 resourceEntries 中
        // Ensures that all the threads which may be in a race to load
        // a particular class all end up with the same ResourceEntry
        // instance
        ResourceEntry entry2 = resourceEntries.get(path);
        if (entry2 == null) {
            // 向本地资源缓存注册 ResourceEntry
            resourceEntries.put(path, entry);
        } else {
            entry = entry2;
        }
    }

    return entry;
}

14、 WebappClassLoader stop 方法

在进行热部署/重部署时, 会调用 WebappClassLoader 的 stop 方法, 它主要做了下面四种资源的清除

public void stop() throws LifecycleException {

    // Clearing references should be done before setting started to
    // false, due to possible side effects
    clearReferences();              // 1. 清除各种资源

    started = false;

    resourceEntries.clear();        // 2. 清空各种 WebappClassLoader 加载的数据
    jarModificationTimes.clear();   // 3. 清空各种 监视的资源(监视的资源一旦有变动, 就会触发 StandardContext 的重新加载机制)
    resources = null;

    permissionList.clear();         // 4. 下面两个清空的是与 Java 权限相关的资源
    loaderPC.clear();
}

15、总结

现在再回头看看开篇提出的问题, 现在我们有了答案了, 先看 Tomcat classLoader 设计的优点吧!

1. 热部署功能或项目(PS: 热部署JSP, Context)
2. 隔离资源的访问
    (1) 不同的 Context 之间不能相互访问对方加载的资源, 举例: 可能Context1用Spring3.1, 而 Context2用Spring4.1 若用同一个Classloader 则遇到 spring 的class只能加载一份, 就会出现想用 spring4.1里面的 AnnotationUtils, 但是 classLoader 其实加载的是 spring 3.1里面的类, 这样很有可能出现 NoSuchMethodError 异常
    (2) 不让 Context 加载类不能访问到 Tomcat 容器自身的类

但我们再想想, 为了一个热部署, Tomcat 在Stop方法里面做了多少的清理工作, 而在真实产线上 很少用Tomcat的reload, 为啥? 就是我们写的程序有时会做些Tomcat始料不及的事情 (比如 自己创建一些ClassLoader, 再用这个 ClassLoader 开启一个 loop, loop里面有引用 WebappClassLoader 加载出来的数据, 想想就觉得害怕....), 这样的话 Tomcat, 就不能完全清理所有的资源, 最终在 几次 StandardContext.reload 的情况下, Tomcat最终因为内存溢出而挂了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章