24.2. Apusic的Classloader体系

在上一节,我们了解了基本的Classloader层次结构模型,知道了Bootstrap Classloader、System Classloader的职责,还知道可以通过自定义Classloader来完成特定的装载任务,除此之外,我们还了解了什么是Caller Classloader和线程上下文Classloader。下面,我们就可以根据这些基本的Classloader概念去看看Apusic Classloader体系是长什么样的了。

24.2.1. JavaEE应用对Classloader的要求

Apusic应用服务器本身运行需要的类都在CLASSPATH中,由System Classloader加载。在上一节中,我们提到Apusic应用服务器中定义了若干个专有的Classloader,负责装载部署在Apusic中的JavaEE应用中的类和资源。Apusic为何要额外的去自定义Classloader呢?把应用需要的类和资源都放在CLASSPATH中,System Classloader不也可以加载这些类吗?要回答这些问题,我们先考虑一下下面两个简单的需求:

  • 不同的应用中,可能有同名的资源文件或类,它们在各自应用中有不同的行为或语义。

  • 应用发生变化的时候,例如改了Jsp或者JavaBean,在不重启服务器甚至不重启应用的情况下,需要立即看到修改的效果。

我们前面提到过在一个JVM中一个类的唯一标识,当不能改变类的包名和类名的情况下,除非 Classloader的实例发生变化,才有可能实现对一个类的再次加载。显然,在只有System Classloader的情况下,无法满足上面两个简单的需求。这是因为在运行期,我们无法重新创建System Classloader的实例,也没办法让它装载一个已经装载过的类

对于第一个需求,我们可以对不同的应用中的类和资源进行隔离加载,这就需要为每个应用使用不同的Classloader实例;对于第二个需求,当Jsp或JavaBean发生变化时,我们需要把原来装载Jsp的Classloader销毁掉,创建一个新的Classloader实例,并让它去装载修改后的类,因此,要专门定义一个Classloader去负责装载Jsp、JavaBean,使得在重新创建Classloader时,受影响的范围尽可能的小。

24.2.2. Apusic的Classloader和它们的层次结构

Apusic为装载JavaEE应用中的类定义了EJBClassLoader和ServletClassLoader这两个主要的Classloader。假设一个JavaEE应用的结构如下:

  • EJBClassLoader

    每个JavaEE应用都有一个EJBClassLoader,用于装载EJB module和公共类。上图中的ejbjarA.jar、ejbjarB.jar、util.jar以及app.ear我们可以看成是一个jar文件,也可以看成是一个目录,它们里边的类和文件都由同一个EJBClassLoader实例装载,因此,同一个JavaEE应用中的EJB module和公共类是相互可见的。

    不同的应用,其EJBClassLoader实例也不同,且每个EJBClassLoader实例间是平级关系,所以不同应用中的类是相互不可见的。

  • ServletClassLoader

    在Apusic应用服务器中,每个Web module都有一个ServletClassLoader,用于装载Web module中的类和资源文件。所以,每个JavaEE应用中都可能有一个或多个ServletClassLoader,例如上图表示的JavaEE应用就有两个ServletClassLoader,它们是平级关系,所以Web module中的类相互不可见。对于ServletClassLoader,还有一些特殊的行为,将在下一节介绍。

  • 层次结构

    通过以上的介绍,我们可以知道,Apusic应用服务器启动后,假设其中部署了两个应用,分别是appA.ear和appB.ear,那么其Classloader层次结构可表现为:

    其中,我们可以看到,EJBClassLoader是ServletClassLoader的父,由ejbClassLoaderA装载的类和文件,对于servletClassLoaderA和servletClassLoaderB装载的类都是可见的。也就是说,同一个应用中的任意Web Module的类(即位于WEB-INF/classes、WEB-INF/lib中的类),都可以使用ejb jar或util jar中的类。

    但对于上图中ejbClassLoaderA装载的类,servletClassLoaderC是看不见的,它们属于不同的应用。

24.2.3. ServletClassLoader的特性

在前面几节,我们提到过Apusic对于Web module中的类,包括jsp(最终被应用服务器解析成servlet并编译成Java类)、WEB-INF/classes和WEB-INF/lib里边的class和资源文件,专门定义一个ServletClassLoader进行加载是为了满足类似开发期中类的动态加载、不同Module间类的隔离等的需要。Apusic应用服务器在Classloader体系中做了充分的考虑以降低Web应用开发的复杂性及提升应用服务器的易用性。下面将介绍Apusic的ServletClassLoader的行为特性:

24.2.3.1. 类的动态加载

在Apusic检测到jsp或WEB-INF/classes目录下的类的更新后,会重新加载修改过的类。对于用户来说,不需要做任何事情,在修改完后马上调用该类就可以看到刚刚做的更新。

考虑到运行期和开发期的要求不同,运行期类和资源文件不会频繁更新,因此,在运行期,不需要频繁检测类文件是否已经更新,可通过配置apusic.conf中的ServletReloadCheckInterval属性值来修改检测时间。当值小于”0”时,不检测。此值默认是3,即每3秒中检测一次。

24.2.3.2. ServletClassLoader的多层结构

ServletClassLoader是一层壳,根据配置的不同策略,委托给不同的Classloader执行装载任务。Servlet Classloader的装载行为有两种策略,可通过配置进行指定,配置有两种方式:

  • 在web.xml中增加Context Parameter

    <context-param>
        <param-name>com.apusic.web.ServletClassLoaderDelegate</param-name>
        <param-value>Separated</param-value>
    </context-param>

    这样的配置有效范围只有当前应用。如果修改的是$DOMAIN_HOME/config/web.xml下的配置,则适用所有应用。

  • 通过VM参数指定

    -Dcom.apusic.web.ServletClassLoaderDelegate=Separated

    这种系统属性配置,所有的应用都生效。

ServletClassLoader的两种装载策略分别通过com.apusic.web.ServletClassLoaderDelegate的两个值来指定:

  • Composite

    默认值,表示ServletClassLoader的行为委托给了两层Classloader,一层叫CompositeClassLoader,它的父Classloader是EJBClassLoader,它负责WEB-INF/lib和WEB-INF/classes目录下的类和资源的装载,其中,如果在WEB-INF/lib和WEB-INF/classes下有同名的类或资源,WEB-INF/classes下的类将被优先装载;另一层是JSPClassLoader,它的父Classloader是CompositeClassLoader,它负责装载解析编译后的JSP。

  • Separated

    表示ServletClassLoader的行为委托给了三层Classloader,跟上一种策略不同的是WEB-INF/lib下类和WEB-INF/classes目录下的类和资源由不同的Classloader装载,前者叫StaticClassLoader,它的父是EJBClassLoader;后者我们称为ReloadableClassLoader,它的父是StaticClassLoader,子是JSPClassLoader。根据前面对Classloader父子关系的描述,我们可以知道,WEB-INF/lib下的类看不见WEB-INF/classes下的类,而WEB-INF/classes下的类可以看见WEB-INF/lib下的类。考虑到客户应用中,资源文件一般放在WEB-INF/classes目录中,因此,如果在WEB-INF/lib和WEB-INF/classes下有同名的资源文件,仍然是WEB-INF/classes下的资源优先装载。

如果客户应用系统中,WEB-INF/lib下的类会引用WEB-INF/classes下的类或资源,或者认为WEB-INF/classes下的类应该优先于WEB-INF/lib下的类装载,我们建议使用Composite,即默认的策略。

如果考虑到在开发期WEB-INF/lib下的类或文件不会频繁更新,为了避免检测范围太大而导致的检测时间过长,不扫描WEB-INF/lib下的更新(即此目录下的类只被装载一次,如果有更新,则需要重启应用才能生效),或者认为WEB-INF/lib下的类应该优先于WEB-INF/classes下的类装载时,可采用Separated策略。

24.2.3.3. Session中对象的类动态装载

如果session中保存的对象实例的类发生了更改,且类的签名未发生变化,那么对象实例的类型信息将被标识为新装载的类,从Session中取出对象后,它的行为按更新后的类执行。但如果类的签名发生了变化,那么此session中的对象实例将被丢弃。

24.2.3.4. 类装载的Web优先策略

在默认情况下,ServletClassLoader遵循大多数Classloader的装载行为,如“Classloader的基本概念”一节描述的那样,会按顺序向上询问其所有父节点装载,如果父没装载到,才会由自身进行加载。这种默认的Java类装载机制有时也会碰到麻烦,比如WEB-INF/classes中有某个类,在系统Classpath中有这个类的另一个版本,Classloader默认的装载行为决定了系统Classpath中的类会被优先加载。如果我们期望WEB-INF/classes中的类要优先加载,Apusic的Servlet Classloader提供了机会,可以通过配置系统属性或者在web.xml中增加Context Parameter来达到此目的:

  • 在web.xml中增加Context Parameter

    <context-param>
        <param-name>apusic.prefer.war.classes</param-name>
        <param-value>true</param-value>
    </context-param>

    这样的配置有效范围只有当前应用。如果修改的是$DOMAIN_HOME/config/web.xml下的配置,则适用所有应用。

  • 通过VM参数指定

    -Dapusic.prefer.war.classes=true

    这种系统属性配置,所有的应用都生效。

24.2.4. 类装载查看服务

Apusic应用服务器提供了类装载查看服务,通过此服务,可以查找指定的类是由哪一层的Classloader装载的,类文件路径等信息,从而可以协助排查一些跟类装载相关的问题。类装载查看服务的相关配置段如下:

...
<SERVICE
    CLASS="com.apusic.util.ClassLoaderViewer"
    >
</SERVICE>
...

可以通过Admin Console上提供的类加载器来访问类装载查看服务,如何使用类加载器请参考Admin Console文档。