25 | Context容器(中):Tomcat如何隔离Web应用?
我在专栏上一期提到,Tomcat 通过自定义类加载器 WebAppClassLoader 打破了双亲委托机制,具体来说就是重写了 JVM 的类加载器 ClassLoader 的 findClass 方法和 loadClass 方法,这样做的目的是优先加载 Web 应用目录下的类。除此之外,你觉得 Tomcat 的类加载器还需要完成哪些需求呢?或者说在设计上还需要考虑哪些方面?
我们知道,Tomcat 作为 Servlet 容器,它负责加载我们的 Servlet 类,此外它还负责加载 Servlet 所依赖的 JAR 包。并且 Tomcat 本身也是也是一个 Java 程序,因此它需要加载自己的类和依赖的 JAR 包。首先让我们思考这一下这几个问题:
-
假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。 -
假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。 -
跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。
在了解了 Tomcat 的类加载器在设计时要考虑的这些问题以后,今天我们主要来学习一下 Tomcat 是如何通过设计多层次的类加载器来解决这些问题的。
Tomcat 类加载器的层次结构
为了解决这些问题,Tomcat 设计了类加载器的层次结构,它们的关系如下图所示。下面我来详细解释为什么要设计这些类加载器,告诉你它们是怎么解决上面这些问题的。
我们先来看第 1 个问题,假如我们使用 JVM 默认 AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。
因此 Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。
SharedClassLoader
我们再来看第 2 个问题,本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。
CatalinaClassloader
我们来看第 3 个问题,如何隔离 Tomcat 本身的类和 Web 应用的类?我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?
CommonClassLoader
老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。
Spring 的加载问题
在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring 是通过调用Class.forName
来加载业务类的,我们来看一下 forName 的源码:
public static Class<?> forName(String className) {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
复制代码
可以看到在 forName 的函数里,会用调用者也就是 Spring 的加载器去加载业务类。
我在前面提到,Web 应用之间共享的 JAR 包可以交给 SharedClassLoader 来加载,从而避免重复加载。Spring 作为共享的第三方 JAR 包,它本身是由 SharedClassLoader 来加载的,Spring 又要去加载业务类,按照前面那条规则,加载 Spring 的类加载器也会用来加载业务类,但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,这该怎么办呢?
于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此 Tomcat 为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。Spring 取线程上下文加载的代码如下:
cl = Thread.currentThread().getContextClassLoader();
复制代码
本期精华
今天我介绍了 JVM 的类加载器原理并剖析了源码,以及 Tomcat 的类加载器的设计。重点需要你理解的是,Tomcat 的 Context 组件为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,由于不同类加载器实例加载的类是互相隔离的,因此达到了隔离 Web 应用的目的,同时通过 CommonClassLoader 等父加载器来共享第三方 JAR 包。而共享的第三方 JAR 包怎么加载特定 Web 应用的类呢?可以通过设置线程上下文加载器来解决。而作为 Java 程序员,我们应该牢记的是:
-
每个 Web 应用自己的 Java 类文件和依赖的 JAR 包,分别放在 WEB-INF/classes
和WEB-INF/lib
目录下面。 -
多个应用共享的 Java 类文件和 JAR 包,分别放在 Web 容器指定的共享目录下。 -
当出现 ClassNotFound 错误时,应该检查你的类加载器是否正确。
线程上下文加载器不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的,感兴趣的话可以深入了解一下。
课后思考
在 StandardContext 的启动方法里,会将当前线程的上下文加载器设置为 WebAppClassLoader。
originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);
复制代码
在启动方法结束的时候,还会恢复线程的上下文加载器:
Thread.currentThread().setContextClassLoader(originalClassLoader);
复制代码
这是为什么呢?
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
免责声明
本技术栈所有广告内容,本网站不做担保,出现任何问题您自己承担。