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未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。