大型的基于 WebSphere 的项目开发中,同一个 WebSphere Application Server(以下简称 WAS)上会部署多个应用程序,而这多个应用程序必然会共用一些 jar 包,包括第三方提供的工具和项目内部的公共 jar 等。把这些共用的 jar 包提取出来在多个应用程序之间共享,不仅可以统一对这些 jar 包进行维护,同时也提高了 WAS 的性能。但是随着应用的不断扩大,新的应用程序的不断增加,新的应用程序会希望使用一些更高版本的共享 jar 包,而由于系统运行维护的需要,老的应用程序仍然希望用老版本的共享 jar 包,这样就必然造成了共享 jar 包的版本冲突。jar 包版本冲突问题是在大型应用项目的开发中经常遇到的问题,本文试图从 WebSphere 的类加载器入手,讨论几种在不同情况下解决 jar 包冲突问题的办法。
WebSphere 中类加载器介绍
Jar 包冲突实际上是应用程序运行时不能找到真正所需要的类,而影响类的查找和加载的是 JVM 以及 WebSphere 中的类加载器(class loader),为此,我们首先介绍一下 WebSphere 中的类加载器以及一些相关的概念。
WebSphere 中类加载器层次结构
Java 应用程序运行时,在 class 执行和被访问之前,它必须通过类加载器加载使之有效,类加载器是 JVM 代码的一部分,负责在 JVM 虚拟机中查找和加载所有的 Java 类和本地的 lib 库。类加载器的不同配置影响到应用程序部署到应用程序服务器上运行时的行为。JVM 和 WebSphere 应用程序服务器提供了多种不同的类加载器配置, 形成一个具有父子关系的分层结构。WebSphere 中类加载器的层次结构图 1 所示:
图 1:WebSphere 中类加载器的层次结构 如上图所示,WebSphere 中类加载器被组织成一个自上而下的层次结构,最上层是系统的运行环境 JVM,最下层是具体的应用程序,上下层之间形成父子关系。
- JVM Class loader:位于整个层次结构的最上层,它是整个类加载器层次结构的根,因此它没有父类加载器。这个类加载器负责加载 JVM 类, JVM 扩展类,以及定义在 classpath 环境变量上的所有的 Java 类。
- WebSphere Extensions Class loader:WebSphere 扩展类加载器 , 它将加载 WebSphere 的一些 runtime 类,资源适配器类等。
- WebSphere lib/app Class loader:WebSphere 服务器类加载器,它将加载 WebSphere 安装目录下 $(WAS_HOME)/lib/app 路径上的类。 在 WAS v4 版本中,WAS 使用这个路径在所有的应用程序之间共享 jar 包。从 WAS v5 开始, 共享库功能提供了一种更好的方式,因此,这个类加载器主要用于一些原有的系统的兼容。
- WebSphere "server" Class loader:WebSphere 应用服务器类加载器。 它定义在这个服务器上的所有的应用程序之间共享的类。WAS v5 中有了共享库的概念之后,可以为应用服务器定义多个与共享库相关联的类加载器,他们按照定义的先后顺序形成父子关系。
- Application Module Class Loader:应用程序类加载器,位于层次结构的最后一层,用于加载 J2EE 应用程序。根据应用程序的类加载策略的不同,还可以为 Web 模块定义自己的类加载器。
关于 WebSphere 的类加载器的层次结构,以下的几点说明可能更有助于进一步的理解类的查找和加载过程:
- 每个类加载器负责在自身定义的类路径上进行查找和加载类。
- 一个子类加载器能够委托它的父类加载器查找和加载类,一个加载类的请求会从子类加载器发送到父类加载器,但是从来不会从父类加载器发送到子类加载器。
- 一旦一个类被成功加载,JVM 会缓存这个类直至其生命周期结束,并把它和相应的类加载器关联在一起,这意味着不同的类加载器可以加载相同名字的类。
- 如果一个加载的类依赖于另一个或一些类,那么这些被依赖的类必须存在于这个类的类加载器查找路径上,或者父类加载器查找路径上。
- 如果一个类加载器以及它所有的父类加载器都无法找到所需的类,系统就会抛出 ClassNotFoundExecption 异常或者 NoClassDefFoundError 的错误。
类加载器的委托模式
类加载器有一个重要的属性:委托模式(Delegation Mode,有时也称为加载方式:Classloader mode)。委托模式决定了类加载器在查找一个类的时候, 是先查找类加载器自身指定的类路径还是先查找父类加载器上的类路径。
类加载器的委托模式有两个取值:
- Parent_First:在加载类的时候,在从类加载器自身的类路径上查找加载类之前,首先尝试在父类加载器的类路径上查找和加载类。
- Parent_Last:在加载类的时候,首先尝试从自己的类路径上查找加载类,在找不到的情况下,再尝试父类加载器类路径。
有了委托模式的概念,我们可以更加灵活的配置在类加载器的层次结构中类的加载和查找方式。表 1 中给出了在 WebSphere 的类加载器层次结构中各个类加载器的委托模式的定义,并给出了不同的类加载器内类的生命周期。
注意:在上表中,"JVM Class loader" 因为在类加载器的最顶层,它没有父类加载器,因此其委托模式为 N/A,"WebSphere Extensions Class loader"和"WebSphere lib/app Class loader"的委托模式固定为表中的取值,不可配置,其它的类加载器的委托模式都是可以配置的。
WebSphere 中的类加载器策略
WebSphere 中对类加载器有一些相关的配置,称为类加载器策略(class loader policy)。类加载器策略指类加载器的独立策略(class loader isolation policy), 通过类加载器策略设置,我们可以为 WAS 和应用程序的类加载器进行独立定义。
每个 WAS 可以配置自己的应用程序类加载器策略,WAS 中的每个应用程序也可以配置自己的 Web 模块类加载器策略,下面我们对这两种策略分别介绍。
1 .应用服务器(WAS)配置:应用程序类加载器策略
应用服务器对应用程序类加载器策略有两种配置:
- Single:整个应用服务器上的所有应用程序使用同一个类加载器。在这种配置下,每个应用程序不再有自己的类加载器。
- Multiple:应用服务器上的每个应用程序使用自己的类加载器。
2 .应用程序配置:Web 模块类加载器策略
应用程序中对 Web 模块类加载器有两种配置:
- Application:整个应用程序内的所有的实用程序 jar 包和 Web 模块使用同一个类加载器。
- Module:应用程序内的每个 Web 模块使用自己的类加载器。应用程序的类加载器仍然存在,负责加载应用程序中 Web 模块以外的其它类,包括所有的实用程序 jar 包。
从上面的定义可以看出,不同的类加载器策略的配置下,类加载器的层次结构上的某些类加载器可能不存在。比如在应用程序服务器的应用程序类加载 器策略定义为 single 的情况下,应用程序的类加载器将不存在,同一个应用服务器上的所有应用程序将共用同一个类加载器,这也就意味着不同的应用程序之间的类是共享的,应用程序 间不能存在同名的类。
回页首
在 WebSphere 中解决 jar 包冲突
Jar 包冲突问题实际上就是应用程序希望用某一个确定版本的 jar 包中的类,但是类加载器却找到并加载了另外一个版本的 jar 包中的类。在上一部分介绍了 WebSphere 中类加载器的基本概念和相关配置之后,我们来看如何在 WebSphere 中解决 jar 包冲突。
在 WAS v5 版本之前,使用共享 jar 包的方式是将 jar 包放在 $(WAS_HOME)/lib/app 路径下,从上一部分中,我们可以看到,这个路径正是"WebSphere lib/app Class loader" 类加载器的类查找路径,WebSphere 会查找这个路径以取得相应得 jar 包中的 Java 类,从而做到在 WebSphere ND 上的多个应用程序之间共享 jar 包的目的。但是这样做的一个缺点就是这些共享 jar 包暴露给 WebSphere ND 上所有的应用程序,对于那些希望使用 jar 包其它版本的应用程序,这些 jar 包也同样存在在了它们的类加载器类路径上,因此,就不可避免的会造成版本的冲突。在 WAS v5 版本及之后,增加了共享库(shared library)的概念,推荐的在多个应用程序间共享 jar 包并避免 jar 包冲突的方式是使用共享库。
具体分析引起 jar 包冲突的情况,主要有三种:
- 多个应用程序间 jar 包冲突:多个应用程序间由于使用了共享 jar 包的不同版本而造成 jar 包版本冲突。
- 应用程序中多个 Web 模块间 jar 包冲突:同一个应用程序内部,不同的 Web 模块间同时使用一个 jar 包的不同版本而造成 jar 包版本冲突。
- 应用程序中同一个 Web 模块内 jar 包冲突:同一个应用程序内部,同一个 Web 模块内,由于需要同时使用同一个 jar 包的两个版本而造成的 jar 包冲突
本部分根据这三种 jar 包冲突的情况,讨论三种解决 jar 包冲突的办法,并具体讨论三种解决办法的实现步骤和适用情况:
- 共享库方式解决 jar 包冲突:主要解决应用程序间的 jar 包冲突问题
- 打包到 Web 模块中解决 jar 包冲突:主要解决应用程序中多个 Web 模块间 jar 包冲突问题
- 命令行运行方式解决 jar 包冲突:主要解决应用程序中同一个 Web 模块内 jar 包冲突问题
共享库方式解决 jar 包冲突
在 WAS v5 中,提供了一种很好的机制,使得 jar 包只存在于需要这个 jar 包的应用程序的类加载器的路径上,而其它的应用程序不受它的任何影响,这就是共享库(Shared library)。共享库可以用在应用服务器级别和应用程序级别,使用应用程序级别的共享库,其好处就是在不同的应用程序之间使用共享 jar 包的不同版本。我们可以为一些通用 jar 包的每个不同版本定义成不同的共享库,应用程序希望使用哪个版本,就把这个版本的共享库放到应用程序的类加载器的类路径上,这种方式有效的解决了应用程序 之间 jar 包冲突的问题。
下面举例介绍定义和使用共享库的具体方法,本例中,假设存在 xerces.jar 包版本冲突。
1 . 定义共享库
系统管理员可以在 WebSphere 的 Admin console 中定义共享库,可以分别在 Cell、Node 以及 server 的级别上定义。
步骤一 : 进入 Admin console,选择 Environment > Shared Library > new。如图 2 所示:
图 2:WebSphere Admin Console 中进入共享库页面 步骤二: 给出共享库的名字,并指定共享的文件和目录。多个不同的文件 / 目录之间通过"Enter"键分隔,且不能有路径分隔符,如":"和";"等。如图 3 所示:
图 3:WebSphere Admin Console 中添加共享库 步骤三:点击 Apply 或者 OK 之后,就添加了一个名字为 Xerces V2.0 的共享库。记住添加完成后一定要在 admin console 保存配置。如图 4 所示:
图 4:WebSphere Admin Console 中共享库列表-
2 .安装应用程序
进入 Admin console,选择 Applications > Install New Application 安装应用程序。请参照 IBM WebSphere 的 Admin console 使用手册进行安装新的应用程序,此处不再详细介绍。
3 .将共享库关联到应用程序
步骤一:进入 Admin console,选择 Applications > Enterprise applications ,并选择需要使用共享库的应用程序。注意:因为要改变应用程序的设置,所以如果应用程序已经运行,需要先停掉应用程序。如图 5 所示:
图 5:WebSphere Admin Console 中选择需要配置的应用程序 步骤二 : 点击应用程序,进入后,选择 Libraries。如图 6 所示:
图 6:WebSphere Admin Console 中选择应用程序库属性步骤三 : 点击 Add,为应用程序添加共享库。如图 7 所示:
图 7:WebSphere Admin Console 中应用程序添加库 步骤四 : 从下拉列表中选择所需要的共享库,点击 OK。如图 8 所示:
图 8:WebSphere Admin Console 中应用程序添加库页面指定所用的共享库
这样,Xerces V2.0 共享库定义的 xerces 版本就存在于了应用程序类加载器的类加载路径上。注意,在添加完成后要保存服务器的设置。
4 .设置应用程序的类加载器的委托模式为 Parent_Last
为了进一步防止共享库定义的 jar 包的其它版本已经存在于 JVM 或者 WebSphere 的类加载器路径上,还需要设置应用程序的类加载器的委托方式为 Parent_Last。
通过上面的配置,即使 xerces 的其它版本已经存在于系统中,应用程序在运行时,其类加载器也会首先查找并加载指定的共享库中的 xerces 版本。这样我们就通过使用共享库的方式,解决了 jar 包版本冲突问题。
打包到 Web 模块中解决 jar 包冲突
共享库的方式,只是在应用程序的层次上,在多个应用程序之间解决了共享 jar 包造成的版本冲突问题,如果一个应用程序的内部,其中一个 Web 模块使用了一个 jar 包的 A 版本,而另一个 Web 模块使用这个 jar 包的 B 版本,在这种情况下造成的 jar 包冲突,共享库的方式是无法解决的,我们可以考虑将其中一个在多个应用程序间共享的 jar 包版本,比如 A 版本,定义成共享库,或者放在"WebSphere lib/app Class loader"类加载器路径上供多个应用程序使用,而将 B 版本的 jar 包打包到使用它的 Web 模块中的方式来解决冲突。
其次,目前很多在线的系统是 WAS v4 的遗留系统,其上运行的应用程序已经使用了"WebSphere lib/app Class loader"类加载器,将 jar 包放在 $(WAS_HOME)/lib/app 目录下进行共享。如果由于其中某个应用程序的升级或者新增加某个应用程序,需要使用某个共享 jar 包的其它版本,在这种情况下,为了减少对系统的影响,也可以考虑将这个共享 jar 包的新版本打包到升级(或新增)的应用程序中的方式来解决 jar 包冲突。
由于 Web 模块的 WebContent/WEB-INFO/lib 目录在应用程序 Web 模块的类加载器查找路径上,因此,我们可以把 jar 包放在这个目录下,Web 模块的类加载器将自动查找并加载这个 jar 包中的类。
步骤一:在 WSAD IE 集成开发环境中,将冲突 jar 包放在 Web 模块的 WebContent/WEB-INFO/lib 目录下。如图 11 所示:
图 11:WSAD IE 中为 Web 模块添加库步骤二:在 Admin console 中,将应用程序部署到 WebSphere server 上之后,进入 Applications > Enterprise Applications, 选择相应的应用程序,并确认应用程序不在运行状态 ( 参见前面章节中选择应用程序的步骤 )。点击进入应用程序,确认应用程序的类加载器的委托模式为 Parent_First, 应用程序的类加载器策略为 Module。如图 12 所示:
图 12:WebSphere Admin Console 中应用程序属性配置页面步骤三:在同一个页面上,选择 Web Modules,点击进入。如图 13 所示:
图 13:WebSphere Admin Console 中选择应用程序 Web 模块属性步骤四:点击相应的包含冲突 jar 包的 Web 模块,设置 Web 模块的类加载器的委托模式为 Parent_Last。注意:在设置完成后要保存服务器配置,并启动应用程序。如图 14 所示:
图 14:WebSphere Admin Console 中为 Web 模块指定类加载器委托模式
将冲突 jar 包打包到 Web 模块中,并设置相应 Web 模块的类加载器的委托模式为 Parent_Last,应用程序在运行过程中加载类的时候,这个 Web 模块的类加载器会首先查找 WebContent/WEB-INFO/lib 目录下的 jar 包进行类的加载;而对于其它的 Web 模块,由于其类加载器的委托模式仍然为缺省的 Parent_First,它们的类加载器仍然首先从应用程序的共享库或者 WebSphere 的共享路径上加载 jar 包中的类,从而解决了 jar 包冲突的问题。
命令行运行方式解决 jar 包冲突
不论是设置共享库,还是将冲突 jar 包打包到应用程序中,其解决的问题都是在应用程序的一个 Web 模块中只使用了冲突 jar 包的一个版本的情况。我们在开发中曾经遇到过这样的情况:应用程序的 Web 模块中已经使用了 1.4 版本的 xerces.jar,由于 Web 功能的扩展,在这个模块中又引入一个新的第三方工具,而这个第三方工具需要使用 2.0 版本的 xerces.jar 才能正常工作,这种情况下的 jar 包冲突如何解决呢?
在前面类加载器的部分已经介绍过,每个应用程序的一个 Web 模块最多只能有一个类加载器,而 Web 模块的类加载器中加载的类的生命周期为整个应用程序的运行期,也就是说,Web 模块加载器不可能同时加载一个类的两个版本,同时,Web 模块的类加载器的委托模式也是在应用程序运行前设置的,在应用程序运行期内无法改变的,因此,上面描述的在一个 Web 模块中同时使用两个版本的 jar 包的问题,象前两种方法那样配置运行在一个 JVM 内的类加载器的设置的方法是无法解决的。
唯一的解决办法就是在应用程序运行的 JVM 外,启动另外一个 JVM 来运行调用冲突 jar 包的代码,因为两个不同的 JVM 可以加载各自的类,从而解决 jar 包冲突问题。
这种情况下,原来使用 jar 包的老版本的方式(包括 jar 包放置路径,共享库设置方式,类加载器的委托模式等)不变,将对 jar 包新版本的调用通过命令行运行方式实现。具体做法是:将对 jar 包新版本内功能的调用,封装到一个可以单独运行的类中,在 Web 模块中以命令行方式运行这个类。同时把这个类以及 jar 包的新版本放在任意一个 was 可访问的路径上(比如 /usr/WebSphere),在命令行的 classpath 参数中包含这个路径(比如 /usr/WebSphere)。
下面通过举例说明命令行运行方式的编程过程,在本例中,假设 TestEar 应用程序的 Web 模块 TestWar 中,已经使用了 conflict_v1.jar,由于新添功能需要使用 conflict_v2.jar 中的 exampleCall() 功能。
冲突 jar 包 conflict_v2.jar 提供的功能:
代码 1:冲突 jar 包 conflict_v2.jar 功能 1 Package com.ibm.conflict;
2
3 Public class ConflictClass{
4 …… .
5 Public static String exampleCall(string param){
6 String rs;
7 …… ;
8 Return rs;
9 }
10 ……
不存在冲突问题时的编码举例:
如果没有 jar 包冲突问题,则对这个功能的调用是简单的,只需要将 conflict_v2.jar 放在应用程序自身或者其父类加载器的查找路径上,然后在 Web 模块中直接调用即可,如下:
代码 2:不存在冲突时的调用方式1 Public String methodA(String param){
2 ……
3 String rs = ConflictClass.exampleCall(param);
4 ……
5 Return rs;
6 }
存在冲突后的命令行运行方式编码举例
针对 jar 包冲突问题,我们需要在 Web 模块中做如下的修改:
- 步骤一:将冲突 jar 包放在 was 可访问的路径上,比如 /usr/WebSphere/conflict_v2.jar。
步骤二:将对包含冲突代码的调用封装到一组可单独运行的类中,它们将调用冲突 jar 包的功能,并将结果以系统输出的方式打印到系统标准输出上。 将这些类封装到一个单独的 jar 文件中,比如 workAroundConflict.jar,并将其放在 was 可访问的路径上,比如 /usr/WebSphere/workAroundConflict.jar。
1 Package com.ibm.test;
2
3 Import com.ibm.ConflictClass;
4
5 Public class WorkAround{
6 Public static void main(String[] args){
7 String param1=args[0];
8 String returnStr=ConflictClass.exampleCall ();
9 System.out.println("<RTStr>"+returnStr+"</RTStr>");
10 Return;
11 }
12 }
代码 3:将对冲突代码的调用写入一个单独的类 WorkAround 步骤三:在 Web 模块中通过命令行方式调用封装的类,通过 classpath 指定所有依赖的 jar 包和类路径。运行封装类,从系统标准输出中取得运行结果。
1 Public static String methodA (String param){
2 ……
3 String rtStr = "";
4 String lStr="<RTStr>";
5 String rStr="<RTStr>";
6 //put all the dependency jar here
7 String classPath=
8 "/usr/WebSphere/conflict_v2.jar;
9 /usr/WebSphere/workaroundConflict.jar; ……";
10 String className="com.ibm.test.WorkAround";
11 String cmdLine="java -classpath " +classPath +" " +className + " "+ param;
12 Try{
13 Process process = Runtime.getRuntime().exec(cmdLine,null);
14 process.waitFor();
15 BufferedReader br= new BufferedReader(
16 new InputStreamReader(process.getInputStream()));
17 while ((s = br.readLine())!=null) {
18 if (null == out)
19 out=s;
20 else
21 out+=s;
22 }
23 //get result from out
24 if (null != out){
25 int lIndex = out.lastIndexOf(lStr);
26 int rIndex = out.lastIndexOf(rStr);
27 rsStr = out.substring(lIndex+lStr.length, rIndex);
28 }
29
30 } catch (Exception e){
31 e.printStackTrace();
32 }
33 …… .
34 return rsStr;
35 }
代码 4:在应用程序中通过命令行方式运行 WorkAround -
命令行运行方式通过启动另外一个 JVM 的方式运行冲突代码,在一个不同的 JVM 中加载冲突的类,从而解决了 jar 包冲突问题。
但是命令行运行方式毕竟不是一个很好的方式,它存在以下的弊端:
- 命令行运行方式只适用于对冲突 jar 包的使用只是运行一段代码,或者只要求返回简单的字符串结果的情况。对于复杂的交互,命令行方式无法做到。但是如果在别无他法的情况下,可以适当地划分封 装对冲突代码调用的 jar 包的包含范围,尽量将命令行运行的代码接口简单化。
- 命令行运行方式因为启动了另外一个 JVM 来运行,降低了 WebSphere 的性能。
因此,命令行方式只适用于一些极为特殊的情况下解决 jar 包冲突问题。
回页首
结论
本文对基于 WebSphere 的大型项目开发中遇到的 jar 包冲突问题,结合 WebSphere 中类加载器的概念,给出了三种解决办法以及相应的操作步骤和实现代码,并分析了各种方式所适用的具体情况。
项目开发过程中的 jar 包冲突,主要存在以下三种情况:
- 多个应用程序间的 jar 包冲突
- 应用程序内多个 Web 模块间的 jar 包冲突
- 应用程序内同一个 Web 模块内部 jar 包冲突
通过对类加载器的分析我们知道,解决 jar 包冲突问题的根本在于合理配置类加载器。在深入理解 WebSphere 中类加载器的层次结构的基础上,我们给出了"共享库解决 jar 包冲突"以及"打包到 Web 模块中解决 jar 包冲突"的办法,通过合理地配置 WebSphere 中类加载器及其委托模式,可以解决大多数的 jar 包冲突问题。但是由于这个层次结构中类加载器只配置到 Web 模块,因此,对于 Web 模块内部的 jar 包冲突问题,类加载器的配置是无法解决的,为此我们给出了"命令行运行方式解决 jar 包冲突"的办法。
表 2 中列出了在不同的 WAS 版本下,三种解决 jar 包冲突问题的办法所适用的 jar 包冲突情况。
表 2:Jar 包冲突解决办法适用的 jar 包冲突情况总结 参考资料
- IBM WebSphere Application Server V5 ClassloaderNamingSpace Guide。
- IBM WebSphere Application Server V5.1 System Management and Configuration。
作者简介
郝爱丽是一位 IBM CSDL 的软件工程师,从事多年 J2EE 开发工作,有丰富的 J2EE 开发经验。目前从事企业电子商务应用的开发和支持。
常红平是一位 IBM CSDL 的软件工程师,IBM certified DB2 DBA 和 IBM certified DB2 developer。他目前正在从事企业电子商务应用的开发。您可以通过 changhp@cn.ibm.com 和他联系。