Day692.Tomcat如何隔离Web应用 -深入拆解 Tomcat & Jetty
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day692.Tomcat如何隔离Web应用 -深入拆解 Tomcat & Jetty相关的知识,希望对你有一定的参考价值。
Tomcat如何隔离Web应用
Hi,阿昌
来也,今天针对学习记录的内容是关于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 设计了类加载器的层次结构,它们的关系如下图所示。
下面我来详细解释为什么要设计这些类加载器,告诉你它们是怎么解决上面这些问题的。
我们先来看第 1 个问题,假如我们使用 JVM 默认 AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。
因此 Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。
我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例
。
这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同
。
这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。
1、SharedClassLoader
我们再来看第 2 个问题,本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。
在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。
因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。
如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类
,之后返回给 WebAppClassLoader,这样共享的问题
就解决了。
2、CatalinaClassLoader
我们来看第 3 个问题,如何隔离 Tomcat 本身的类和 Web 应用的类?
我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。
兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。
基于此 Tomcat 又设计一个类加载器 CatalinaClassLoader,专门来加载 Tomcat 自身的类
。
这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?
3、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 应用创建一个 WebAppClassLoader 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。
Spring 取线程上下文加载的代码如下:
cl = Thread.currentThread().getContextClassLoader();
三、总结
Tomcat 的 Context 组件为每个 Web 应用创建一个 WebAppClassLoader 类加载器
,由于不同类加载器实例加载的类是互相隔离的,因此达到了隔离 Web 应用的目的,同时通过 CommonClassLoader 等父加载器来共享第三方 JAR 包
。
而共享的第三方 JAR 包怎么加载特定 Web 应用的类呢?
可以通过设置线程上下文加载器来解决。
作为 Java 程序员,应该牢记的是:
- 每个 Web 应用自己的 Java 类文件和依赖的 JAR 包,分别放在WEB-INF/classes和WEB-INF/lib目录下面。
- 多个应用共享的 Java 类文件和 JAR 包,分别放在 Web 容器指定的共享目录下。
- 当出现 ClassNotFound 错误时,应该检查你的类加载器是否正确。
线程上下文加载器
不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的。
蓝色部分就是JVM的类加载器分层结构。Tomcat在JVM的基础上又加了三层。
线程上下文类加载器,打破了双亲委托机制。
上下文加载器是比如说我在加载spring的线程设置为webappclassloader那么就算spring的jar是由shared classloader加载的,那么spring加载的过程中也是由webappclassloader来加载,而用完设置回去,是因为我只需要跨classloader的时候才需要线程上下文加载器
- CommonClassLoader对应/common/*
- CatalinaClassLoader对应 /server/*
- SharedClassLoader对应 /shared/*
- WebAppClassloader对应 /webapps//WEB-INF/*目录
四、思考
在 StandardContext 的启动方法里,会将当前线程的上下文加载器设置为 WebAppClassLoader。
originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);
在启动方法结束的时候,还会恢复线程的上下文加载器:
Thread.currentThread().setContextClassLoader(originalClassLoader);
这是为什么呢?
优先加载web应用的类,当加载完了再改回原来的。
以上是关于Day692.Tomcat如何隔离Web应用 -深入拆解 Tomcat & Jetty的主要内容,如果未能解决你的问题,请参考以下文章
25 | Context容器(中):Tomcat如何隔离Web应用?
无法隔离 Scala 批量数据加载应用程序中的 JDBC 内存泄漏