Tomcat ClassLoader

所属文集:ClassLoader串烧


Tomcat 的类加载器

Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。

findClass 方法

先看 findClass 方法的实现,去掉了一些细节:

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. 如果在本地目录没有找到,交给父类的findClass查找,不是父加载器,只是父类的方法
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
       }
    
    //3. 如果父类也没找到,抛出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }
 
    return clazz;
}

在 findClass 方法里,主要有三个步骤:

先在 Web 应用本地目录下查找要加载的类。
如果没有找到,调用super.findClass()

<深入拆解Tomcat & Jetty> 里解释为 交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
这句话可疑,super.findClass()为什么会是AppClassLoader的findClass;父类是URLClassLoader
父加载器是系统类加载器也有问题,WebAppClassLoader的父加载器应该是SharedClassLoader.

如何父加载器也没找到这个类,抛出 ClassNotFound 异常。
loadClass 方法

接着我们再来看 Tomcat 类加载器的 loadClass 方法的实现,同样我也去掉了一些细节:

public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class clazz = null;
        //1. 先在本地 cache 查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) return clazz;

        //2. 从JVM的 cache 中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null)  return clazz;
 
        // 3. 尝试用 ExtClassLoader 类加载器类加载,
        ClassLoader javaseLoader = getJavaseClassLoader();
        clazz = javaseLoader.loadClass(name);
        if (clazz != null) return clazz;

        // 4. 尝试在本地目录搜索 class 并加载
        clazz = findClass(name);
        if (clazz != null)  return clazz;

        // 5. 尝试用父类加载器 (也就是SharedClassLoader) 来加载
        clazz = Class.forName(name, false, parent);
        if (clazz != null) return clazz;
    }
    //6. 上述过程都加载失败,抛出异常
    throw new ClassNotFoundException(name);
}

loadClass 方法稍微复杂一点,主要有六个步骤:

  1. 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。
  2. 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
  3. 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
  4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
  5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由父加载器去加载,委派加载的顺序为SharedClassLoader-> CommonClassLoader->AppClassLoader。
  6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。
Tomcat ClassLoader_第1张图片
image.png

从上面的过程我们可以看到,Tomcat 的类加载器打破了双亲委托机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。那为什么不先用系统类加载器 AppClassLoader 去加载?很显然,如果是这样的话,那就变成双亲委托机制了,这就是 Tomcat 类加载器的巧妙之处。

自定义类加载器
Tomcat ClassLoader_第2张图片
image.png

比如可以把多个项目共享的jar包放到${CATALINA_HOME}/shared目录下,让sharedclassloader来加载,并且是所有context的web应用共享的,而都有的放在web路径下,先让扩展类加载器加载,避免覆盖jre中的类,再让自定义的web加载器来加载独有的类,最后加载让应用加载器加载扩展类加载器和自定义加载器加载不到的类.

如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么被 CommonClassLoader 或 SharedClassLoader 加载的 Spring 如何访问并不在其加载范围的用户程序呢?

spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean


tomcat 的类加载器的源码从3个主线来看

Tomcat ClassLoader_第3张图片
image.png
  • 1.common catalina shared classLoader 的创建以及委托关系的确立
  • 2.WebAppClassLoader 的创建,以及委托关系的确立
  • 3.线程上下文加载器的设置,以及spring对线程上下文类加载器的使用.

主线1
Bootstrap类作为入口,在main方法中做个3件事
1.创建 CommonClassLoader,CatalinaClassLoader ,SharedClassLoader.

  1. 设置线程上下文类加载器为 catalinaLoader
  2. 反射方式创建 org.apache.catalina.startup.Catalina实例,并调用其setParentClassLoader方法,值设置为 sharedClassLoader.

主线2
StandardContext 的startInternal
1.创建WebappLoader,参数为SharedClassLoader (从Catalina#getParentClassLoader()获得)
2.调用WebappLoader的start()方法,创建WebAppClassLoader,指定父加载器为SharedClassLoader
3.获取并暂存原始线程上下文类加载器,设置新的线程类加载器.
try{
//....一些逻辑(主线3)
}finally{
恢复原始线程上下文类加载器
}

主线3
web.xml中配置的spring的监听器执行,
创先Spring容器,指定线程上下文类加载器为类加载器.
spring在getBean的时候,用forName的方式通过线程上下文类加载器 来加载类.

主线1 : common catalina shared classLoader 的创建以及委托关系的确立

通过 Bootstrap.java 类main方法中调用了init 方法来创建加载器,
将 catalina classLoader 设置线程上下文加载器.

init(){
    //创建common,catalina,shared 三个类加载器
    initClassLoaders();
    // 上下文类加载器设置为 catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    //反射加载 Catalina类
    catalinaLoader.loadClass ("org.apache.catalina.startup.Catalina");
    // (2)  实例化 Catalina
    Object startupInstance = startupClass.newInstance();
    ....
    // 反射调用 Catalina的setParentClassLoader 方法,设置为  sharedLoader
    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);
}

initClassLoaders 方法中 创建好common,catalina,shared 三个ClassLoader

private void initClassLoaders() {
   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);
    ...
}

若非额外配置,common,catalina,shared 三个类加载器 是同一个,都是common classLoader.

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
    //默认情况下common,catalina,shared 三个类加载器 是同一个,都是common cl
    //除非在配置文件中独立配置.
    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;
    ...
}

主线2 : WebAppClassLoader 的创建

StandardContext#startInternal()方法中

//getParentClassLoader() 内部 从Catalina#getParentClassLoader()获得SharedClassLoader 
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
//内部创建了WebAppClassLoader
setLoader(webappLoader);

// Binding thread
ClassLoader oldCCL = bindThread();
try{

}
finally {
            // Unbinding thread
            unbindThread(oldCCL);
        }

setLoader(webappLoader);内部创建了WebAppClassLoader

setLoader(){
    //这个逻辑,模板方法的模式;会调用到 WebApploader的startInternal
    ((Lifecycle) loader).start();
}

WebApploader#startInternal(){
    //创建类加载器关键方法
    classLoader = createClassLoader();
}

反射方式创建 org.apache.catalina.loader.WebappClassLoader,parent就是Shared ClassLoader

private WebappClassLoaderBase createClassLoader()
    throws Exception {
    Class clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

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

    return classLoader;
}
---

主线3
Tomcat ClassLoader_第4张图片
image.png

看初始化spring容器的代码 initWebApplicationContext
如何用org.springframework.web.context.ContextLoader类 来获取和使用线程上下文类加载器来装载bean

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    try {
        // 创建WebApplicationContext
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        // 将其保存到该webapp的servletContext中     
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
        // 获取线程上下文类加载器,默认为WebAppClassLoader
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        // 如果spring的jar包放在每个webapp自己的目录中
        // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
            // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
            currentContextPerThread.put(ccl, this.context);
        }
        
        return this.context;
    }
    catch (RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (Error err) {
        logger.error("Context initialization failed", err);
        throw err;
    }
}
主线3 spring容器创建与类加载.

创建应用上下文类加载器
createWebApplicationContext(servletContext);

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
   //beanFactory此时来指定ClassLoader,后续获取Bean的时候就用它
   beanFactory.setBeanClassLoader(this.getClassLoader());
}

this.getClassLoader() 的值是什么 ,代码在org.springframework.core.io.DefaultResourceLoader

    public ClassLoader getClassLoader() {
        return this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader();
    }

继续看 ClassUtils.getDefaultClassLoader()的实现 ,可以发现会返回线程上下文类加载器.

    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
      
        try {
            //如果线程上下文类加载不是null 就返回线程上下文类加载器
            cl = Thread.currentThread().getContextClassLoader();
        } catch (Throwable var3) {
        }

        if (cl == null) {
            //如果线程上下文类加载器是null 就用 当前类加载器
            cl = ClassUtils.class.getClassLoader();
            if (cl == null) {
                try {
                    //兜底用 系统类加载器.
                    cl = ClassLoader.getSystemClassLoader();
                } catch (Throwable var2) {
                }
            }
        }

        return cl;
    }

Spring的getBean方法

  1. org.springframework.beans.factory.support.AbstractBeanFactory#doResolveBeanClass 中 获取类加载器,就是线程上下文类加载器 tccl
  2. org.springframework.beans.factory.support.AbstractBeanDefinition#resolveBeanClass
    Class beanClass = Class.forName(类名,false,线程上下文类加载器);

3.拿到类型进行实例化.


参考链接:
真正理解线程上下文类加载器(多案例分析)
深入拆解Tomcat & Jetty
细说Tomcat如何打破双亲委派
细说Tomcat如何打破双亲委派-续

违反ClassLoader双亲委派机制三部曲

你可能感兴趣的:(Tomcat ClassLoader)