UML软件工程组织

用 J2EE 1.2 部署多个应用程序
规划 Enterprise JavaBeans 组件中的重用
Kyle Brownbrownkyl@us.ibm.com),高级技术人员,IBM
Keys Botzumbotzumk333@yahoo.com),高级 I/T 咨询专家,IBM
如果您使用 EJB 技术进行开发,则可能会创建可重用组件。遗憾的是,通常准备好处理重用的计划时,已经为时已晚。在本文中,IBM 企业开发人员 Kyle Brown 和 Keys Botzum 研究了一种常见重用方案,并探究了由这一方案而产生的一些考虑事项。他们将向您显示如何为打包和部署应用程序作出最佳选择。他们还通过将 IBM WebSphere Application Server 用作示例来提供实现的详细信息。

在构建 J2EE 应用程序时存在一个严重的部署问题,许多介绍该主题的书籍和文章都在无意中传播了这一主题。该问题是由适合于介绍性书籍上下文的项目需求和现实世界中项目所具有的需求之间的差异引起的。简而言之,问题就是:您决不会只有一个项目,而且多个项目之间决不会彼此完全孤立。

在现实世界中,您不会只部署单个项目并满足既得的成就。现实要复杂得多。您通常部署一个项目,然后两个月之后,部署另一个项目,该项目依赖于第一个项目所提供服务。有时候,您部署一个项目,然后六个月之后,部署该项目的第二个版本,同时让第一个版本并发地运行一段时间。

如果您是应用程序服务供应商(Application Service Provider,ASP),这甚至会更复杂。在这种情况下,您实际上可能要为不同客户多次部署相同的应用程序,只是不同的客户具有不同的 URL 入口点以及不同的静态位图和 Web 页面细节。

从前……(一个应用程序开发故事)
作为的确存在的复杂类型示例,请考虑这样一个方案。假定由我们 Widgets Inc. 公司构建的第一个 J2EE 应用程序是人力资源部的雇员考勤卡应用程序。我们创建一组 EJB 组件以作为应用程序的一部分,用于处理雇员管理方面的问题。我们有一个 EmployeeManagement 会话 bean,它可检索向特定经理报告的一组雇员,我们还可以从 HR 数据库检索有关特定雇员的信息(姓名、部门、雇用日期和薪金级别等)。

不过,接着又产生了一个新的需求。要求我们构建一个新的应用程序,该应用程序允许雇员选择各种公司综合福利,允许他就医疗保险和储蓄计划签约等。对于该应用程序,我们正好需要已经在前一个应用程序中创建的业务逻辑类型。例如,要允许雇员买卖休假日,我们需要知道他的服务年数 — EmployeeManagement 会话 bean 已经可以为我们检索这些项。

那么,我们该做些什么呢?第二次重写逻辑,还是找到重用现有逻辑的方法?重写似乎确实不太可取,所以重用似乎是最有效的选择。但是,如何重用该会话 bean 及其相关类呢?让我们研究几个选项。

选项 1:合并组件部署
在这种情况下,我们将把新的 Benefits 应用程序打包为现有 Timesheet 企业归档文件(EAR)中的 Web 归档文件(WAR)(或许是一个或两个 EJB-JAR)。如图 1 所示:

图 1. 合并组件部署
图 1. 合并组件部署

我们在这里所做的是将两个应用程序放在同一个 EAR 中,以便 Benefits 应用程序可以访问 Timesheet 应用程序中的类和 EJB 组件。该方法非常容易实现,它解决了我们的当前问题。然而,它有一些缺点,这些缺点使它不能成为吸引人的长期解决方案。

让我们考虑一下使用这两个应用程序的方法。Timesheet 应用程序的使用非常频繁,每位时薪雇员每天随时都会使用它。这意味着:我们可以容易地预测最大负载并确定部署该应用程序所需的适当硬件。但 Benefits 应用程序不同。公司中的每位雇员(不一定是小时工)都可能使用该应用程序,但对于每位雇员,只会在两周福利签约周期内的任何时间使用它一次。所以,运行该应用程序所需的硬件容量与 Timesheet 应用程序差异甚大 — 如果每个人都等到最后一刻,在登记周期的最后一天签约(虽然我们确信决不会发生),那么负载明显会更高。

还有就是,应用程序的使用模式不同 — Timesheet 应用程序在每天的正常工作时间内使用,而晚上停止使用,进行服务器和应用程序维护。然而,Benefits 应用程序应该可以让雇员在晚上通过公司的虚拟专用网(VPN)来使用,以便在他们进行福利选择时让他们的家属一起参与。那么,我们如何来调和不同的需求呢?

另外,可用性怎样?在我们的示例中,如果 Benefits 应用程序当机 1 小时左右,这或许还可以接受,但 Timesheet 应用程序不行 — 它要求在工作时间内 100% 地运行。

根据这些需求,我们可以得出结论:不能在同一个 EAR 文件中部署这两个应用程序;这些应用程序是相关的,但不够紧密,所以无法保证共同部署。事实上,对前面几个考虑事项的评估将使您在大多数情况下拒绝将合并组件部署作为部署解决方案,因此我们提出了共享服务部署。

选项 2:共享服务部署
通过使用这一部署方法,我们将连接到 EmployeeManagement 会话 bean 所需的代码包括到 Benefits 应用程序的 WAR 文件中,而将应用程序分别放在它们自己的 EAR 文件中。在图 2 中说明了这个选项:

图 2. 共享服务部署
图 2. 共享服务部署

该方法将 EmployeeManagement bean 视为共享服务。将应用程序部署到不同的 EAR 文件中让我们将它们部署到不同的应用程序服务器 JVM 实例中,从而允许我们对每个应用程序分配多少硬件做出不同决定。另外,该方法似乎有助于维护问题 — 现在我们可以关闭 Benefits 应用程序,而根本不会影响 Timesheet 应用程序。

然而,我们并没有彻底解决该问题。现在,这两个应用程序之间具有依赖性。虽然我们可以关闭 Benefits 应用程序,但 Timesheet 应用程序现在必须具有与 Benefits 应用程序相同的可用性需求,所以可能会给管理带来麻烦。此外,我们现在必须处理一个 J2EE 1.2 规范未解决的问题:如何将 EJB 存根代码包括在 Benefits WAR 文件中。从理论上讲,我们可以从 Timesheet EJB-JAR 文件分离出客户机代码,但这看起来容易出错并且很可能会造成问题。稍后我们回头讨论这种可能性。

另一种可能性是将客户机代码放在 EAR 文件之外的共享类路径上,但这种方法不仅违反了 J2EE 规范的精神,而且如果我们选择错误的类路径,那么它会对该应用程序和应用程序服务器本身造成破坏。简而言之,这个解决方案引起的问题比它解决的问题要多得多。

与合并组件解决方案相比,共享服务部署会降低性能。如果我们选择通过将两个应用程序放在不同 JVM 中来分离它们,那么我们需要一个跨进程调用,而这样的调用本可以由两个应用程序都位于同一个 JVM 的解决方案中的容器进行优化。简而言之,在部署了 EJB 组件的 JVM 内部调用组件比从该 JVM 外部调用组件快好几倍。

然而,该解决方案还会造成一个更隐蔽的问题,它也出现在上一个解决方案中:版本漂移。设想一下,在 EmployeeManagement 会话 bean 的初始版本中,我们有一个具有以下特征符的方法:


EmployeeVO getEmployeeDetails(int employeeID)



该方法特征符假设 employeeID 为整数,如果它们真的是整数,则工作正常。然而,让我们假设它们进一步受到整数大小的限制 — 一些其它后端数据库需求将雇员标识限制为 6 位数。假设这对于两个应用程序的初始版本运行正常,但是此后有一天我们用完了 6 位整数。作为变通方法,HR 部门对雇员标识规则进行重新定义,使之还可以包括字母字符,从而产生了下面经更改的特征符:


EmployeeVO getEmployeeDetails(String employeeID)



因为 Timesheet 团队开发这个 EJB 组件,所以他们计划更新他们自己的应用程序,以使用新的雇员标识特征符。但是,Benefits 应用程序会怎样呢?假设就在福利签约周期开始之前 Benefits 应用程序一年只推出一个新版本(因为我们只可以在签约周期内更改福利,但我们可以在任何时候查看我们的福利)。如果 Benefits 团队未准备好推出其应用程序的新版本,那么这次对 Timesheet 应用程序的更改将破坏 Benefits 应用程序。

那么,我们该如何避免 API 版本漂移问题呢?要找到解决方案,我们需要发散性地思考一下。虽然 J2EE 规范可能看上去使我们受困于一系列糟糕的解决方案,但实际上它隐含着很好的思想,使我们能够十分干脆利落地解决这一问题。解决问题的关键是断定逻辑项目块尽可能不要跨越 EAR 文件。让我们研究一下。

选项 3:独立部署
在这第三个选项中,EAR 是完全独立的。通过使用不同的全局 Java 命名目录接口(Java Naming Directory Interface,JNDI)名称多次(每个 EAR 一次)部署同一个 EJB 类来处理共享 EJB 组件 — 使本地 JNDI 名在 web.xml 文件和 ejb-jar.xml 文件中保持一致,但当部署 EAR 文件时,更改本地和全局名称之间的运行时绑定,如图 3 所示:

图 3. 独立部署
图 3. 独立部署

要了解该部署选项的工作原理,让我们研究一下它在 IBM WebSphere Application Server V4.0 中的实现方式。Application Server 有一个在域级别上管理的“全局”JNDI 名称空间。Application Server V4.0 中的 JNDI 命名服务在 Admin 服务器上运行,所以虽然每个节点有一个 JNDI 服务,但它们都共享保存在公共管理数据库中的同一个全局名称空间。尽管实现不同,但 Application Server 5.0 也有一个全局和本地名称空间,所以在新版本中原理是一样的。然而,这有两个部分:本地引用(例如,java:comp/env 项)是特定应用程序所特有的,因为它们是在 web.xml 和 ejb-jar.xml 文件中指定的。这表示我们的代码可以引用这些名称,并确保如果这些名称下面的配置更改,则代码也不必更改。然后,第二部分是这些本地名被绑定(在部署时)到共享的全局 JNDI 名称空间中的名称。所以,我们可能会有表 1 中所示的情况:

表 1. 绑定到全局 JNDI 名称空间中名称的本地名称

Web 应用程序 本地 JNDI 引用 全局 JNDI 引用
Benefits java:comp/env/ejb/EmployeeManagementHome benefits/com/ibm/ejbs/ EmployeeManagementHome
Timesheet java:comp/env/ejb/EmployeeManagementHome timesheet/com/ibm/ejbs/ EmployeeManagementHome(不同的全局引用)


该方法解决了在尝试分割跨 EAR 的应用程序时发生的许多问题 — 最显著的是:当两个应用程序各自都依赖于稍有不同的共享 EJB 组件版本时发生的“API 版本漂移”问题也解决了。我们常常听到有关该解决方案的争论是:它“产生了太多的 EJB”而且浪费内存。这个说法完全错误。我们可以调优 EJB 服务器和容器的高速缓存大小,使这种方案的内存管理比共享方案中的更有效。另外,许多不熟悉 EJB 技术(尤其是实体 beans)的人认为:该解决方案将不起作用,因为操作实体 beans 的方法有点“不可思议”— 不知何故,对于任何数据块只有一个实体 bean 对象,以多种类似于此的方法部署 bean 将破坏数据管理。这也是个错误观点,因为当在类似于本解决方案中的群集环境中有多个应用程序克隆时,也会有行锁定和隔离级别问题。正如在 EJB 2.0 规范(请参阅参考资料)的 10.5.9 章节中所定义的那样,实体 beans 中在数据库级别管理并发性,除非我们正在使用 EJB 提交选项 A,这通常在群集环境中不起作用。在任何情况下,选择该方法将丝毫不会影响数据的一致性或访问速度。

然而,该解决方案仍可能会引起另一个问题,而该问题会产生许多新问题。在解决“API 版本漂移”问题时,我们已经使自己暴露在一个更微妙的“数据/外部漂移”问题面前。现在,一些应用程序使用的 EJB 代码版本比其它应用程序旧。在实现较新版本代码的过程中,很有可能更改数据库模式,而这样可能会破坏旧的代码。除非我们保存两个产品数据副本(数据管理恶梦),否则我们必须十分仔细地实现数据库模式更改。

另外,在更改业务逻辑时,甚至会产生一个更微妙的问题。通常,对外部接口的更改表示 API 工作方式的语义更改。然而,有时更改语义时没有更改外部 API — 例如,可能会修正关键错误,或者可能重构复杂的业务逻辑块,以获得更佳性能。这种情况的主要问题是:如果我们选择独立部署,则只能通过更新所有其它 EAR 文件来处理这种类型的更改。因而,当发生这种情况时,共享服务部署实际上更可取。

权衡选项
按照我们的开发小故事,似乎独立部署对于每种应用程序开发方案都是正确选择。但事实并不是这样。在我们的方案中,简化了几个考虑事项,而正是由于这些考虑事项,使得在许多不同的情况下,使用共享服务部署比使用独立部署更合适。虽然您应该仔细地考虑在许多部署方案中使用独立部署,但它并不适合于每种情况。特别是,如果满足下列任一条件时,应该选择共享服务部署:

  1. 如果逻辑应用程序十分复杂(多个 JAR 文件),那么可能很难将其所有逻辑部件都包括在每个单独的应用程序中。然而,您无论如何都应该留意应用程序被打包在多个 JAR 文件中这一事实。如果您认为您自己必须包括几个 JAR 文件来表示单个逻辑应用程序,那么把逻辑应用程序制作成一个较大的 JAR 文件可能会更好。在 J2EE 中,尤其对于 EJB-JAR 文件的颗粒度下限有一些经验法则(如应该将多少个 EJB 组件放在单个 EJB-JAR 文件中)。例如,对于任何 IBM EJB 扩展(象实体 bean 继承)或任何 EJB 2.0 扩展(关系等),只有同一个 EJB-JAR 文件中的 bean 可以相互引用。然而,上限不太清楚 — 单个 EJB-JAR 中有多少个 bean 算太多不太明确。颗粒度级别通常根据开发优先级设置而不是根据部署考虑事项设置(例如,对于每个开发人员或每个开发小组可能有不同的 EJB-JAR)。

    如果应用程序有特定的部署约束,那么可能会证明独立部署的实现很难或花费很大。例如:

    • 应用程序可能将 JMS 与 WebSphere MQ 一起使用或者将 JCA 与 CICS Transaction Gateway(CTG)一起使用,而您不想在每个应用程序服务器中重复那些产品的安装。例如,您可能不想安装几个 WebSphere MQ 服务器副本,以使生产环境的整体软件成本最小化。

    • 应用程序可能有复杂的管理或状态信息。或许,它创建多个线程,这些线程以复杂的方式运行以处理工作。将这种应用程序与其它应用程序集成会很困难。

    • 应用程序可能有严格的安全性约束并可能包含需要谨慎保护的信息(如密码)。这种信息应该尽可能地集中存放。因而,将应用程序内容复制到另一个应用程序是不明智的做法。

    • 也可能您的应用程序数据需要谨慎地保护,让其它应用程序直接访问这些数据是不合适的。
  2. 如果应用程序的基本外部接口定义良好且稳定,但该实现的内部很可能更改,那么实现独立部署时要小心。内部更改的示例包括更改数据库模式、更改业务逻辑或者甚至更改实现的基础结构(新的数据库、新的第三方产品等)。在这种环境中,最好分隔系统层,这样可以以基本方法更改第二个应用程序,而不影响第一个应用程序。只要您预先花时间设计稳定且良好设计的会话 bean 接口,共享服务部署在这种情况下就会表现出其优势。

  3. 如果部署环境涉及大量分布在不同地点的数据中心,则独立部署会没有效率。例如,如果应用程序 1 的数据在数据中心 1 中,应用程序 2 的数据在数据中心 2 中,则没有好的位置来放置组合的应用程序 1 和 2。因此,最好将应用程序 2 放置在数据中心 2 中并配置它,使之对应用程序 1 进行远程 EJB 组件调用。

通常,应用程序越复杂,实现独立部署就越困难。复杂的应用程序通常有复杂的管理、安装和初始化过程(许多配置文件和 Application Server 配置约束等)。在这种情况下,将它们与其它应用程序组合在一起会给客户机应用程序带来很大负担。只向客户机应用程序提供适当的客户机 EJB-JAR 会简单些。这样,客户机只需要处理客户机应用程序的客户机接口,而不需要处理其余部分。当然,对于简单的应用程序来说,组合它们远比处理分布式环境的复杂性要容易得多。即使在独立部署可以节省时间和工作的情况下,许多组织也不考虑它。

如果必须实现共享服务部署,在现有的 J2EE 工具不直接支持该选项的情况下如何使它工作呢?事实上,您必须为该工具找个折中方案。一个选项是开发您自己的定制工具功能,使之可以“剥离”标准 EJB-JAR 文件并打包新的客户机 JAR 文件。至少,从 EJB-JAR 文件中除去 ejb-jar.xml 文件会将它变成客户机 JAR 文件,因为如果缺少 ejb-jar.xml 文件中保存的信息,开发工具将不会试图在 JAR 内部部署 EJB 组件。然而,在大多数情况下,您会希望“剥离”的 JAR 文件的额外部分,以使它尽可能的紧凑(例如,包括 EJB 实现类和生成的框架和持久性类)。

最后,我们需要考虑的另一个共享服务部署变化就是是否有可能将共享组件(在我们的示例中是 EmployeeManagement bean)完全分割成其自己的 EAR 文件,并与任何其它特定于应用程序的代码完全分离。对于最适合选用共享服务部署的应用程序来说,这或许是最佳的长期解决方案。然而,这带来了其自身的一些问题:一旦组件与应用程序代码分离,现在谁拥有它们并负责维护它们呢?如果已经建立了专门的重用组来维护共享分布式组件,那么这可以工作得很好,但如果没有建立,那么当共享组件出错时,这种选择会导致相互推脱,都说“这不是我的事情”。

WebSphere Application Server 示例
如果独立部署适合于您的应用程序并且您还在 IBM WebSphere Application Server 中进行开发,那么侧栏“在 IBM WebSphere Application Server V4.0 和 V5.0 中实现共同部署的组件”说明了如何通过仔细地设计应用程序代码来完成独立部署,并演示了该配置如何在 WebSphere Application Server 中操作。

从中得到哪些知识
那么,您如何继续下去呢?下面是所涉及问题的一些总结:

  • EAR 是一个部署合同。每个 EAR 都应该表示一个完整的应用程序,并且不应该依赖 EAR 文件外部的代码,除了应用程序服务器本身提供的以外。

  • 不同的逻辑应用程序应该放在单独的 EAR 文件中。当在 J2EE 组件之间存在依赖性时,必须仔细设计并考虑可用性。从理论上讲,应该设计松散耦合的应用程序,每个应用程序在任何时候都不依赖于另一个可用的应用程序。这最好通过使用象采用 JMS 的异步通信之类的技术来完成。然而,当业务需要时,也可采用同步通信。可是,请紧记,该需求对相关应用程序带来了很重的负担。如果许多应用程序非常依赖于彼此的完全可用性,那么系统不太可能会长期工作下去。

  • 每个 EAR 版本都包含了由该逻辑应用程序版本组成的 J2EE 组件集合。在本文所讨论的示例中,Benefits 应用程序应该由 Benefits WAR 文件和 Timesheet EJB-JAR 文件(以及任何其它需要的 J2EE 组件一起)组成。

  • 必须在源代码控制管理(Source Control Management,SCM)系统中单独对每个 J2EE 组件(WAR 或 EJB-JAR)进行版本控制和维护。这允许您通过共同配置适当的 WAR 和 EJB-JAR 文件的正确版本来构建 EAR 文件。如果 Benefits 应用程序 V2.0 需要 Timesheet JAR 文件 V1.0,那么当 Timesheet 应用程序 V2.0 需要 TimeSheet JAR 文件 V1.1 时,您可以很快地识别并解决这一问题。

事实上,对这最后一点值得进行少许澄清和提供一些额外提示。我们鼓励的一种实践是:J2EE 组件(EAR、WAR 和 JAR)包含额外的、人类可读的元数据,该元数据指出组成它的各种子组件的版本。例如,一个 EJB-JAR 文件可能包含特殊的 XML 文件(不是由应用程序服务器使用的,而是由人读取并可能是由 SCM 工具或脚本生成),该文件列出了它所包含的不同 EJB 组件的版本号。如果 EJB-JAR 或 WAR 文件依赖于其它 J2EE 组件的其它版本,那么它还应该将该信息包括在它的 XML 元数据文件中。同样,EAR 文件应该包含它所包含的 WAR 和 EJB-JAR 文件的版本列表。这样,在构建 EAR 时,就可以自动捕捉版本不匹配问题。如果检测到不匹配,那么在部署之前,构建会异常终止。

结束语
打包和部署相关的 J2EE 应用程序是一个复杂问题。对于每个环境或应用程序都没有单一的答案。在本文中,我们提供了有关部署 EAR 文件的一些简单建议,从而避免相关应用程序之间的版本漂移、不兼容的硬件需求和不兼容的可用性需求问题。我们很快排除了一种我们时常见到的方法,然后介绍了两种更有用的方法(共享服务部署和独立部署)。我们还演示了在为特殊问题选择正确的部署选项时您必须权衡的不同因素。

虽然我们建议您在最简单的解决方案(独立部署)适用时就采用它,但我们还提供了一些必需的工具,以了解它何时不适用而需要一个更复杂的方法。这应该有助于您确定如何最佳配置您自己的应用程序服务器以及如何避免不正确部署可能引起的一些更棘手的问题。

参考资料



关于作者
Kyle Brown 是 IBM 软件服务部(Software Services)WebSphere 方面的高级技术人员。Kyle 向《财富》500 强客户提供有关面向对象主题和 Java 2 企业版(J2EE)技术方面的咨询服务、培训和指导。他与人合著了 Enterprise Java Programming with IBM WebSphereWebSphere 4.0 AEs Workbook for Enterprise JavaBeans(第三版)The Design Patterns Smalltalk Companion 这三本书。他还经常在有关企业 Java、OO 设计和设计模式的会议上发言。可以通过 brownkyl@us.ibm.com 与他联系。


Keys Botzum 是 IBM 软件服务部 WebSphere 方面的高级顾问。Keys 在大规模分布式系统设计方面有十多年经验,并且专攻安全性问题。他使用过各种分布式技术,包括 Sun RPC、DCE、CORBA、AFS 和 DFS。最近,他着重研究 J2EE 及其相关技术。他拥有斯坦福大学计算机科学硕士学位和卡内基梅隆大学应用数学/计算机科学学士学位。可以通过 botzumk333@yahoo.com 与 Keys 联系。

这两位作者非常感谢 Tom Alcott、Bill Hines、Randy Stafford、Martin Fowler、Mark Hapner、Bobby Woolf 和 Richard Monson-Haefel,感谢他们对本文内容提出的敏锐分析和有帮助的意见。

 

版权所有:UML软件工程组织