提纲:
===================================
一、模块次序问题
二、依赖库的问题
2.1 解决方案之一
2.2 解决方案之二
2.3 依赖库应用实例
===================================
正文:
===================================
在前面两篇文章中,我们了解了j2ee应用封装和部署的基本概念和实践操作,下面我们来看看几个可能遇到的问题。
一、模块次序问题
j2ee规范没有对ear文件内的j2ee模块应该如何部署作出任何规定。特别地,j2ee规范没有明确规定部署模块的次序。如果一个模块中的某个组件要用到另一个待部署模块的组件,它可能会带来问题。
因此,必须注意大多数应用服务器以如下步骤部署ear文件:
ear文件内的所有资源适配器作为基本连接器部署。如果存在多个资源适配器,则它们的部署次序就是它们在application.xml部署描述器中列出的次序。
部署所有ejb模块。由于ejb可能在初始化期间用到某些资源适配器,所以ejb的部署在资源适配器之后。如果存在多个ejb模块,它们的部署次序将是它们在application.xml中列出的次序。
部署所有web应用模块。由于web应用初始化期间可能用到资源适配器和ejb,所有web应用在这两者之后部署。如果存在多个web应用模块,它们部署次序就是它们在application.xml中列出的次序。
二、依赖库的问题
在j2ee应用封装和部署过程中,最常见的问题出现在工具类和支持类上。封装web应用或ejb应用时,这些库应该放在哪里?web应用和ejb应用一般都有被“卸出”(这里指装入的反向过程)的能力,这种能力由部署时装入它们的类装载器支持。如果我们把工具类和支持类放入应用服务器的标准类路径,这些类很可能完全失去被卸出的能力。这样,如果web应用或ejb应用要更新某个库的版本,重新部署web应用或ejb应用时,包含工具类和支持类的依赖库也要重新部署。在这种情形下,把工具类放入应用服务器标准类路径很不方便,因为每次部署web应用和ejb应用时,都要重新启动整个应用服务器,这显然不是理想的选择。
那么,在j2ee标准定义中,假定依赖库放在哪里才能实现运行时的重新部署(热部署)呢?有两个简单的方案,但从根本上来说两者都不甚合理:
以jar文件形式封装的依赖库可以放入web应用的web-inf\lib目录。一般地,web-inf\lib目录基本上只用来存放servlet,jsp页面和servlet会在读取新类时寻找该目录。如果工具类库只供一个web应用的jsp页面和servlet使用,应该说这个方案已经足够。然而,如果ejb组件、jms消费者、启动应用的类和关闭应用的类也要用到同一工具类库,这种方案不再有效,因为对于它们来说,web-inf\lib目录是不可见的。
除了把工具类库放入web-inf\lib目录之外,同时在每一个ejb jar文件中包含一份完整的拷贝。部署ejb时,ejb类装载器将只在它自己的jar文件中寻找被引用的工具类,不去查看其他已部署ejb应用的jar文件和web-inf\lib目录。如果有几个ejb应用都要用到同一个工具类库,则在每一个jar文件中放入该类库的一份拷贝就能解决问题。虽然这种方案实现了依赖库的热部署能力,但它明显地不够完善。封装jar文件的目的是为了提高应用的模块化程度,把同一个类文件放入多个jar包正好是背其道而行之。此外,多次复制同一组类无谓地加大了应用的体积。最后,即使只改变一个库,每一个jar文件也都要重新构造,从而使构造过程复杂化。
下面我们来看两种较为有效的解决方案。
2.1 解决方案之一
要解决这个问题,一种可能的方案是在j2ee中避免使用多个jar文件,把所有ejb和工具类聚集成单个包。ejb 2.0规范正在推动几个项目进行这方面的工作。这个规范要求参与关系的实体ejb使用本地引用,因此参与关系的各个ejb必须封装到同一个jar文件。
由于新的规范要求避免使用远程关系,因此,许多供应商正在考虑提供能够把多个ejb jar文件合并的工具。这些工具能够读取两个合法的ejb jar文件,把它们的类和部署描述器合并成单个统一的包。但应当注意的是,即使所有ejb组件都聚合到了单个jar文件里面,避免了在多个ejb之间复制依赖库,但如果web应用也要用到依赖库,则web-inf\lib目录下仍旧需要依赖库的副本。
此外,对ejb应用模块化的需求仍旧存在,许多人希望能够单个地部署ejb。因为重新部署一个jar文件时,该jar文件内的每一个ejb都将重新部署,如果实际要部署的只是单个ejb,部署过程中就会出现许多不必要的操作。
2.2 解决方案之二
随着jdk 1.3的发布,sun重新定义了支持可选包必不可少的“扩展机制”。这个扩展机制支持两方面功能:
jar文件可以声明自己对其他jar文件的依赖性。
类装载器经过了修改,能够在可选的包和应用路径中搜索类。
j2ee 1.3规范要求应用服务器必须提供这方面的支持。这就要求部署描述工具能够装载所有通过扩展机制定义的可选库。它同时也意味着,如果一个应用服务器或部署工具能够在运行时卸出或重新部署那些通过扩展机制使用库的ejb应用,那么,该应用服务器或工具也支持所有依赖库的卸出或重新部署。
这种扩展机制为web应用war文件和ejb应用jar文件指定自己需要哪些企业应用ear文件中的依赖库提供了一种标准化的方法。那么,这种扩展机制是如何工作的呢?每一个jar文件里都有一个manifest(意为载货单、旅客名单)文件,这个文件由jar工具自动创建,默认名字是manifest.mf。jar文件可以在manifest文件中加入一个class-path属性,引用它所依赖的jar文件。我们可以手工编辑manifest.mf文件,在原有内容的基础上,添加class-path属性。实际上,许多供应商提供的ejb封装工具会在封装过程中处理依赖类,自动创建合适的manifest.mf文件,使这个文件包含正确的class-path属性。
如果在创建ejb jar文件时修改了manifest.mf,引入了class-path属性,在生成一个新的ejb应用文件时,应用服务器提供的容器生成工具必须保存这个值。在weblogic server 6.1下,如果ejb jar已经在manifest.mf文件中包含class-path属性,weblogic.ejbc工具会在生成新ejb应用时保存这个值。当前,专门用来创建class-path属性并把它插入manifest.mf的工具还没有出现,所以这个工作有时还需要通过手工编辑jar文件的manifest文件进行。
class-path属性的值是用来搜索工具类库的相对url。这个url总是相对于包含class-path属性的组件(而不是ear文件的根)。单个class-path属性内可以指定多个url,一个manifest文件可以包含多个class-path属性。class-path属性的一般格式是:
class-path: 列出用空格分隔的多个jar文件名字
下面是一个例子:
class-path: mylog4j.jar xmlnew.jar foo/bar/myutil.jar
在j2se应用中使用这种扩展机制时,class-path属性可以引用目录。然而,对于全部内容都包含在jar文件中的j2ee应用,class-path属性只能引用其他jar文件。此外,在manifest文件中,class-path属性声明必须在一个独立的行上,以便与其他属性声明区分。
这种扩展机制的功能非常强大。特别地,通过创建一个以解析次序为最终次序的、包含所有类的统一类路径,它能够方便地解决循环引用问题。例如,在下面这个例子中,假设首先被解析的是myejb1.jar。myejb1.jar引用了:
class-path: jaxp1.jar myejb2.jar ..\xmlnew.jar
此时,类装载器将解析myejb2.jar。myejb2.jar引用了:
class-path: jaxp1.jar myejb1.jar
类装载器最终使用的“应用级类路径”将是:
class-path: jaxp1.jar myejb2.jar ..\xmlnew.jar myejb1.jar
2.3 依赖库应用实例
下面这个例子示范了一个企业应用各种可能的类装载情形。它示范了多个ejb模块、多个web应用,以及在这些应用之间多个共享的依赖库。通过这个例子,我们可以了解应用服务器从不同应用装载各个类的过程。当应用运行时,通过输出不同类的类装载器层次关系,我们可以了解:是否所有的类都通过单个类装载器装入,还是通过不同的类装载器装入;如果通过多个类装载器装入,这些装载器有什么关系。
这个例子包含两个ejb模块、两个web应用模块、七个在不同情形下使用的依赖类库。ear文件的结构如下:
mydepend1-container.jar
mydepend2-container.jar
mywebapp1.war
mywebapp2.war
testutil1.jar
testutil2.jar
testutil3.jar
testutil4.jar
testutil5.jar
testutil6.jar
testutil7.jar
meta-inf\
application.xml
每一个“mydepend?-container”jar文件包含一个ejb。每一个web应用中包含一个名为testservlet的servlet。每一个工具类库包含一个类,这个类有一个方法,它的功能是输出类装载器的层次关系。这个例子测试了ear文件内许多不同的类装载情形,其中包括:
当一个ejb用到一个依赖库,且在ejb manifest里面的类路径中引用它时,它是如何装载的?
当不同ejb模块内的多个ejb共享一个依赖库,且依赖库通过各个ejb的manifest类路径指定时,它是如何装载的?
当web应用引用一个依赖库,且通过web应用模块的manifest路径引用依赖库时,它是如何装载的?
当web应用引用一个依赖库,且依赖库保存在web应用模块的web-inf\lib目录下时,它是如何装载的?
当一个ejb模块和一个web应用都在它们的manifest路径中引用一个依赖库时,依赖库是如何装载的?
要执行这个例子,请先把mydepend.ear文件(从本文后面下载源文件)部署到服务器上,然后运行各个web应用的testservlet。testservlet将调用适当的ejb方法,ejb的方法又调用依赖库中类的方法。下面是执行两个servlet的url:
http://<服务器名字>:<端口>/web1/testservlet/
http://<服务器名字>:<端口>/web2/testservlet/
下面是servlet运行时在浏览器上的输出:
weblogic server 6.1控制台输出如下:
下面是application.xml部署描述器:
inc.//dtd j2ee application 1.2//en http://java.sun.com/j2ee/dtds/application_1_2.dtd>
app name
mydepend1-container.jar
mydepend2-container.jar
mywebapp1.war
web1
mywebapp2.war
web2
第一个ejb模块的manifest声明的class-path属性是:
class-path: testutil1.jar testutil3.jar testutil6.jar testutil7.jar
其他ejb模块和web应用模块的manifest类路径声明都有所不同,它们是ear文件包含的七个依赖库的不同组合。web应用中包含的各个servlet提供有关执行过程的详细信息,具体请参见各个组件的源代码。
毫无疑问,manifest.mf文件里面声明的类路径有助于提高j2ee应用的模块化。使用这种技术时,我们可以通过一种简单的模式确定哪些ejb应该封装为一个jar文件,哪些应该封装为另一个jar文件:
标识出一个参与cmr(container-managed relationship)关系的实体ejb。标识出所有可以从这个源实体ejb通过cmr关系到达的实体ejb。把这个关系图中的ejb封装为一个ejb jar。为每一组独立的实体ejb关系重复这个过程。
把所有剩余的ejb分别封装成jar文件。
分析业务和技术方面的需求,如果有必要的话,把多个jar文件合并成一个。如果修改单个ejb时重新部署多个ejb是可接受的,则可以用单个jar文件封装多个ejb。
每一个ejb jar文件必须通过manifest的class-path列出其依赖关系。类装载器将自动地解决循环引用和重复引用问题。例如,在前面演示依赖关系的例子中,多个ejb引用了第三个库。但是,虽然存在这种重复引用,ear类装载器只装载该库一次。