• Tomcat与spring的类加载器案例


    接下来将介绍《深入理解java虚拟机》一书中的案例,并解答它所提出的问题。(部分类容来自于书中原文)

    Tomcat中的类加载器
    在Tomcat目录结构中,有三组目录(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/*”,把java类库放置在这些目录中的含义分别是:

    放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
    放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
    放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
    放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
    为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示

     

    灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

    从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

    Spring加载问题
    Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

    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;
    }
    }

    解答
    答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~

    源码分析
    有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

    具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用TCCL来解决所有可能面临的情况。

    总结
    通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

    当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
    当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

    不积跬步,无以至千里;不积小流,无以成江海。
  • 相关阅读:
    GPUImage原理
    iOS开发技巧
    iOS如何做出炫酷的翻页效果
    iOS开发CAAnimation详解
    iOS开发CAAnimation类动画, CATransition动画
    iOS开发UUIView动画方法总结
    iOS开发NS_ENUM和NS_OPTIONS区别
    iOS开发SDWebImage源码解析之SDWebImageManager的注解
    iOS开发SDWebImageOptions理解
    Swift-重写(Override)
  • 原文地址:https://www.cnblogs.com/CCTVCHCH/p/14995380.html
Copyright © 2020-2023  润新知