abstract:本文截取IBM 红皮书《WebSphere Application Server V6.1:系统管理与配置》的第12 章节内容,详细阐述了类加载器的基本概念和使用,以及如何客户化类加载器,通过例子阐述了影响类加载的选项的使用。
keywords:WebSphereApplication Server,类加载器
常常有客户询问我,WAS 中的类加载策略怎么理解。这不是一句话能够说清楚的。所以截取了IBM 红皮书《WebSphere Application Server V6.1:系统管理与配置》的第 12 章节,详细阐述了类加载器的基本概念和使用,以及如何客户化类加载器。
理解Java以及WebSphere的类加载策略对打包和部署J2EE程序至关重要。当启动应用程序时,如果类加载失败,就会出现类加载的异常,比如ClassNotFoundException。本文解释了类加载以及为了应用程序的需要如何客户化WebSphere 类加载。
后简称WebSphere Application Server V6.1 为WAS 6.1
12.1 Java类加载器介绍
类加载器负责把类加载到Java虚拟机(JVM)中。指定类的名称,类加载器就会定位这个类的定义,每一个Java类必须由类加载器加载。
当启动JVM的时候,可以使用三个类加载器:引导(bootstrap)类加载器、扩展(extensions)类加载器、应用程序(application)类加载器。
1.引导类加载器仅仅负责加载核心的Java库,比如位于<JAVA_HOME>/jre/lib 目录下的vm.jar,core.jar。这个类加载器,是JVM核心部分,是用native代码写成的。
2.扩展类加载器负责加载扩展路径下的代码,一般位于<JAVA_HOME>/jre/lib/ext 或者通过java.ext.dirs 这个系统属性指定的路径下的代码。这个类加载器是由sun.misc.Launcher$ExtClassLoader 实现的。
3.应用程序类加载器负责加载java.class.path(映射系统参数CLASSPATH的值)路径下面的代码,这个类加载器是由 sun.misc.Launcher$AppClassLoader 实现的。
当处理类加载器时,父委托模式是一个需要理解的关键概念。它规定:类加载器在加载自己的类之前,可以委托先加载父类。父类加载器可以是客户化的类加载器或者引导类加载器。但是有一点很重要,类加载器只能委托自己的父类加载器,而不能是子类加载器(只能向上不能向下)。
扩展类加载器是应用程序类加载器的父亲。类加载器的层次图见图12-1 。
图12-1 类加载器层次图
如果应用程序类加载器需要加载一个类,它首先委托扩展类加载器,扩展类加载器再委托引导类加载器。如果父类加载器不能加载类,子类加载器就回在自己的库中查找这个类。基于这个特性,类加载器只负责它的祖先无法加载的类。
如果类加载器加载一个类,这个类不是在类加载器树上的叶子节点上,就会出现一些有趣的问题。比如例12-1,一个名为WhichClassLoader1 的类加载了一个名为WhichClassLoader2类,WhichClassLoader2又调用了名为WhichClassLoader3的类。
例12-1 WhichClassLoader1 和 WhichClassLoader2 源代码
public class WhichClassLoader1 {
public static void main(String[] args) throws javax.naming.NamingException {
// Get classpath values
String bootClassPath = System.getProperty("sun.boot.class.path");
String extClassPath = System.getProperty("java.ext.dirs");
String appClassPath = System.getProperty("java.class.path");
// Print them out
System.out.println("Bootstrapclasspath =" + bootClassPath + "\n");
System.out.println("Extensionsclasspath =" + extClassPath + "\n");
System.out.println("Applicationclasspath=" + appClassPath + "\n");
// Load classes
Object bj = new Object();
WhichClassLoader1 wcl1 = new WhichClassLoader1();
WhichClassLoader2 wcl2 = new WhichClassLoader2();
// Who loaded what?
System.out.println("Object was loadedby "
+ obj.getClass().getClassLoader());
System.out.println("WCL1 was loaded by"
+ wcl1.getClass().getClassLoader());
System.out.println("WCL2 was loaded by"
+ wcl2.getClass().getClassLoader());
wcl2.getTheClass();
}
}
======================================================================
public class WhichClassLoader2 {
// This method is invoked from WhichClassLoader1
public void getTheClass() {
WhichClassLoader3 wcl3 = new WhichClassLoader3();
System.out.println("WCL3 was loaded by"
+ wcl3.getClass().getClassLoader());
}
}
如果所有的WhichClassLoaderX 类都放在应用程序的类路径下,三个类就会被应用程序类加载器加载,这个例子就会运行正常。现在假定把WhichClassLoader2 类文件打包成JAR文件放在<JAVA_HOME>/jre/lib/ext 目录下,运行WhichClassLoader1,就会看到例12-2的输出:
例12-2 NoClassDefFoundError 异常跟踪
Bootstrap classpath
=C:\WebSphere\AppServer\java\jre\lib\vm.jar;C:\WebSphere\AppServer\java\jre\lib
\core.jar;C:\WebSphere\AppServer\java\jre\lib\charsets.jar;C:\WebSphere\AppServ
er\java\jre\lib\graphics.jar;C:\WebSphere\AppServer\java\jre\lib\security.jar;C
:\WebSphere\AppServer\java\jre\lib\ibmpkcs.jar;C:\WebSphere\AppServer\java\jre\
lib\ibmorb.jar;C:\WebSphere\AppServer\java\jre\lib\ibmcfw.jar;C:\WebSphere\AppS
erver\java\jre\lib\ibmorbapi.jar;C:\WebSphere\AppServer\java\jre\lib\ibmjcefw.j
ar;C:\WebSphere\AppServer\java\jre\lib\ibmjgssprovider.jar;C:\WebSphere\AppServ
er\java\jre\lib\ibmjsseprovider2.jar;C:\WebSphere\AppServer\java\jre\lib\ibmjaa
slm.jar;C:\WebSphere\AppServer\java\jre\lib\ibmjaasactivelm.jar;C:\WebSphere\Ap
pServer\java\jre\lib\ibmcertpathprovider.jar;C:\WebSphere\AppServer\java\jre\li
b\server.jar;C:\WebSphere\AppServer\java\jre\lib\xml.jar
Extensions classpath =C:\WebSphere\AppServer\java\jre\lib\ext
Application classpath=.
Exception in thread "main"java.lang.NoClassDefFoundError: WhichClassLoader3
atjava.lang.J9VMInternals.verifyImpl(Native Method)
atjava.lang.J9VMInternals.verify(J9VMInternals.java:59)
at java.lang.J9VMInternals.initialize(J9VMInternals.java:120)
atWhichClassLoader1.main(WhichClassLoader1.java:17)
正如所看到的,由于WhichClassLoader3 在应用程序类路径下,程序失败,收到一个NoClassDefFoundError 的异常,这看起来有些奇怪。问题在于:它现在一个错误的类路径下面。当WhichClassLoader2被扩展类加载器加载的时候,发生了什么呢?实际上,应用程序类加载器委托扩展类加载器加载WhichClassLoader2,扩展类加载器又委托引导类加载器。由于引导类加载器找不到这个类,类加载的控制就会返回给扩展类加载器。扩展类加载器在自己的路径下找到了这个类将它加载。现在,当一个类已经被类加载器加载后,这个类需要的任何其他的新类都必须用同一个类加载器加载他们(或者遵循父委托模式,由父类加载器加载)。所以当WhichClassLoader2 需要访问WhichClassLoader3 的时候,扩展类加载器就会获得这个请求去加载WhichClassLoader3,扩展类加载器先委托引导类加载器,但是引导类加载器找不到这个类,于是扩展类加载器便试图装入自身但是也找不到这个类,原因是WhichClassLoader3不在扩展类路径而是在应用程序类路径。由于扩展类加载器无法委托应用程序类加载器,所以就会出现NoClassDefFoundError 的异常。
注意:开发者通常会使用如下语法通过类加载器机制加载属性文件:
Properties p = new Properties();
p.load(MyClass.class.getClassLoader().getResourceAsStream("myApp.properties"));
这个意思是:如果MyClass 由扩展类加载器加载,而 myApp.properties 文件只能应用程序类加载器看到,则装入属性文件就会失败。
12.2 概览WebSphere 类加载器
注意:每一个JVM都有自己的类加载器。在WebSphere 环境中会有多个应用程序服务器(JVM),也就是说JVM的类加载器是分开的,尽管它们运行在同一个物理机器上。
还需要注意的是:Java虚拟机(JVM) 使用的扩展和应用程序类加载器,你能够看到WebSphere 运行环境也使用名为扩展和应用程序类加载器,尽管它们的名字有些一致,但它们和JVM使用的是不一样的。
WebSphere 提供了几个客户化委托类加载器,如下图12-2:
图12-2 WebSphere 类加载器层次图
最顶端的方框表示Java类加载器(引导、扩展和应用程序)。WebSphere 在这里加载它自己的引导类加载器和初始化WebSphere扩展类加载器。
12.2.1 WebSphere 扩展类加载器
V6.1新特性: WebSphere 扩展类加载器是WebSphere加载自己的地方。在WebSphere之前的版本,运行环境被单一的类加载器加载。然而,从WAS6.1开始,WebSphere 被打包成若干OSGi(Open Services Gateway Initiative) 包。每一个OSGi 包都由自己的类加载器加载。OSGi类加载器的网络通过OSGi网关类加载器跟扩展类加载器和类加载器层次结构中的其余部分相连。
不管WebSphere装入自己类的方式如何改变,应用程序也不会有改变。还是同样的可见性、同样的类加载选择。
V6.1新特性:在WAS之前的版本中,WebSphere运行时类文件保存在<was_home>目录下的classes、lib、lib\ext和installedChannels 目录下。由于OSGi包,这些目录都不存在了,运行时类文件会存放在<was_home>\plugins 目录下。
扩展类加载器的类路径是由ws.ext.dirs 系统属性指定,也就是setupCmdLine 脚本文件中指定的WAS_EXT_DIRS环境变量设置。ws.ext.dirs 的缺省值如例12-3:
例12-3 we.ext.dirs 的缺省值
SET
WAS_EXT_DIRS=%JAVA_HOME%\lib; %WAS_HOME%\classes;%WAS_HOME%\lib;%WAS_HOME%\installedChannels;%WAS_HOME%\lib\ext;%WAS_HOME%\web\help;%ITP_LOC%\plugins\com.ibm.etools.ejbdeploy\runtimews.ext.dirs环境变量中列出的每个路径都会被添加到WebSphere扩展类加载器的类路径中,并且该目录中的每个JAR文件和ZIP文件都会被添加到类路径中。
正如所看到的,尽管在<was_home>目录中不再有classes、lib、lib\ext 和installedChannels文件夹,但是setupCmdLine 脚本文件会把它们添加到扩展类路径中。这就意味着,如果之前你已经把自己的JAR文件添加到<was_home>\lib 目录下,你可以创建这个目录并且把JAR文件添加进去,它们依然会被扩展类加载器加载。然而,不推荐这么做,在安装过程中还是正确的迁移比较好。
另一方面,如果你已经开发了Java应用程序,它依赖之前在<was_home>\lib 目录下的WebSphere JAR文件,你需要让你的程序保留兼容性。WAS 6.1 针对这样的应用程序提供了两种瘦客户端库:管理客户端库和Web 服务客户端库。可以在<was_home>\runtimes 目录下面找到这两个客户端库:
_ com.ibm.ws.admin.client_6.1.0.jar
_com.ibm.ws.webservices.thinclient_6.1.0.jar
这些库为应用程序提供了连接和与WebSphere一起工作的所有内容。
缺省设置是Allow,意思是你的应用程序可以无限制的调用非公用的内部WebSphere 类。但是不推荐这么使用,在未来的版本这个功能可能会被限制。因此,作为管理员,如果应用程序还能正常运行,最好把这个设置改成Restrict。如果它们依赖非公用的WebSphere内部类,你就收到一个ClassNotFoundException,这样还得改成Allow。这样开发人员就需要对应用程序进行移植,以保证应用程序能够兼容未来的WAS版本。
V6.1新特性:WAR 6.1 限制对WebSphere内部类的访问,这样你的程序才不会依赖那些未公开发布的WAS 的API。这个设置针对每个服务器(JVM),称为访问内部服务器类。
12.2.2 应用程序和Web模块类加载器
J2EE 应用程序包含五个主要元素:Web 模块,EJB 模块,应用程序客户端模块,资源适配器(RAR文件),工具JAR。工具JAR包含了EJB 和Servlet使用的代码。log4j 就是一个好的典型的工具框架的例子。EJB 模块、工具JAR、资源适配器文件和应用程序关联的共享库都会归结到同一个类加载器。这个类加载器称为应用程序类加载器。根据类加载器的规则,缺省情况下,多个应用程序(EAR)可以共享这个类加载器或者每一个应用程序都有对应的类加载器。
缺省情况下,Web 模块使用它们自己的类加载器(WAR类加载器)加载WEB-INF/classes 和EB-INF/lib 目录下的内容。通过修改应用程序WAR类加载策略可以改变这个缺省情况。
缺省情况,应用程序中每个WAR文件都有自己的类加载器(早期的版本,这个设置称为 Module)。如果WAR类加载策略设置成Single(早期的版本称为Application)类加载器,除了EJB、RAR、工具JAR以及共享库之外,Application类加载器还加载Web模块的内容。Application类加载器是WAR类加载器的父亲。Application类加载器和WAR类加载器都是可重装入的类加载器。一旦它们自动监测到程序代码改变了,就会重装入修改的类。我们可以在部署应用程序的时候改变这个行为。
12.2.3 操纵JNI 代码
因为JVM只有一个地址空间,每个地址空间中native代码只能装入一次,JVM规范要求在JVM中native代码只能被一个类加载器加载。
例如,这将导致一个问题,如果你有一个包含两个Web模块的应用程序(EAR文件),两个Web模块都需要通过Java Native Interface(JNI)装入native 代码,那么只有第一个被加载的Web模块能够成功。
为了解决这个问题,你可以把加载到类中的native 代码拆分成多个Java代码并且把它放在WebShpere应用程序类加载器(放在工具JAR)中。然而,如果要在同一个应用程序(JVM)中部署多个这样的应用程序(EAR文件),你需要把类文件放到WebSphere扩展类加载器上,取代每个JVM只能加载一次native代码。
如果native 代码放在一个可加载的类加载器(比如应用程序类加载器或者WAR类加载器),有一点很重要:能够正确的卸载自己的native代码而Java代码能够重加载。WebSphere 无法控制native代码,如果native代码不能正确的卸载或者加载,应用程序可能失败。
如果native 库依赖另一个,事情就会变得很复杂。关于native库的依赖关系,请查看信息中心。
12.3 配置 WebSphere类加载器
在之前的内容中,我们了解了WebSphere 类加载器以及类加载器如何协同工作。这一部分我们会讨论如何修改WAS的配置来影响WebSphere类加载的行为。
12.3.1 类加载策略
对于系统中的每个应用程序服务器,类加载策略可以设置成 Single 或者Multiple。当应用程序类加载策略设置成 Single,单一的应用程序类加载器可以加载应用程序服务器(JVM)中的所有 EJB、工具JARs 和共享库。如果WAR类加载策略设置成Single类加载(或者Application),这个应用程序中的Web模块也会被这个single类加载器加载。
当应用程序服务器类加载策略被设置成 Multiple,缺省值,每个应用程序会使用自己的类加载器加载EJB、工具JAR 和共享库。依赖于WAR类加载器加载策略是设置成应用程序中每个WAR文件使用自己的类加载器(或者称为Module),还是设置成Single 类加载(或者称为 Application),Web模块能够或者不能使用自己的类加载器。
下面用一个例子说明。我们有两个应用程序,程序1和程序2,它们运行在同一个应用程序服务器上。每个应用程序有一个EJB模块,一个工具JAR和两个Web模块。如果应用程序服务器自己的类加载策略设置成缺省值 Multiple ,所有Web模块的类加载策略也设置成缺省值,即应用程序中的WAR文件都有自己的类加载器,如下图12-3:
图12-3 类加载器策略:例一
每一个应用程序被完全的分开,每个应用程序当中的Web模块也被完全的分开。WebSphere缺省的类加载策略就是应用程序和模块都是完全隔离的。
如果我们现在把WAR2-2 模块的类加载策略修改成 Single,就会变成下图12-4:
图12-4 类加载策略:例二
Web 模块 WAR2-2 由程序2的类加载器加载,Util2.jar中的类能够看到WAR2-2 的 /WEB-INF/classes 和/WEB-INF/lib 目录下的类。
在上一个例子中,如果我们把应用服务器的类加载策略从Multiple改成Single,把WAR2-1模块的类加载策略从Module改成Single,结果就变成了下面的内容12-5:
图12-5 类加载策略:例三
现在只剩下一个应用程序类加载器加载程序1和程序2的。Util1.jar 中的类能够看到EJB2.jar、Util2.jar、WAR2-1.war 和WAR2-2.war 的类。然而,应用程序类加载器仍然看不到WAR1-1 和WAR1-2 的类,因为类加载器只能向上查看类而不能向下查看类。
12.3.2 类加载/委托模式
WebSphere 的应用程序类加载器和WAR类加载器都有一个称为类加载顺序的设置。这个设置决定了是否遵循正常的Java的类加载委托机制(见Java类加载器介绍)还是覆盖它。
类加载模式有两个可能的选项:
l 父类优先
l 应用程序优先
在WebSphere 的早期版本中,这个设置称为 PARENT_FIRST 和 PARENT_LAST。缺省类加载模式是父类优先(PARENT_FIRST)。这个设置要求类加载器在加载自己类路径中的类之前先加载父类。这是标准Java类加载器的缺省策略。
如果类加载策略设置成应用程序优先(PARENT_LAST),类加载器就会在加载父类之前,先把自己的类路径中的类加载进来。这个策略允许应用程序类加载器覆盖和提供已经在父加载器中存在的自己版本的类。
注意: 在这一点上,管理控制台有点模糊。在Web模块的配置界面,类加载顺序有两个选项:父类加载器优先和应用程序类加载器优先。然而,根据上下文,这里的“应用程序类加载器”其实指的是WAR类加载器,所以应用程序类加载器优先其实指的是WAR类加载器优先。
假如你有一个应用程序,类似于之前例子中的程序1,EJB模块和两个Web模块都使用log4j 生成日志。假定每一个模块把自己唯一的log4j.properties 文件打包进模块文件中。在EAR文件中,你只需要把log4j配置成工具JAR就可以了。然而,如果这样做,你可能会奇怪所有的模块都会看到这个jar文件,包括Web模块,尽管log4j.properties 文件是EJB模块加载的。
原因是,当Web模块初始化log4j包时,应用程序类加载器加载了log4j的类。Log4j 被配置成工具JAR。Log4j 会在自己类路径下查找log4j.properties 这个文件,在EJB模块中发现了它。
如果EJB模块不使用log4j记录日志,EJB模块中也不包含log4j.properties 文件, log4j 不会在任何的Web模块中找到log4j.properties这个文件。原因是类加载器只能够向上查找类,而不能向下。
要解决这个问题,可以这么做:
1. 单独创建一个文件,比如,Resource.jar,把它配置成工具JAR,把所有的log4j.properties都移到这个文件里面,但是要保证文件名唯一(比如 war1-1_log4j.properties、war1-2_log4j.properties和 ejb1_log4j.properties)。当从每个模块中初始化 log4j 的时候,告诉它为模块装入正确的配置文件,而不是都使用缺省(log4j.properties)。在原始位置(/WEB-INF/classes)存放Web模块的log4j.properties,把log4j.jar 添加到所有的Web 模块(/WEB-INF/lib)下,设置Web模块的类加载模式为应用程序类加载优先(PARENT_LAST)。当从Web模块中初始化log4j,模块自己装入log4j.jar,log4j 在自己的本地类路径下找到log4j.properties 。当EJB模块初始化log4j的时候,从应用程序类加载中加载,在相同的类路径下找到EJB1.jar中的log4j.properties.
2. 如果可能,把所有的log4j.properties文件合并成一个,放在应用程序类加载器中,比如放在Resource.jar 文件中。
12.3.3 共享库
共享库是多个应用可以公用的文件。典型的例子是框架的使用,比如Apache Struts 或者log4j。可以让共享库指向一个JAR集合,把这个JAR文件跟应用程序、Web模块或者应用程序服务器类加载关联起来。当你有多个不同版本的相同框架,你希望跟不同的应用程序关联,这个时候就能够使用共享库。共享库通过控制台进行定义。定义项包括一个象征性的名字,Java类路径和装入JNI库的native 路径。可以在单元、节点、服务器或者集群中定义共享库。但是,简单的定义一个库不会装入这个库的。必须把这个库跟一个应用程序、Web模块或者应用程序服务器的类加载器关联起来,这个共享库才能够被加载。如果共享库跟应用程序服务器类加载器关联起来,服务器上的所有应用程序都能够使用这个库。
注意: 如果已经把一个共享库跟应用程序关联起来,就不要把同一个共享库跟这个应用程序服务器关联起来。
可以通过如下两种方法关联共享库:
1. 可以使用管理控制台。在企业应用程序界面,引用选项可以关联到共享苦,创建共享库。
2. 可以使用应用程序的manifest 文件和共享库。库的依赖会在应用程序 manifest 文件中指明,在扩展列表中罗列出库的扩展名。
使用管理控制台,将共享库跟应用程序服务器的类加载器关联起来。这个设置能够在服务器基础架构中找到。展开Java和进程管理,选择 Class loader ,单击 New 按钮,定义一个新的类加载。一旦定义了一个新的类加载器,你可以修改它,同时使用共享库引用连接,可以把这个共享库跟新建的类加载器关联起来。
请查看“步骤4:: 使用共享库共享工具JAR”
12.4 类加载查看器
V6.0.2新特性: WebSphereApplication Server V6.0.2 引入了一个新的工具,类加载查看器。一旦激活,这个工具能够帮助你诊断类加载问题,显示不同类加载器,设置以及每个的类加载情况。
如果类加载查看器服务不可用,类加载查看器只能显示类加载器的层次结构以及他们的路径,而不是每个类加载器的具体类加载情况。也就是类加载查看器的查询功能缺省是不可用。
要启动类加载查看器服务,执行如下:Servers → Application Servers → <server name> ,选择AdditionalProperties 链接下的Class Loader Viewer Service,接着选择 Enable serviceat server startup 。完成这个设置,需要重新启动应用服务器才能够生效。
在下一节,我们会给出一个例子,说明不同类加载器如何工作以及使用类加载查看器记录不同的结果。
12.5 通过案例学习类加载
我们已经介绍了多个影响类加载的选项。这个部分,会举一个例子来说明这些。
创建一个简单的应用程序,有一个servlet,一个EJB。它们都调用一个类,VersionChecker ,见例12-4。这个类可以打印出哪一个类加载器加载这个类。VersionChecker 类还有一个内部值,显示正在使用的是哪个版本的类。这个会在后面用到,用来描述同一个工具JAR的不同版本的使用情况。
例12-4 VersionChecker 类源代码
package com.itso.classloaders;
public class VersionChecker {
static final public String classVersion ="v1.0";
public String getInfo() {
return ("VersionChecker is " +classVersion +
". Loaded by " + this.getClass().getClassLoader());
}
}
一旦装入,可以通过如下链接访问应用程序:http://localhost:9080/ClassloaderExampleWeb/ExampleServlet.
它启动了调用了VersionChecker 的ExampleServlet,会显示出例12-5中的信息:
例12-5 调用 ExampleServlet
VersionChecker is v1.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@71827182
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\classes;C:\W
ebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\Cl
assloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\lib\VersionCheck
erV1.jar;C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8
Node02Cell\ClassloaderExample.ear\ClassloaderExampleWeb.war
Delegation Mode: PARENT_FIRST
VersionCheckerV1.jar 文件包含了VersionChecker 类文件,它返回版本数是1.0。如下测试,如果没有特别指出,类加载策略和加载模式都使用缺省值。换句话说,应用程序有一个类加载器,WAR文件有一个。两个的委托模式都设置成了父加载器优先(PARENT_FIRST )。我们假定应用程序已经加载到了一个名为AppSrv02的应用服务器上了。
12.5.1 步骤 1:简单的Web模块打包
假定这种情况:我们的工具类只被一个servlet调用。把VersionCheckerV1.jar 文件放在WEB-INF/lib 目录下面。对于这样的配置,运行应用程序时,出现例12-6中的内容。
例12-6 类加载例1
VersionChecker called from Servlet
VersionChecker is v1.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@71827182
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\classes;C:\W
ebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\Cl
assloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\lib\VersionCheck
erV1.jar;C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8
Node02Cell\ClassloaderExample.ear\ClassloaderExampleWeb.war
Delegation Mode: PARENT_FIRST
从上述跟踪信息中,可以得到:
1.类加载器的类型是:
com.ibm.ws.classloader.CompoundClassLoader.
2.查找类的顺序是:
ClassloaderExampleWeb.war\WEB-INF\classes
ClassloaderExampleWeb.war\WEB-INF\lib\VersionCheckerV1.jar
ClassloaderExampleWeb.war
WEB-INF/classes 目录包含了没有打包的资源(比如servlet 类、Java类和配置文件),但是WEB-INF/lib 目录下就包含了打包好的JAR文件。你可以选择将Java代码打包到JAR文件中,将他们放到库目录下面或者可以未打包直接放在类路径下面。他们会在同一个类路径下面。由于我们的示例应用程序都是使用Application Server Toolkit开发和导出的,servlet直接就在类目录下面,在导出应用程序的时候,toolkit不会把Java类打包成一个JAR文件。WAR文件的根是下一个能够存放代码或者配置文件的地方,但是不建议这么用,因为这个目录对于Web服务器是文档根的目录,所以这个目录下的任何内容都能够通过浏览器直接访问。根据J2EE的规范,WEB-INF是受保护的,这就是为什么classes和lib目录都在WEB-INF目录下。
在应用程序启动的时候,类加载器的类路径是动态构建的,我们可以使用类加载查看器显示类加载。在管理员控制台上,选择Troubleshooting → Class Loader Viewer,展开server1 →Applications → ClassloaderExample → Web modules,单击 ClassloaderExampleWeb.war,就会看到如下图12-6:
图 12-6 类加载器显示应用程序树
展开Web模块时,类加载查看器显示了从JDK扩展类加载器到JDK应用程序类加载器再到WAR类加载器的层次结构,称为混合类加载器,参见12-7:
图12-7 类加载查看器显示类加载层次
如果你展开WAS模块的类路径----混合类加载器,可以看到跟VersionChecker打印出来同样的内容。见12-7。
例12-7 类加载查看器显示的WAR类加载器的类路径
file:/C:/WebSphere/AppServer/profiles/AppSrv02/installedApps/kcgg1d8Nod
e02Cell/ClassloaderExample.ear/ClassloaderExampleWeb.war/WEB-INF/classe
s
file:/C:/WebSphere/AppServer/profiles/AppSrv02/installedApps/kcgg1d8Nod
e02Cell/ClassloaderExample.ear/ClassloaderExampleWeb.war/WEB-INF/lib/Ve
rsionCheckerV1.jar
file:/C:/WebSphere/AppServer/profiles/AppSrv02/installedApps/kcgg1d8Nod
e02Cell/ClassloaderExample.ear/ClassloaderExampleWeb.war
类加载查看器会提供一个表,显示所有类加载器和每个加载器加载的类。同时这个表也显示出了委托模式:True 表示类的加载方式是父加载优先(PARENT_FIRST);false 表示类的加载方式是应用程序类加载优先 (PARENT_LAST),或者在只有一个Web模块的情况下是WAR类加载,见图12-8:
图12-8 类加载查看器结果表
正如所看到的,WAR类加载器已经装入示例中的servlet 和VersionChecker 类。类加载查看器还提供了查询功能:可以查找类、JAR文件、目录等等。如果你不知道类加载器加载了哪个类,这个功能就很有用。查询功能是大小写敏感但是允许使用通配符,比如使用*VersionChecker* 查询 VersionChecker 类。
12.5.2 步骤 2:添加一个EJB模块和工具JAR
下面,往应用程序中添加一个EJB,它也依赖VersionChecker JAR文件。在此,在EAR的根目录添加一个VersionCheckerV2.jar 文件。在这个JAR文件中的VersionChecker 类返回了Version 2.0。为了保证扩展类加载中的工具JAR可用,在EJB模块的manifest文件中添加一个引用,如例12-8:
例12-8 更新EJB模块的 MANIFEST.MF 文件
Manifest-Version: 1.0
Class-Path: VersionCheckerV2.jar
现在的结果是:有一个Web模块,在它的WEB-INF/classes 目录下面有一个servlet,在WEB-INF/lib 目录下面有VersionCheckerV1.jar 文件。还有一个EJB模块引用了EAR根目录下面的VersionCheckerV2.jar 工具JAR。你期望Web模块装入VersionChecker 类文件的版本是什么?是WEB-INF/lib 下的Version 1.0 还是工具JAR下面的Version 2.0?测试结果如例12-9:
例12-9 类加载例2
VersionChecker called from Servlet
VersionChecker is v2.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@26282628
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleEJB.jar;C:\WebSphere\AppServ
er\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\ClassloaderExample
.ear\VersionCheckerV2.jar
Delegation Mode: PARENT_FIRST
VersionChecker called from EJB
VersionChecker is v2.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@26282628
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\ClassloaderExample.ear\ClassloaderExampleEJB.jar;C:\WebSphere\AppServ
er\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\ClassloaderExample
.ear\VersionCheckerV2.jar
Delegation Mode: PARENT_FIRST
正如所看到的,当同时调用EJB模块和Web模块,VersionChecker 是 Version 2.0 。当然,原因是:WAR类加载器将请求委托给了父类加载器而不是他自己,所以工具JAR就被同一个类加载器加载,而无需考虑请求是来自于servlet还是EJB。
12.5.3 步骤 3:改变WAR类加载的委托模式
现在是否希望Web模块使用WEB-INF/lib 目录下面的VersionCheckerV1.jar 文件?为了这个目的,需要先将类加载委托模式从parent first 改为parent last。
设置委托模式为PARENT_LAST,使用如下步骤:
1. 在向导栏选择Enterprise Applications;
2. 选择ClassloaderExample 应用程序;
3. 在模块部分选择Manage modules;
4. 选择ClassloaderExampleWeb模块;
5. 将类加载顺序修改成应用程序类加载优先(PARENT_LAST)。记住,这个条目应该称为WAR类加载优先,参见 “类加载/委托模式”;
6. 单击 OK.
7. 保存配置;
8. 重新启动应用程序。
WEB-INF/lib 下的VersionCheckerV1 返回version of1.0。可以在例12-10中看到:
例12-10 类加载例3
VersionChecker called from Servlet
VersionChecker is v1.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@4d404d40
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\classes;C:\W
ebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\Cl
assloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\lib\VersionCheck
erV1.jar;C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8
Node02Cell\ClassloaderExample.ear\ClassloaderExampleWeb.war
Delegation Mode: PARENT_LAST
VersionChecker called from EJB
VersionChecker is v2.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@37f437f4
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleEJB.jar;C:\WebSphere\AppServ
er\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\ClassloaderExample
.ear\VersionCheckerV2.jar
Delegation Mode: PARENT_FIRST
如果你使用类加载器的搜索功能,搜索*VersionChecker*,会得到图12-9:
图12-9 类加载查看器搜索功能
例12-11 显示源代码
例12-11 类加载查看器搜索功能
WAS Module Compound Class Loader (WARclass loader):
file: / C: / WebSphere /AppServer / profiles / AppSrv02 /
installedApps / kcgg1d8Node02Cell /ClassloaderExample.ear /
ClassloaderExampleWeb.war / WEB-INF / lib /VersionCheckerV1.jar
WAS Module Jar Class Loader(Application class loader):
file: / C: / WebSphere / AppServer /profiles / AppSrv02 /
installedApps / kcgg1d8Node02Cell /ClassloaderExample.ear /
VersionCheckerV2.jar
12.5.4 步骤 4:使用共享库共享工具JAR
在此之前,只有一个应用程序使用VersionCheckerV2.jar 文件。是否希望多个应用程序能够共享它?当然,你可以在每个EAR文件中把这个文件打包进去。但是如果需要修改这个工具JAR,那需要重新部署所有的应用程序。为了避免这个麻烦,你可以使用共享库全局共享这个JAR文件。
共享库可以定义在单元、节点、应用程序服务器和集群。一旦你定义了共享库,必须将它跟应用程序服务器的类加载器或者单独的Web模块关联起来。根据共享库指派的目的地不同,WebSphere会使用匹配的类加载器加载共享库。
只要愿意,可以定义多个共享库。也可以为应用程序、Web模块或者应用程序服务器指派多个共享库。
在应用程序级别使用共享库
定义一个名为VersionCheckerV2_SharedLib 的共享库,并把它跟ClassloaderTest应用程序关联起来,步骤如下:
1. 在管理控制台,选择Environment→ Shared Libraries;
2. 选择共享库的作用域,比如单元,单击New;
3. 如图12-10:
图 12-10 共享库配置
– Name: 输入VersionCheckerV2_SharedLib;
– Class path: 输入类路径中的条目,每个条目之间用回车隔开。如果提供绝对路径,建议是用WebSphere环境变量,比如 %FRAMEWORK_JARS%/VersionCheckerV2.jar ,确定你已经定义了一个和共享库相同作用域的变量。
– Native library path: 输入JNI代码使用的DLLs 和.so 文件列表。
4. 单击 OK;
5. 选择Applications → Enterprise Applications;
6. 选择应用程序Classloader***ample ;
7. 在引用选项,选择Shared library references ;
8. 在应用程序列选择ClassloaderExample ;
9. 单击 Referenceshared libraries;
10. 选择VersionCheckerV2_SharedLib ,单击 >> 按钮将选中的移动到 Selected 列,如下图12-11:
图12-11 指定一个共享库
11. 单击OK ;
12. ClassloaderExample 应用程序共享库配置窗口如下图12-12:
图12-12 将共享库指派给应用程序 ClassloaderExample
13. 单击OK ,保存配置。
如果我们现在从EAR文件的根目录将VersionCheckerV2.jar删除,在EJB模块的manifest文件中也把引用删除,重新启动应用服务器,看到例12-12的结果。记住,Web模块的类加载顺序依然是应用程序类加载优先(PARENT_LAST)。
例12-12 类加载例4
VersionChecker called from Servlet
VersionChecker is v1.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@2e602e60
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\classes;C:\W
ebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\Cl
assloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\lib\VersionCheck
erV1.jar;C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8
Node02Cell\ClassloaderExample.ear\ClassloaderExampleWeb.war
Delegation Mode: PARENT_LAST
VersionChecker called from EJB
VersionChecker is v2.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@19141914
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleEJB.jar;C:\henrik\VersionChe
ckerV2.jar
Delegation Mode: PARENT_FIRST
正如预料的,由于Web模块的委托模式,当servlet 需要VersionChecker 类,VersionCheckerV1.jar 文件被加载。当EJB需要VersionChecker 的类的时候,就会从指向C:\henrik\VersionCheckerV2.jar 的共享库中加载它。如果你希望Web模块也使用共享库,只需要将类加载顺序恢复成缺省值,Web模块的类加载是父类加载优先。
在应用程序服务器级别使用共享库
共享库也可以跟应用程序服务器关联起来。在这个服务器上的部署的所有应用程序都能够看到共享库的代码列表。要把共享库跟应用程序服务器关联起来,首先要为应用程序服务器创建一个附加的类加载器,步骤如下:
1. 选择应用程序服务器;
2. 在应用程序基础结构部分,展开 Java and Process Management,选择 Class loader;
3. 选择New,为这个类加载器选择类加载顺序,父类加载优先 (PARENT_FIRST) 或者应用程序类加载优先 (PARENT_LAST),单击 Apply;
4. 单击刚刚创建的类加载器;
5. 单击 Sharedlibrary references;
6. 单击 Add,选择希望跟应用程序服务器关联的库。重复选择操作,将多个库跟这个类加载器关联。比如选择 VersionCheckerV2_SharedLib 条目;
7. 单击 OK;
8. 保存配置;
9. 重新启动应用程序服务器,修改才会生效。
将VersionCheckerV2 共享库跟应用程序服务器关联起来,就得到例12-13的结果。
例12-13 类加载例5
VersionChecker called from Servlet
VersionChecker is v1.0.
Loaded by com.ibm.ws.classloader.CompoundClassLoader@40c240c2
Local ClassPath:
C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cel
l\ClassloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\classes;C:\W
ebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8Node02Cell\Cl
assloaderExample.ear\ClassloaderExampleWeb.war\WEB-INF\lib\VersionCheck
erV1.jar;C:\WebSphere\AppServer\profiles\AppSrv02\installedApps\kcgg1d8
Node02Cell\ClassloaderExample.ear\ClassloaderExampleWeb.war
VersionChecker called from EJB
VersionChecker is v2.0.
Loaded by com.ibm.ws.classloader.ExtJarClassLoader@7dee7dee
Local ClassPath:C:\henrik\VersionCheckerV2.jar
Delegation Mode: PARENT_FIRST
我们定义的新的名为 ExtJarClassLoader 类加载器,在EJB模块请求时,它装入VersionCheckerV2.jar文件。由于委托模式,WAR类加载器继续装入自己的version。
12.6 类加载器问题诊断
JVM 5.0提供了一些配置,可以让我们查看详细的类装入,比如JVM 参数 -verbose:dynload、-Dibm.cl.verbose=<name>。
在实际开发过程中,如果使用不当,会出现很多类加载相关的问题。当遇到类加载问题时,可以查看WAS的相关日志,在日志中出现如下异常,可以认为是类加载器出现了问题:
ClassCastException
ClassNotFoundException
NoClassDefFoundError
NoSuchMethodError
IllegalArgumentException
UnsatisfiedLinkError
VerifyError
关于问题诊断,将在下一篇文章《WAS 6.1 类加载问题诊断》详细阐述。
总结
本文针对WAS6.1版本,详细介绍了类加载的概念以及如何客户化,并通过几个例子向大家讲述了影响类加载的选项的使用。虽然WAS 6.1 允许根据需要修改类加载策略,比如将父类优先改成应用程序优先,但是不推荐这么使用。笔者曾经就遇到因为修改策略,导致应用程序无法启动。原因是WAS中的组件和应用程序使用的某些类是一致的,加载策略选择不正确,就会导致类加载错误。
参考资料
1. WebSphereApplication Server V6.1: System Management and Configuration
http://www.redbooks.ibm.com/abstracts/sg247304.html?Open
2. WebSphere Application ServerV6.1: Classloader Problem Determination
http://www.redbooks.ibm.com/abstracts/redp4307.html?Open
3. 类装入问题解密,第 1 部分: 类装入和调试工具介绍
http://www.ibm.com/developerworks/cn/java/j-dclp1/
4. 类装入问题解密,第 2 部分: 基本的类装入异常
http://www.ibm.com/developerworks/cn/java/j-dclp2.html
5. 类装入问题解密,第 3 部分: 处理更少见的类装入问题
http://www.ibm.com/developerworks/cn/java/j-dclp3/
6. 类装入问题解密,第 4 部分: 死锁和约束
http://www.ibm.com/developerworks/cn/java/j-dclp4/
7. J2EE 类装入揭密
http://www.ibm.com/developerworks/cn/WebSphere/library/techarticles/0112_deboer/deboer.html
8. WAS 6.1 信息中心
http://publib.boulder.ibm.com/infocenter/wasinfo/v6r1/index.jsp
关于作者
胡伟红,西安交通大学硕士,目前就职于IBM 软件部。主要负责WebSphere软件产品的技术支持。可通过huweih@cn.ibm.com与她联系。