UML软件工程组织

架构师观点:将EJB组件公开为业务服务

 

作者:不详 来自:甲骨文OTN

 

设计模式对于面向服务的体系结构具有深远的影响(人们对此尚认识不足),因此请明智地选择您的设计模式。

当机构使用 Web 服务技术构建、部署和组织业务服务时,显然必须进行仔细、全面设计 Java 2 平台企业版 (J2EE) 应用程序。在这方面,最有效的帮助是严格应用旨在实现面向服务的体系结构 (SOA) 的体系结构模式。当公开 Enterprise JavaBean (EJB) 时,此类模式尤其有用。

人们通常把模式仅仅看作是为特殊设计问题提供指导的参考工具;而事实上,应将模式看作是体系结构要求的组成部分。它们是影响业务服务(封装了业务规则验证、计算、数据访问以及其他驱动 J2EE 应用程序的核心功能的逻辑)组织决策的起点。除其他适用于 J2EE 的模式和思考方式以外,一些学术界人士、供应商和用户还从 SOA 的角度对著名的Design Patterns一书(由 Addison-Wesley 出版社于 1994 年出版,作者是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides —— 通常称之为“四人帮”或“GoF”)中的许多模式进行了再计算和研究。

本文将从体系结构的角度介绍最重要的模式及其应用,假设您熟悉以下 Web 服务基础知识:简单对象访问协议 (SOAP)、HTTP、XML、J2EE、EJB、Java 消息服务 (JMS) 等。

走进 Web 服务世界

从 J2EE 的角度而言,Web 服务基本上是 J2EE 编程模型的扩展(参见图 1),具体体现在:

Web 服务继承了符合 J2EE 的应用服务器的容器功能。

远程过程调用 (RPC) 样式的 Web 服务通过非会话状态 EJB 公开。

消息样式的 Web 服务通过 JMS(JMS 监听程序)和消息驱动的 bean 公开。

非会话状态 EJB 或 JMS 被定义为 Web 服务定义语言 (WSDL) 的应用程序入口点,并被部署为 Web 服务。

Web 服务客户机使用 WSDL 生成服务请求程序基于 SOAP 的代码。


 图 1:J2EE/Web 服务模型

首先,我们将简要概述将 EJB 应用程序公开为业务服务的过程。尽管本文使用 Oracle Application Server Containers for J2EE (OC4J) 环境,但对于任何其他符合 J2EE 的应用服务器也同样适用。

下面,我将介绍一个企业对客户 (B2C) 商务领域中的示例。(参见图 2 和图 3;图 2 描绘了角色交互,图 3 描绘了系统分解。)在该示例中,客户登录到“电子商店”网站,点击感兴趣的商品,选择某些要购买的商品,查看订单信息,提供支付信息,最后注销。为简单起见,我省略了大量详细信息—例如,所有表示(Web 层)组件、安全支持等。

图 2:“电子商店”示例—协作图表
 
 
 图 3:“电子商店”示例—分解图表

如图 3 所示,该解决方案包含四种 EJB 子系统(每个子系统包含多个 bean):

客户验证管理: 负责登录/注销、站点注册和客户首选项

客户体验管理:负责客户可以在“电子商店”站点执行的所有功能—查看目录、购买商品、付款、查询订单状态等

产品清单管理: 负责提供商品可用性和更新存货/脱销数据库以及触发实现进程

财务管理: 负责帐目管理和其他财务处理,如开发票。

此处最简单的服务可能是基于非会话状态会话 bean SystemIdEJB 的 GetSystemIdService。该 bean 是称作“验证客户”的一组对象类的一部分。如果客户已经成功通过验证,则该 bean 提供稍后用于获取其他客户信息(例如,与商品目录的呈现方式相关的客户首选项)的系统用户 ID。如果客户未成功通过验证,则该 bean 发出例外。

以下是发送至该服务的 SOAP 1.1 请求以及该服务发出的响应的示例:

POST /SystemId HTTP/1.1
Host:eStore.com
Content-Type:text/xml; charset="utf-8"
Content-Lengthnnnn
SOAPAction:"http://eStore.com/getSystemId"

<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<m:GetSystemIdRequest
xmlns:m="http://eStore.com/GetSystemId.wsdl/"
xmlns:xsd="http://eStore.com/GetSystemId.xsd/">
<xsd:logon>LogonID</xsd:logon>
</m:GetSystemIdRequest>
</soapenv:Body>
</soapenv:Envelope>

..........

<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<m:GetSystemIdResponse
xmlns:m="http://eStore.com/GetSystemId.wsdl/"
xmlns:xsd="http://eStore.com/GetSystemId.xsd/">
<xsd:authenticatedId>SystemID</xsd:authenticatedId>
</m:GetSystemIdResponse>
</soapenv:Body>
</soapenv:Envelope>

从以上示例可以清楚地看到:WSDL 和 XML 消息表示是启用服务的主要元素。最重要的是,WSDL 描述了该服务的消息和端口、绑定以及服务定义:

<message name="GetSystemIdInput">
<part name="body" element="xsd:logon"/>
</message>

<message name="GetSystemIdOutput">
<part name="body" element="xsd:authenticatedId"/>
</message>

<portType name="SystemIdPortType">
<operation name="GetSystemId">
<input
message="tns:GetSystemIdInput"/>
<output
message="tns:GetSystemIdOutput"/>
</operation>
</portType>

.....................

<service name="GetSystemIdService">
<port name="SystemIdPort"
.....................
<port>
</service>

但这些 WSDL 和 XML 消息表示究竟如何映射到 Java 对象,尤其是 EJB?首先,mapping.xml 文件指定 Java 到 WSDL 映射(例如,程序包名称与 XML 名称空间之间的映射,WSDL 类型与 Java 对象之间的映射等等)。对于 OC4J,该文件由 wsadmin 工具生成,如下所示:

<package-mapping>
<package-type>AuthenticateCustomer</package-type>
<namespaceURI>urn:oracle-ws</namespaceURI>
</package-mapping>

然后,创建一个名为 webservices.xml 的部署描述符,并将其置于 ejb-jar 文件的 META-INF 中。该描述符指定了由 J2EE 应用服务器控制的服务描述以及它们与容器功能的相关性:

...............

<webservice-description>
<webservice-description-name>SystemIdEJB</webservice-description-name>
<wsdl-file>META-INF/GetSystemId.wsdl</wsdl-file>
...........
<port-component>
<port-component-name>SystemIdPort</port-component-name>
<wsdl-port>
<namespaceURI>urn:oracle-ws</namespaceURI>
<localpart>SystemIdPort</localpart>
</wsdl-port>
<service-endpoint-interface>AuthenticateCustomer.GetSystemIdService</service-
endpoint-interface>
<service-impl-bean>
<ejb-link>SystemIdEJB</ejb-link>
</service-impl-bean>
</port-component>
</webservice-description>

以上示例表明:如果在开发环境中使用适当的工具(如 Oracle  应用服务器 和 Oracle JDeveloper),则公开 EJB 的过程将非常简单。但情况并非绝对如此,而且实际的大型分布式应用程序需要模块化、可重用性、可扩展性、可移植性、版本控制、一致性以及可伸缩性的可持续特性。

有关如何利用标准 J2EE 组件(其中的模块功能面向业务服务上下文)以及 Web 服务中不断涌现的技术改进或增强软件开发过程以高效构建高质量的大型应用程序的信息很少。即使我们能够精确地标识“可以服务”的业务功能并拥有所有必要的应用程序和中间件工具,也无法回答下面这个问题:如何在分布式处理元素中安排应用程序功能或职责,以便在使用 Web 服务时最大限度地增强质量功能?

通常情况下,研究 Web 服务的组织利用其现有 Java 对象并直接使用 Web 接口公开它们。最终的结果是不符合 SOA 的要求,且此类实现可以使组织面临以下比较严重的问题:

  • 低可重用性—为某个项目开发的服务不易于在其他项目中重用,这是因为它们的结构通常过分依赖于 Web 服务接口的实现。某个服务中的组件之间的依赖性降低了该服务在其他上下文中的可用性。
  • 低可扩展性—当出现新功能需要时,很难扩展现有接口,或至少将更改或扩展孤立到一到两个组件。通常情况下必须设计新接口,尤其是当服务控制逻辑集中用于执行消息处理的代码中时。
  • 低可伸缩性—很难存档高容量工作负载(例如,每秒钟 1,000 个以上的服务请求)。直接使用 Web 服务接口公开每个对象的细粒度解决方案无法通过经济有效的方式扩展到这样的级别。

要避免此类问题,组织需要部署正确的体系结构模式来帮助创建粗粒度、高度面向业务的 J2EE 组件。

分层的重要性

此处要考虑的主要模式是 EJB 层中的分层。该模式移除了所公开的 Web 服务接口的复杂性。分层的 Web 服务驱动的 EJB 可以通过更简单的面向业务的接口进行设计,而不必将多个接口指向服务集合。从 J2EE 的角度而言,大多数设计人员针对此目的解决方案是使用 Floyd Marinescu 在《EJB Design Patterns:Advanced Patterns, Processes, and Idioms》(由 Wiley 出版社于 2002 年出版)一书中所介绍的 Facade(外观)模式—具体而言,就是创建一个用作外观的会话 EJB,并将组成服务的组件集(即其他 bean,其中的某些 bean 可能是会话 bean 和实体 bean)“包装”起来。这样,服务请求程序将与服务实现的细节分隔开,且会话 EJB 将启用一个“虚拟功能”provide_Service() 方法,该方法不包含业务逻辑并公开一个通用(抽象化)接口。该体系结构方法解除了服务请求程序与服务接口之间的耦合,从而实现了组件的重复使用。它还可以在不同版本的服务存在不同的业务要求时,帮助管理 Web 服务的复杂性。

遗憾地是,在实践中,“外观”模式通常未得到充分地应用,尤其是在以下两种常见的情况下。第一种情况是,用户创建了一个直接包装子系统组件的所有方法的外观,但未提供任何其他抽象。这样的方法是否提供了所需级别的松散耦合?实际上并未提供。松散耦合必须减低总体复杂性,而在这种情况下,复杂性并未降低而只是转移到其他对象。

第二种情况与第一种情况正好相反—开发人员走向了另一个极端,即将会话 bean 中的每个用例包装起来,并向每个 bean 公开一个 Web 服务 API。这样做也不会获得任何好处;其结果是生成了许多体现相似行为的会话 bean(例如,GetAccountBalance、UpdateAccountBalance、TransferFunds、ListAllAccountBalances 等等),并且在应用程序中,我们需要密切注意所操作的会话 bean。每个用例实现一个会话外观所需的管理投入令人生畏。此外,复杂性不但没有降低,反而进一步增大。

要实现一个有助于实现其他一些有利设计可能性的理想松耦合状态,必须进一步改进分层:将上述的服务外观层划分为若干层(即子层),这些层按顺序进行排列,即层“n + 1”主要由层“n”的外观公开的服务组成(如图 4 所示)将外观嵌套(而非增加)可以对服务粒度进行管理—从粗粒度程度很高的服务到粗粒度程度中等的服务,再到细粒度服务。注意,粗粒度意味着更正式(通用)的接口;粗粒度服务接口提供了子系统(如订单管理、帐户管理、定价等)的抽象表示。此处的主要设计思想是采用一个足以满足需要的多个外观复合结构(其后隐藏了基础业务逻辑的所有细节)。只有该方法才能有效地帮助我们实现一个完全符合 SOA 的解决方案。


 图 4:J2EE 外观分层参考模型

然而,EJB 并不是唯一用作外观的结构。其他 Java 结构也可用于此目的—尤其是对象工厂(用作 JDBC 或实体 Bean 功能)和操作对象。本文主要介绍一个操作对象—一个根据服务激活者模式构造的对象—。当应用程序需要异步访问 EJB 以支持发布/预订或点对点消息时,服务激活者将负责接收异步请求、定位相应的 EJB 以及代理这些请求,还可以在成功完成请求处理后向请求者发送确认。在 J2EE,服务激活者对象执行 JMS 监听器和代理服务的角色,消息驱动 bean 通常用于此目的。

结构

那么,该解决方案的结构到底如何呢?如图 5 和图 6 所示,所有请求可以转到超级外观(称作 EJB 中介者),它用作 EJB 层的单个入口点,主要控制和同步对 EJB 层的并行访问。为响应请求,中介者按照事件处理器模式进行操作,并通过发送事件将相应的任务代理到通过其外观表示的不同的 EJB 子系统。更具体地说,对于服务请求,EJB 中介者将分解该请求,找到相关的服务外观,使用不同的 Web 服务接口发出子请求,然后撰写结果。如果多服务系统中存在复杂的服务请求,并在 EJB 体系结构的查找程序方法中嵌入了一组服务接口,则 EJB 中介者的任务是标识企业 bean、它们的通过 Web 服务公开的相关方法以及组合这些服务的方法。


 图 5:“电子商店”示例 - EJB 外观分层方案
 
 图 6:“电子商店”示例— EJB 中介者模型

EJB 中介者还有一个重要的功能:启用以文档为中心的服务与 RPC 样式服务。尽管仅该主题就需要一些专门的文章,但值得一提的是,通过在服务使用者与 EJB 层之间引入一个高级抽象层,我们现在能够有效地将会话外观模式与 XML 文档处理组合在一起。

EJB 中介者在其核心类中定义的一个最重要的接口是 IejbMediator 接口。它作为会话 bean 的远程接口(如下面的代码片段所示)实现,并定义了指定的 process_service() 方法。该方法是文档进程,它将 XML DOM(文档对象模型)文档作为它的输入参数并返回 XML DOM 文档:

/**
* imports
*/
import org.w3c.dom.*;
import java.rmi.RemoteException;
import .....

public interface IejbMediator
{
public Document process(Document domXMLInput)
throws RemoteException, xxxException;

}

现在,让我们回过头来介绍总体结构。对于客户体验管理子系统,它的 EJB 外观将所有事件转发到称作 EJB 控制程序的下一层外观,它是一个表示 EJB 层的客户体验管理子系统中的客户的会话状态会话 EJB。EJB 控制程序简化了处理并利用状态计算机模式控制应用程序状态。此外,EJB 控制器使用命令模式实现事件处理,并将创建或更改请求分派到下一层外观—逻辑 EJB 外观或实体 EJB 外观。逻辑 EJB 外观封装子系统的业务逻辑,而实体 EJB 外观提供了一个有效处理数据一致性的方法。

那么,使用这么多层的外观到底实现了哪些功能呢?首先也是最重要的是,使用外观嵌套可以封装参与工作流的服务器端业务对象中的复杂交互。它还可以保证高级别的可重用性—也许不是服务级别,但完全适用于业务对象。其次,该结构还提供了对负责客户交互的对象的快速访问。结构和简化算法使用外观嵌套处理“功能块”。每个块的关联粗粒度方法用于在层次结构中进行导航。此外,外观层次结构的垂直分层提供了高效的缓存功能和 EJB 集群。位于不同节点的 EJB 集群可以显著增强可伸缩性、可靠性和可用性。

您可能会问:外观模式与 EJB 集群之间存在什么关系?令人吃惊的是,有效的 EJB 集群在很大程度上取决于设计决策,尤其是外观结构—这主要是由于 EJB 集群需要很高程度的幂等性。(幂等性方法是可以使用相同参数重复调用并将相同结果存档的方法。)对于会话 bean,所有主要的 J2EE 应用服务器都只依靠 幂等性方法支持故障切换。在大型多功能 J2EE 系统中,外观嵌套允许建立大量 幂等性方法,从而实现了高效的 EJB 集群。

结论

在构建 J2EE 系统时应牢记 SOA 原则以便最大限度地增强灵活性和适应性。注意,SOA 不随 Web 服务和 SOAP 的启用而启动或停止。它需要通过建立多级 EJB 层分层(这将依次转换为灵活且经济有效的 SOA 实现)正确设计专用于实现更高级别的松散耦合的 J2EE 组件、功能自主和相关的 API。

 


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