文章
面向服务的架构
2011 年 10 月发布
Oracle SOA Suite
DecoupledSpringPaper.zip
(包括本文所述组件的示例代码。)
本文面向那些实现和设计面向服务的 Spring 应用程序的软件开发人员和软件架构师。它还面向那些使用服务组件架构 (SCA) 组合向服务界公开其现有业务逻辑的 Oracle SOA Suite 开发人员。读者应熟悉面向服务架构 (SOA) 的原则、WebService、Java、Spring、XSLT 和 Oracle SOA Suite 11g。
2002 年底,Rod Johnson 在《Expert One on One J2EE》一书中首次介绍了作为 Enterprise JavaBeans (EJB) 架构的轻型替代品的 Spring Framework [Wiki]。从那时起,不同的公司和组织开发了基于 Spring 的应用程序,在其中定义了多个处理业务逻辑的 bean。其中许多 bean 旨在作为独立的组件。不过,为了将独立开发的组件用于新的应用程序或用于开发业务服务,大多数开发人员在应用配置更改和实现额外的适配器逻辑以便在这些组件之间进行通信时,一般会将这些 Spring 组件非常紧密地耦合在一起。使用共享业务对象、公共库或公共消息类型意味着无法在不影响多个其他部分的情况下轻松更改接口和数据类型。因此,Spring bean 是紧密耦合的,难以治理和重用。
为解决此问题,可以将组件开发方法与 SCA 标准结合使用,以产生扩展性更强的替代方案。使用 SCA,我们可以有一个灵活的轻型模型来组合独立组件和应用 SOA 原则。记住,使用 SCA,我们不一定要使用 SOAP 上的 Web 服务或任何其他专用技术;Spring bean 就已足够。我们可以直接组合轻型 Spring bean 并从其他独立应用程序或服务调用它们,这样不会在 SOA 环境中产生大量开销。
本文的主要目的是提出一种基于轻型 SOA 的设计,包括使用 SCA 进行交互的完全分离的 Spring 组件。本文分为两个主要部分。第 1 部分介绍设计并描述其主要组件,第 2 部分介绍一个测试用例,包含 Oracle SCA 引擎(Oracle SOA Suite 的一个组件)对应的源代码。在总结中,就影响和范围对结果进行了分析。
这种方法背后的主要思想是提供一种方式来定义两个或多个完整、独立的 Spring 组件,它们无需依赖外部或第三方对象即可实现彼此通信。此方法遵循服务设计原则,无需修改源代码或其他外部实体即可轻松交换和重用已定义的 Spring 组件。为实现这一目标,在定义 Spring 组件之间的通信之前,首先必须了解这些组件应遵循的设计模式。
每个 Spring 组件可以定义两个接口:一个用于接收传入消息,另一个(可选)用于发送消息。每个传输对象(相对于每个接口)都应属于一个且只属于一个组件,并且应对其余组件保持未知。所有组件甚至在物理层就应保持完全分离,因此,组件之间不应共享类文件。图 1 显示了这种方法的类图。

图 1 的类图显示在应用程序内部组合以增添新功能的两个独立 Spring 组件之间的平行关系。尽管这两个组件由完全不同的团队或公司开发,但它们遵循相同的设计原则,在此要再次强调,两个组件都不能共享任何对象或配置。
组件设计可分为两个交互流:从公开函数接收对象,在完成内部业务逻辑实现之后返回对象。也可能还有其他交换模式。
接收部分由一个接收接口、两个传输对象和一个实现内部逻辑的类组成。在本示例中,接收方接口(图 1 中的 IReceiverA 或 IReceiverB)定义输入方法,它使用两个传输对象,一个实际接收消息 (ReceivedObject),另一个将结果返回到发送方 (ReturnedObject)。这两个对象代表传送的实际信息,它们都应遵循 JavaBeans 约定。该类实现接收方接口并应执行或委托其他 Spring bean 执行业务逻辑。如果组件需要向另一个 Spring 组件发送消息,它还应定义设计的第二个(可选)部分。
设计的第二部分与第一部分类似。它包含一个发送方接口、两个传输对象,但没有 实现类。这里的差别在于实现类(图 1 中的 ReceiverImplA 或 ReceiverImplB)与两个接口之间的关系。ReceiverImpl 是一个 IReceiver,它有(同样是可选地)一个 ISender。如果组件不与其他 Spring 组件通信,它就不应有 ISender 接口。与 IReceiver 类似,ISender 接口定义一个发送方法并使用两个传输对象,一个发送信息,另一个获取结果。
现在的主要问题是:如何才能在使用这两个 bean 时公开一个新业务服务接口,以及信息如何从一个组件到达另一个组件?换句话说,如何以基于轻型服务的方式将两个组件绑定在一起而不会使 SOA 难以承受?
由于 Oracle SOA Suite 11g 中新推出的 SCA 架构,使用基于 Spring 的 SCA 容器实现,现在可以将此实现留给其中一个 SOA 组件来完成。在本文中,SCA Mediator 组件将实现 ISender 接口,它将以可视化的方式把消息从一个 Spring 组件传输到另一个 Spring 组件,同时在外部执行必要的转换以调整组件特定的消息。这样一来,每个组件都可以与其他组件通信,而无需依赖这些组件的实现。图 2 显示此设计在 SCA 中的样子。

图 2 显示一个实现了图 1 中设计的组合。其中有三个 Spring 组件和一个调解器。测试用例中将显示的正是这些组件。
BookstoreShipping Spring 组件公开一个基于其 IReceiver 接口的 Web 服务描述语言 (WSDL) 接口,并使用其 ISender 接口与调解器通信。该调解器实现此 ISender 接口并使用 inlandShipping 组件和 overseasShipping 组件的 IReceiver 接口传递和路由消息。
此模型通过使用某些 Spring 应用程序上下文特性和 SCA Spring 扩展来实现。由于每个 Spring 组件都是独立的,因此每个组件都应有自己的应用程序上下文配置文件。与所有 Spring 应用程序类似,配置文件定义 bean 及其属性。如果组件向另一个组件发送信息,则其属性之一应是对目标的引用。换句话说,应如下所示注入 ISender 属性:
<bean id="receiverA" class="com.decoupledspring.demo.ReceiverImplA"> ... <property name="senderProperty" ref="sender"/> ... </bean>
ReceiverA bean 的配置文件将指定发送方属性使用对发送方对象的引用,但并不定义发送方对象。发送方对象的定义将在 SCA 组合自身内完成。
该组合定义多个 Spring 应用程序配置文件,每个 Spring 组件一个。如果组件没有 ISender 接口,它就应该只导入组件的应用程序上下文并声明其公开的服务。如果组件有 ISender 接口,则应导入应用程序上下文并使用如下一些 SCA 扩展进行扩展:
此扩展提供一个名为 sender 的 bean,并可将其注入先前定义的 sender 属性。然后该组合将定义一个连接,以便将 SCA 引用绑定到调解器:
<wire>
<source.uri>ReceiverASCA/sender</source.uri>
<target.uri>MediatorSCA/MediatorSCA</target.uri>
</wire>
这个测试用例实现了一家书店的示例交货流程。该书店依赖于两个外部货运公司。如果订单是在国内,则联系国内货运公司,但如果订单需要海外运输,就联系海外货运公司。
当书店的货运部门收到某书的订单时,它会将订单转交给 SCA Mediator 组件,该组件然后决定将使用海外还是国内货运组件,并根据情况执行必要的转换。
书店和交货组件遵循图 1 中所示的设计。因此所有组件均实现 IReceiver 接口,但只有书店组件需要引用 ISender 接口。
该测试用例是使用 Oracle JDeveloper 11g 和附加的 Oracle SOA Suite 插件开发的。
测试用例的第一步是按照前面所述的设计创建独立的 Spring 组件。为确保组件真正独立,这三个组件在同一应用程序的不同项目中进行编码。组件还可以在不同的应用程序中开发,因为最终所有组件都导出到 JAR 文件并导入 SCA 组合。
上述的书店组件必须实现以下所有 Java 构件:ReceivedObject、ReturnedObject、IReceiver、ReceiverImpl、ISender、SendedObject 和 ObtainedObject。该组件分成三个部分。第一部分接收订单,第二部分转发订单,第三部分将其他两个部分绑定在一起同时执行某些轻型业务逻辑。
ReceivedObject 构件的 BookOrder 对象是一个简单传输对象,它使用以下属性表示传入订单:package com.decoupledspring.demo.bookstore.model。
public class BookOrder {
private String bookName;
private String isdn;
private String price;
private boolean inland;
private String address;
private String buyer;
private String country
.....
OrderResult(表示 ReturnedObject 的类)也是一个遵循 JavaBeans 约定的传输对象。它只有一个属性用于处理交货状态(成功或失败)。
这两个对象全部生成之后,就定义了 IBookShipping(IReceiver 接口),它公开用于接收订单的方法(ReceivedObject 类的一个实例)并返回相应的结果(ReturnedObject 类的一个实例)。
public interface IBookShipping {
public OrderResult shipBook(BookOrder bookOrder);
}
发送部分与接收部分非常类似。它要求定义两个传输对象:BookShippingOrder (SendedObject) 和 BookShippingResult (ObtainedObject),以及一个名为 IShippingOrder (ISender) 的接口。BookShippingOrder 使用以下属性表示交货订单:
public class BookShippingOrder {
private String bookName;
private String isdn;
private String priceinEuros;
private String priceinDollars;
private boolean inland;
private String shippingAddress;
private String buyer;
private String shippingCountry;
同样,BookShippingResult 有一个属性代表交货成功与否,IShippingOrder 定义了一个方法,该方法以 BookShippingOrder 的一个实例作为输入,以 BookShippingResult 的一个实例作为输出。
public interface IShippingOrder {
public BookShippingResult sendShippingOrder(BookShippingOrder bookShippingOrder);
}
第三部分涉及定义 ReceiverImpl(在本测试用例中名为 BookShippingImpl)。它实现了 IBookShipping 接口并包含对 IShippingOrder 接口的引用。这个类的主要目的是在订单发送之前执行某些业务逻辑。
public class BookShipping Impl implements IBookShipping {
private IShippingOrder shippingOrder;
public BookShippingImpl() {
super();
}
public OrderResult shipBook(BookOrder bookOrder) {
BookShippingOrder bookShippingOrder= new BookShippingOrder();
bookShippingOrder.setBookName(bookOrder.getBookName());
bookShippingOrder.setBuyer(bookOrder.getBuyer());
bookShippingOrder.setInland(bookOrder.isInland());
bookShippingOrder.setIsdn(bookOrder.getIsdn());
bookShippingOrder.setPriceinDollars(this.getPriceinDollars(bookOrder.getPrice()));
bookShippingOrder.setPriceinEuros(bookOrder.getPrice());
bookShippingOrder.setShippingAddress(bookOrder.getAddress());
bookShippingOrder.setShippingCountry(bookOrder.getCountry());
BookShippingResult result= shippingOrder.sendShippingOrder(bookShippingOrder);
OrderResult orderResult = new OrderResult();
orderResult.setResult(result.getShippingResult());
return orderResult;
}
生成所有构件之后,唯一剩下的一件也是关键的一件事就是使用其对应的应用程序上下文定义将此服务定义为一个 Spring bean:
<bean id=" bookstore " class="com.decoupledspring.demo.bookstore.service.impl.BookShippingImpl">
<property name="shippingOrder" ref="shippingOrder"/>
</bean>
为了进行交互,bookstore bean 必须声明一个对 shippingOrder bean 的引用,但考虑到分离的原因,此引用链接不是在 Spring XML 定义中完成的。稍后将在 SCA 组合中定义这一引用链接。完成所有这些任务之后,bookstore 被导出到一个 JAR 文件以便稍后将其包含在 Oracle SOA Suite 11g 中。
国内和海外交货组件与书店组件非常类似。主要差别在于交货组件只包含一个接收部分,因为它们无需转发订单。也就是说,它们只定义 ReceivedObject、ReturnedObject、IReceiver 和 ReceiverImpl 构件。
在国内组件中,ReceivedObject 名为 InlandShippingOrder 并具有以下属性:
public class InlandShippingOrder {
private String bookName;
private String price;
private String shippingAddress;
private String buyer;
InlandShippingResult 类 (ReturnedObject) 只有一个属性,它代表事务的结果。IReceiver 接口(在本例中即 IInlandShipping)声明了一个方法,它以 InlandShippingOrder 作为输入,以 InlandShippingResult 作为输出:
public interface IInlandShipping {
public InlandShippingResult sendBook(InlandShippingOrder inlandShippingOrder);
}
IInlandShipping 类 (InlandShippingImpl) 实现一个非常简单的业务逻辑,只返回一个表示成功的消息。在更复杂的情况中,它可能将订单存储在数据库中或调用一个外部 Web 服务,但本测试用例中不执行这些任务。
public class InlandShippingImpl implements IInlandShipping{
public InlandShippingImpl() {
super();
}
public InlandShippingResult sendBook(InlandShippingOrder inlandShippingOrder) {
String result = "Book " + inlandShippingOrder.getBookName();
result+=" successfully deliver to " +inlandShippingOrder.getBuyer();
InlandShippingResult shippingResult= new InlandShippingResult();
shippingResult.setShippingResult(result);
return shippingResult;
}
}
此组件的 Spring 定义也非常简单,因为它没有对任何外部元素的引用:
<bean id="inlandShipping" class="com.decoupledspring.demo.inland.service.impl.InlandShippingImpl"/>
在海外组件中,ReceivedObject 名为 OverseasShippingOrder,它定义了以下属性:
public class OverseasShippingOrder {
private String bookName;
private String priceinEuros;
private String priceinDollars;
private String shippingAddress;
private String buyer;
private String shippingCountry;
与其他组件一样,ReturnedObject(或 OverseasShippingOrder)只有一个属性,它返回事务的结果。IOverseasShipping 对象代表 ISender 接口,它使用这两个对象定义用来接收订单的方法:
public interface IOverseasShipping {
public OverseasShippingResult deliverBook(OverseasShippingOrder shippingOrder);
}
OverseasShippingImpl 类实现 IOverseasShipping 接口,它负责海外交货组件的业务逻辑。同样,这里实现的逻辑也非常简单,因为测试用例的目的只是展示如何集成组件,而不是展示组件的内部行为:
public class OverseasShippingImpl implements IOverseasShipping{
public OverseasShippingImpl() {
super();
}
public OverseasShippingResult deliverBook(OverseasShippingOrder shippingOrder) {
String result= shippingOrder.getBookName() +" successfully deliver to ";
result+=shippingOrder.getShippingAddress();
result+=" at " + new Date();
OverseasShippingResult osr= new OverseasShippingResult();
osr.setResult(result);
return osr;
}
}
此组件的 Spring 定义与用于国内组件的类似,因为它同样没有任何外部引用:
<bean id="overseasShipping" class="com.decoupledspring.demo.overseas.service.impl.OverseasShippingImpl"/>
这两个组件也都导出到 JAR 文件以便可以在 SCA 组合中作为库使用。
SCA 组合的实现涉及两个主要活动:定义调解器和导入并绑定 Spring 组件。Spring 组件已经导出到 JAR 文件,因此现在需要将它们导入到两个地方。
第一个是典型的 Oracle JDeveloper 项目中的库的导入,在本例中,是导入 SCA 组合项目。第二个是 SCA lib 目录,它位于 <SCA-Project-Home>/SCA-INF/lib,其中 <SCA-Project-Home> 代表保存 SCA 组合的路径。这三个 JAR 文件必须手动复制到此目录,且之后必须重新启动 Oracle JDeveloper。
导入 JAR 文件后,就可以在 SCA 组合内部定义 Spring 组件。为此,必须在组合内部定义三个 Spring bean,每个开发的组件一个。
一般来说,在 SCA 组合内部添加 Spring bean 时,Oracle JDeveloper 会允许您选择生成一个空的新配置文件或使用现有的配置文件。尽管书店组件已经有 Spring 配置文件,还是会生成新的空配置文件,以便将其用作包装器,这样就可以添加 SCA Spring 扩展。
Spring bean 配置文件修改包括三部分。第一部分导入已经定义的 bean(bookstore、inlandShipping 和 overseasShipping)。
第二部分为每个组件定义一个公开的服务。在 bookstore bean 中,公开的服务基于 IBookShipping 接口。对于 inlandShipping 和 overseasShipping bean,公开的服务分别基于 IInlandShipping 接口和 IOverseasShipping 接口。inlandShipping 配置文件应类似以下所示:
<!--Spring Bean definitions go here-->
<import resource="classpath:com/decoupledspring/demo/inland/resources/inlandShipping.xml"/>
<sca:service name="InlandShipping" target="inlandShipping"
type="com.decoupledspring.demo.inland.service.api.IInlandShipping"/>
</beans>
第三部分也是最有趣的部分,是定义 bookstore bean 所使用的外部服务(在本例中为 shippingOrder)。如前所述,书店组件并不知道哪个组件实现 IShippingOrder 接口。
现在,该实现将被声明为一个 SCA 引用,并将在 SCA 组合内部连接起来:
<!--Spring Bean definitions go here-->
<import resource="classpath:com/decoupledspring/demo/bookstore/resources/bookstore.xml"/>
<sca:service name="BookstoreService" target="bookstore"
type="com.decoupledspring.demo.bookstore.service.api.IBookShipping"/>
<sca:reference name=" shippingOrder " type="com.decoupledspring.demo.bookstore.service.api.IShippingOrder"/>
</beans>
定义完这三个 bean 之后,SCA 组合应如图 3 所示。

最后一部分是向 SCA 组合添加调解器,拖动组件的箭头将其绑定在一起,并定义消息之间必要的转换。
添加完调解器之后,就需要将 inlandShipping bean 的左箭头和来自 overseasShipping bean 的左箭头拖动到来自调解器的右箭头,使这两个组件成为调解器消息的目标。
类似地,必须将来自 bookstore bean 的右箭头拖动到调解器的左箭头,使 bookstore 成为消息源并使调解器成为交货订单引用的实现。
生成的代码显示绑定是如何完成的:
<wire>
<source.uri>BookstoreSCA/shippingOrder</source.uri>
<target.uri>MediatorBookstore/MediatorBookstore</target.uri>
</wire>
最后一步是定义转换,这样就可以在交货组件中接收书店组件发送的信息。使用 XSLT 和 XPath 表达式,在调解器中分别定义转换和路由规则(记住,有些交货订单是针对国内客户的,其他则是针对海外客户的)。这一步完成之后,就可以部署 SCA 组合并在 Oracle 融合中间件中对它进行测试。
SCA 架构为软件开发人员提供了基于与技术无关的 Java 类设计和实现企业应用程序的机会,并为他们提供了将企业应用程序作为具有 SOA 全部优点的可互换、可重用组件贡献出来的机会。这些组件可以是来自 Oracle SOA Suite 的标准组件(例如 BPEL Process、HumanTask 和 Business Rules Engines),也可以是自开发的组件,如 Spring 组件。此架构背后的主要思想是允许软件开发人员组合任何组件而无不必产生庞大的开发和维护成本。
在组合内部包含 Spring bean 是通过直接方式完成的,只需定义应用程序 bean 配置文件。SCA Spring 功能为 Spring 框架增添了两个新标记:sca-reference 和 sca-service。使用这些新标记,现在可以将 Spring 组件与 SOA 组件(如业务流程执行语言 (BPEL) 和业务流程模型和标注 (BPMN) 流程和调解器)集成,并在之后将所需协议(如 POJO、RMI、SOAP 等)装饰到公开的接口上。
通过利用 SCA Mediator 组件特性(如消息和 bean 对象转换及消息路由),可以定义一个 Spring bean 集成方案,其中定义和集成多个独立的 Spring 组件,并保持它们相互独立。Spring bean 可以进行通信而无需共享对象定义或任何其他资源。
借助 Oracle SOA Suite 的 SCA 架构及其原生 Spring 支持,可以重用和编排不同生产应用程序所开发 Spring 组件,而无需开发这些组件的新版本。SCA 组合是组件之间的主要“粘合剂”,软件开发人员可以专注于 Spring 组件的功能,而不是将时间浪费在考虑组件之间的各种依赖关系上。此外,可以在 Spring 项目中添加 Oracle SOA Suite 的强大功能(如故障策略、动态路由、可视化设计和基于模块的组合部署),使得这些项目更加灵活和强健。