1.概述
下面是几个例子,展示前面学到的类加载器相关知识在实际开发中的应用。
2.案例分析
总共四个案例,两个类加载案例,两个字节码案例。
2.1 Tomcat 的类加载器架构
主流的 Java Web 服务器,如 Tomcat、Jetty、WebLogic、webSphere 等都实现了自定义的类加载器,而且一般都不止一个类加载器。因为一个功能健全的 Web 服务器要解决以下问题:
- 部署在同一个 Java Web 服务器上的两个 Web 应用程序所使用的 Java 类库可以实现互相隔离。比如两个应用程序使用了不同版本的相同类库,每个应用程序的使用的类库必须要进行隔离。
- 部署在同一 Java Web 服务器上的两个 Web 应用程序所使用的 Java 类库可以共享。比如同一个 Java Web 服务器上部署了 10 个 Spring 的 Web 应用程序,如果类库不能共享,在使用时类库被加载到服务器内存中,虚拟机的方法区就会很容易出现过度膨胀的风险。
- Java Web 服务器有可能本身也是 Java 实现的,所以它需要保证自身不被部署的 Web 应用程下·序影响。所以一般来说 Java Web 服务器所使用的类库应该与应用程序类库互相独立。
- 支持 JSP 引用的 Web 服务器,十有八九都需要支持 HotSwap 功能。
为此,Tomcat 在目录结构中,可以设置 3 组目录(/common/*、/server/*、/shared/*)用于存放 Java 类库。另外还加上 Web 应用程序自身的 /WEB-INF/* 目录,一共 4 组目录。每一组的独立含义是:
- 放置在 /common 目录中。类库可以被 Tomcat 和所有的 Web 应用程序共同使用。
- 放置在 /server 目录中。类库可以被 Tomcat 使用,对所有的 Web 应用程序不可见。
- 放置在 /shared 目录中。类库可以被所有的 Web 应用程序共同使用,但对 Tomcat 自己不可见。
- 放置在 /WEB-INF 目录中。类库仅可以被该 Web 应用程序使用,对 Tomcat 和其他的 Web 应用程序都不可见。
为了支持这套目录结构,并且对目录里面的类库进行加载和隔离,Tomcat 自定义了多个类加载器,这些类加载器按照经典的双亲委派模式来实现,如图:
最上面 3 个类加载器是 JDK 9 之前默认提供的类加载器。下面的 5 个类加载器是 Tomcat 自定义的类加载器。
Common 类加载器加载 /common/* 目录下的类库,Catalina 类加载(也称 Server 类加载器)加载 /server/* 目录下的类库,Shared 类加载器加载 /shared/* 目录下的类库, Webapp 类加载器加载 /WebApp/WEB-INF/* 中的 Java 类库。每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 JasperLoader 类加载器。
从上面的图片可以看出,Common 类加载器加载的类可以被 Catalina 类加载器和 Shared 类加载器使用,而 Catalina 类加载器和 Shared 类加载自己加载的类是互相隔离的。WebApp 类加载器可以使用 Shared 类加载器加载的类,但是各个 WebApp 类加载器实例之间又互相隔离。而 JasperLoader 的加载范围仅仅是这个 JPS 文件所编译出来的那一个 Class 文件。当服务器检测到 JSP 文件被修改时,会替换掉当前的 JasperLoader 的实例,并通过再建立一个新的 JSP 类加载器来实现 JSP 文件的 HotSwap 功能。
上面这个类加载机构是在 Tomcat 6 以前的默认类加载器结构。在 Tomcat 6 及之后的版本简化了默认目录结构。只有在指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 项后,才会真正建立 Catalina 类加载器和 Shared 类加载器的实例。否则用到这两个类加载器的地方都会用 Common 类加载器的实例代替。由于默认没有设置这两个 loader 项,所以 Tomcat 6 之后也顺利成章的把 /common、/server、/shared 3 个目录默认合并到一个 /lib 目录。这是为了简化大多数的部署场景所做的一项易用性改进,如果默认设置不能满足需求,可以通过两个 loader 来启用原来的完整的类加载器架构。