You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.2 KiB

25 | Context容器Tomcat如何隔离Web应用

我在专栏上一期提到Tomcat通过自定义类加载器WebAppClassLoader打破了双亲委托机制具体来说就是重写了JVM的类加载器ClassLoader的findClass方法和loadClass方法这样做的目的是优先加载Web应用目录下的类。除此之外你觉得Tomcat的类加载器还需要完成哪些需求呢或者说在设计上还需要考虑哪些方面

我们知道Tomcat作为Servlet容器它负责加载我们的Servlet类此外它还负责加载Servlet所依赖的JAR包。并且Tomcat本身也是一个Java程序因此它需要加载自己的类和依赖的JAR包。首先让我们思考这一下这几个问题

  1. 假如我们在Tomcat中运行了两个Web应用程序两个Web应用中有同名的Servlet但是功能不同Tomcat需要同时加载和管理这两个同名的Servlet类保证它们不会冲突因此Web应用之间的类需要隔离。
  2. 假如两个Web应用都依赖同一个第三方的JAR包比如Spring那Spring的JAR包被加载到内存后Tomcat要保证这两个Web应用能够共享也就是说Spring的JAR包只被加载一次否则随着依赖的第三方JAR包增多JVM的内存会膨胀。
  3. 跟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应用创建一个WebAppClassLoader类加载器并在启动Web应用的线程里设置线程上下文加载器这样Spring在启动时就将线程上下文加载器取出来用来加载Bean。Spring取线程上下文加载的代码如下

cl = Thread.currentThread().getContextClassLoader();

本期精华

今天我介绍了JVM的类加载器原理并剖析了源码以及Tomcat的类加载器的设计。重点需要你理解的是Tomcat的Context组件为每个Web应用创建一个WebAppClassLoader类加载器由于不同类加载器实例加载的类是互相隔离的因此达到了隔离Web应用的目的同时通过CommonClassLoader等父加载器来共享第三方JAR包。而共享的第三方JAR包怎么加载特定Web应用的类呢可以通过设置线程上下文加载器来解决。而作为Java程序员我们应该牢记的是

  • 每个Web应用自己的Java类文件和依赖的JAR包分别放在WEB-INF/classesWEB-INF/lib目录下面。
  • 多个应用共享的Java类文件和JAR包分别放在Web容器指定的共享目录下。
  • 当出现ClassNotFound错误时应该检查你的类加载器是否正确。

线程上下文加载器不仅仅可以用在Tomcat和Spring类加载的场景里核心框架类需要加载具体实现类时都可以用到它比如我们熟悉的JDBC就是通过上下文类加载器来加载不同的数据库驱动的感兴趣的话可以深入了解一下。

课后思考

在StandardContext的启动方法里会将当前线程的上下文加载器设置为WebAppClassLoader。

originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);

在启动方法结束的时候,还会恢复线程的上下文加载器:

Thread.currentThread().setContextClassLoader(originalClassLoader);

这是为什么呢?

不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。