Tomcat源码研究之ClassLoader

为了集中放置,所以在收集资料的过程中将所有的信息都直接以草稿的方式放在了CSDN上,在收集了一周的资料后,今早因为CSDN的操作不熟练导致全给删了,我。。。。。。。。。。。

1. 概述

虽然一年多前就开始尝试阅读Tomcat源码源码,但Tomcat源码还是比较庞大的,所以对Tomcat的理解一直比较零散,没有形成体系。而最近碰到一些ClassLoader导致的问题,所以决定趁机研究下Tomcat中对ClassLoader的应用。本次研究我们按照时间线来进行讲解。

2. ClassLoader初始化

2.1 Bootstrap

Bootstrap作为Tomcat启动逻辑的入口,其中涉及到ClassLoader的代码主要有两处,分别是 initinitClassLoaders方法:

Bootstrap.init方法

initClassLoaders(); // 重点关注这个
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);

Bootstrap.initClassLoaders方法
这里初始化完毕的URLClassLoader是遵从ClassLoader的 级委托接待机制的。

// 该方法负责给类级字段`commonLoader`, `catalinaLoader`, `sharedLoader` 赋值。
private void initClassLoaders() {
    try {
        // 这里的common.loader,类型为URLClassLoader
        // 其parent classloader为 sun.misc.Launcher$AppClassLoader; 
        // 注意该ClassLoader是遵从ClassLoader的 上级委托接待机制 的
        // 就是这个loader负责加载Server和Context
        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, sharedLoader等于上面的commonLoader 
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

我们通过JVISUALVM来看看Tomcat运行时的环境变量
Tomcat系统参数

  1. sun.misc.Launcher$AppClassLoader 负责加载的是 System.getProerty(“java.class.path”)下的Class。
  2. 而common.loader则负责加载 ${common.loader}指示下的Class。
  3. 所以加载Server,Context还是common.loader生成的URLClassLoader。

2.2 Catalina.createStartDigester方法

  1. 该方法的作用的就是构建一个用于解析XML配置文件(默认是conf/server.xml)的Digester 组件。
  2. 为相关容器(Container)中的ClassLoader相关字段赋值:
    1. Catalina.SetParentClassLoaderRule。 负责为Engine设置 parentClassLoader(该字段定义在容器基类ContainerBase中)。
    2. 位于 HostRuleSet 中的CopyParentClassLoaderRule 。设置HostparentClassLoader(该字段定义在容器基类ContainerBase中)。
    3. 以上作为 parentClassLoader值来源的是加载Catalina的类 —— sun.misc.Launcher$AppClassLoader
    4. 对于Context 其覆写了继承自ContainerBasegetParentClassLoader方法。
  3. 注意该方法中还有这样一句相关代码digester.setUseContextClassLoader(true);,结合上面的 Thread.currentThread().setContextClassLoader(catalinaLoader); 我们可以得出结论:Server,Context等Class都是使用上面的catalinaLoader (URLClassLoader类型,且父ClassLoader为加载Catalina的类 —— sun.misc.Launcher$AppClassLoader)来进行加载。

3. StandardContext

  1. 关注的重点还是这个。
  2. 其覆写了继承自基类ContainerBasegetParentClassLoader方法。

    @Override
    public ClassLoader getParentClassLoader() {
        // 如果自身显式定义了, 则直接返回
        if (parentClassLoader != null)
            return (parentClassLoader);
        // 如果显式设置了privileged为true, 则返回加载自身的ClassLoader,也就是common.loader代表的URLClassLoader,注意该URLClassLoader的parent ClassLoader为sun.misc.Launcher$AppClassLoader。
        if (getPrivileged()) {
            return this.getClass().getClassLoader();
        } else if (parent != null) { // 如果parent不为null, 在StandardContext的情况下, 这是肯定满足的。所以这里的parent为Host,因此这里的返回值也是 sun.misc.Launcher$AppClassLoader
            return (parent.getParentClassLoader());
        }
        return (ClassLoader.getSystemClassLoader());
    }
  3. 其中涉及到的是Tomcat自身的三个相关类

    1. WebappLoader Loader
    2. WebappClassLoaderBase ClassLoader
    3. DefaultInstanceManager InstanceManager

我们首先要看的是startInternal方法

//
//  代码实在太多了,故进行了非必要的删减
//

// ------------------------------------ 初始化Loader
if (getLoader() == null) {
    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}

...

// Binding thread
// 将Loader关联的ClassLoader绑定到上下文
// 这里返回的应该是在Bootstrap.java的init()方法中绑定的catalinaLoader[默认加载路径在conf/catalina.properties的common.loader对应的值]
ClassLoader oldCCL = bindThread();

// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
    ((Lifecycle) loader).start();

// since the loader just started, the webapp classloader is now created.
// By calling unbindThread and bindThread in a row, we setup the current Thread CCL to be the webapp classloader
unbindThread(oldCCL); // 将common.loader的CCL绑定回当前线程
// setup the current Thread CCL to be the webapp classloader [说得已经比较清楚了,将Loader相关的CCL再次关联当前线程]
// 返回common.loader
oldCCL = bindThread();

...
// Unbinding thread
// 再次将common.loader的CCL绑定回当前线程
unbindThread(oldCCL);

// Binding thread
oldCCL = bindThread(); // 再再一次将Loader关联的ClassLoader绑定到上下文

// ------------------------------------ 初始化InstanceManager
if (getInstanceManager() == null) {
    javax.naming.Context context = null;
    if (isUseNaming() && getNamingContextListener() != null) {
        context = getNamingContextListener().getEnvContext();
    }
    Map<String, Map<String, String>> injectionMap = buildInjectionMap(
            getIgnoreAnnotations() ? new NamingResources(): getNamingResources());
    setInstanceManager(new DefaultInstanceManager(context,
            injectionMap, this, this.getClass().getClassLoader()));
    getServletContext().setAttribute(
            InstanceManager.class.getName(), getInstanceManager());
}

// 回调ServletContainerInitializer相关类(@HandlesTypes标注的实现类)
//使用StandardContext的InstanceManager字段来负责实例化了Servlet,Filter,Listener
// listenerStart() 
// filterStart() 
// loadOnStartup(findChildren()) 
// 相关的解释, 我们放到InstanceManager节点中
... 

// Unbinding thread
unbindThread(oldCCL); // 再再次将common.loader的CCL绑定回当前线程

...

// Close all JARs right away to avoid always opening a peak number of files on startup
if (getLoader() instanceof WebappLoader) {
    ((WebappLoader) getLoader()).closeJARs(true);
}

现在让我们来具体看看WebappClassLoaderBase,WebappLoader,InstanceManager

4. WebappLoader

  1. Loader接口的定义显示其并不负责实际的Class加载工作。
  2. 实现了Lifecycle接口, 也正是在 startInternal 方法实例化并配置WebappClassLoader
  3. Loader的主要作用还是进行相关配置; 对Class进行加载的逻辑还是在 WebappClassLoader

5. WebappClassLoader

  1. 绝大部分逻辑位于其基类WebappClassLoaderBase中。
  2. 主要关注的是覆写继承自基类的loadClass,findClass (可以翻阅下《深入分析Java Web技术内幕(修订版)》P157)。
  3. loadClassfindClass方法中的代码量比较大,有兴趣的读者可以自己去研究下。 涉及到
    1. 自定义在加载class时的各类规则.
    2. 缓存之前已加载过的class.
    3. 预加载某些class.

基类WebappClassLoaderBase中有一些字段值得关注
1. triggers , Servlet规范里要求相关类不允许被Servlet容器加载。 这里就是相关实现处了。 而我们经常会在控制台看到的那句话:validateJarFile(xxx) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: xxx , 正是位于其validateJarFile方法中 - 第3373行
2. packageTriggers 在进行 委托给父classloader之前,不允许自身先加载,类的集合。
3. 等等

扫描二维码关注公众号,回复: 44185 查看本文章

6. InstanceManager

  1. 实现类为 DefaultInstanceManager
  2. 几个关键字段classLoadercontainerClassLoader的赋值情况

    // ------------------- DefaultInstanceManager类的构造函数
    // catalinaContext实际类型为: StandardContext  , 
    // 所以这个classLoader应该就是WebappClassLoader
    classLoader = catalinaContext.getLoader().getClassLoader();
    
    privileged = catalinaContext.getPrivileged();
    
    // containerClassLoader就是加载StandardContext的ClassLoader, 结合之前的digester.setUseContextClassLoader(true); 
    // 所以这个containerClassLoader应该就是common.loader(即URLClassLoader,其praent Classloader是sun.misc.Launcher$AppClassLoader)
    this.containerClassLoader = containerClassLoader;
  3. 就是这个InstanceManager类负责加载所有的Servlet,Filter,Listener等。

    1. StandardContext.listenerStart()。这里可以发现使用了StandardContextInstanceManager字段来负责实例化了Listener
    2. StandardContext.filterStart() 。这里最终可以追踪到ApplicationFilterConfig类的构造函数, 其也是使用了StandardContextInstanceManager字段来负责实例化了Filter
    3. loadOnStartup(findChildren()) 。这里可以追踪到StandardWrapperloadServlet方法, 其正是使用了StandardContextInstanceManager字段来负责实例化了Servlet

本类中负责加载Servlet,Filter,Listener的逻辑为其内部的 loadClass方法,

// ----------------------- DefaultInstanceManager.loadClass 
/*
    大概逻辑是
        1. 使用containerClassLoader进行加载   (这个containerClassLoader应该就是common.loader(即URLClassLoader,其praent Classloader是sun.misc.Launcher$AppClassLoader))
            1. "org.apache.catalina" package内的Class.
            2. ContainerServlet的实现类
        2. 剩下的都使用classLoader来进行加载 (这个classLoader应该就是WebappClassLoader )
        3. 再结合之前的测试, 可以得出结论: 我们自定义的类都是由WebappClassLoader来加载的.
*/
protected Class<?> loadClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
    // 特定的 package 内
    if (className.startsWith("org.apache.catalina")) {
        return containerClassLoader.loadClass(className);
    }
    try {
        Class<?> clazz = containerClassLoader.loadClass(className);
        // 关于这个ContainerServlet, 本篇文章就不涉及了
        if (ContainerServlet.class.isAssignableFrom(clazz)) {
            return clazz;
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
    }
    return classLoader.loadClass(className);
}

7. 总结

  1. 默认情况下,Bootstrap类中的类级字段catalinaLoadersharedLoadercommonLoader为同一个URLClassLoader,而且该URLClassLoader的Parent ClassLoader正是sun.misc.Launcher$AppClassLoader
  2. 加载Serverconfig/server.xml中配置的组件的ClassLoader为catalinaLoader字段指示的URLClassLoader。 也就是在Catalina的createStartDigester方法中设置的digester.setUseContextClassLoader(true);注意这里是遵从父优先的
  3. Host,Engine的parentClassLoader字段,其赋值是通过digester组件来完成的,其值为加载Catalina的ClassLoader,即sun.misc.Launcher$AppClassLoader
  4. 对Server,Context进行加载的ClassLoader为common.loader (即java.net.URLClassLoader), 而其parent Classloader为sun.misc.Launcher$AppClassLoader(即:加载Catalina的类).
  5. Context使用InstanceManager来完成了实例化工作。 Servlet, Filter, Listener就是借助于它来完成实例化操作的,也是使用它来对Servlet, Filter, Listener的相关Class进行加载的。

    1. common.loader指代的URLClassLoader加载”org.apache.catalina” package下的类,以及ContainerServlet的实现类。
    2. 剩下的都使用classLoader来进行加载 (这个classLoader应该就是WebappClassLoader )。 这也是我们开发过程中最常接触到的ClassLoader,我们自定义的类都是由WebappClassLoader来加载的,注意该WebappClassLoader的parent ClassLoader由StandardContext.getParentClassLoader来决定
  6. 实际的加载类的工作最终是交给了WebappClassLoader了,大部分工作逻辑被基类WebappClassLoaderBase来完成, 而Loader接口基本只是做了外围的配置工作.

  1. 《深入分析Java Web技术内幕(修订版)》 P168
  2. 《How Tomcat Works》 P157
  3. Tomcat类加载器以及应用间class隔离与共享

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/79858561