文章
面向服务的架构
作者:Guido Schmutz
使用事件驱动的交互在面向服务的架构内实现极松散的耦合。
2011 年 7 月发布
本文探讨请求驱动和事件驱动解决方案针对同一问题的不同之处,并且讨论了每种方法的优缺点。本文还阐述了事件驱动交互与请求驱动交互相结合的混合方案。基于租车公司的小型示例用例展示了这种方案。
在现代软件解决方案中,可以识别和定义三种基本类型的交互 [1]:
在传统的面向服务的架构 (SOA) 中,请求驱动的交互样式很常见且被广泛使用。很明显,使用者调用的基于 SOAP 的 Web 服务是一种请求驱动的交互形式。
时间驱动的交互用于存在调度机制的时候。这在 SOA 中可以实现,例如,在对 Pick 活动或流程/作用域级别使用各种 OnAlarm 功能的 BPEL 环境中。
事件驱动的交互比较少见,尽管它们能够有效地帮助架构师提高面向服务的解决方案的灵活性和敏捷性。这对于达到 SOA 的如下核心目标又至关重要:提高业务和 IT 的一致性。
在此所展示的示例情况基于一个虚构的租车公司 RYLC Inc.。该公司推出了一项举措,着手将现有 IT 系统升级成基于面向服务的架构的系统。其发展规划的下一步就是创建新的集中式 Customer Management System。RYLC 选择了 Oracle SOA Suite 11g 作为其 SOA 平台。
新的 Customer Management System 将提供两种服务功能,一种是创建新客户,一种是检索现有客户。用于添加新客户的功能涉及执行业务流程,因为需要完成多个步骤,例如:检查客户的信用度、对客户进行评级,以及最后将客户数据插入客户数据库中。RYLC 计划使用 BPEL 来自动执行业务流程,以获得必需的灵活性并使流程与业务的变化相一致。
Customer Management System 接受一个新客户后,RYLC 现有的 Accounting System 必须收到通知。这将使用 Accounting System 现有的基于 SOAP 的 Web 服务接口来实现,此过程将被添加到业务流程步骤中。
图 1 在右侧显示了现有 Accounting System,在左侧显示了新的 Customer Management System。根据域清单模式 [2],这两个系统属于不同的域。

图 1:集成架构图显示链接到现有 Accounting System 的新的 Customer Management System
图 2 显示包含了部分 Customer Management System 实现的服务组件架构 (SCA) 组合图。在左侧可以看到,两项功能已经合并成一个服务并对外公开。CustomerManagement 调解器组件代表中央入口点(应用正式端点模式 [2])。
“新建客户”功能由 NewCustomer BPEL 组件实现。该 BPEL 组件引用 Accounting System 提供的外部服务以及封装了 Customer 实体上操作的 Customer Entity Service。

图 2:包含 Customer Management System 实现的 SCA 组合
图 3 显示 NewCustomer BPEL 组件的实现细节。可以看到逻辑分成不同的作用域。第一个作用域生成客户的唯一 ID。第二个作用域定义处理客户数据的逻辑(即检查信用度、分配评级、调用 Customer Entity Service 插入客户)。最后一个作用于域(用红色框起)调用 Accounting System 上的“添加新客户”Web 服务。

图 3:新建客户 BPEL 组件的实现
Accounting System 使用自己的数据模型表示客户,与 Customer Management System 中使用的模型不同。因此,在调用 Accounting System 上的服务之前,需要对数据进行转换(应用数据模型转换设计模式 [2])。这是在 BPEL 中使用经过改进的 Assign 活动完成的,该特性在 Oracle SOA Suite 11.1.1.4 及更高版本中提供,它提供了一种比早期 Oracle SOA Suite 版本更方便的多值映射方法。图 4 显示了转换结果。

图 4:通过 BPEL Assign 应用数据模型转换设计模式 [2] 来映射不同客户表示
但是对于执行大量数据模型转换,BPEL 通常并不是一个好办法。图 5 显示了一个更好的方法:在 BPEL 组件和外部引用之间放置一个额外的调解器组件 IntegrateAccountSytem。该调解器组件将负责 Customer Management System 与 Accounting System 之间必要的集成逻辑匹配,如本例中的数据模型转换。

图 5:额外添加调解器组件以封装必要的集成逻辑
调解器组件使用 XSL 转换从一个数据模型映射到另一个数据模型。图 6 显示了 JDeveloper 中所需转换的图形表示。

图 6:用于从客户域到帐目管理域的客户转换的 XSL 样式表
6 个月后,RYLC 决定向应用环境添加新的 Car Rental System。此系统是一个现成的商业解决方案 (COTS),它也需要知道添加到 Customer Management System 的新客户。这可以使用 Car Rental System 提供的基于 SOAP 的现成 Web 服务接口来实现。图 7 用绿色显示新的 Car Rental System,它调用 Customer Management System 中的“新建客户”Web 服务。

图 7:显示图中添加了新的现成系统的集成架构图
为了将新的 Car Rental System 与 Customer Management System 集成,我们需要更改 SCA 组合,在 BPEL 编排中添加对 Web 服务的调用。
如图 8 所示,首先要添加对 Car Rental System 的外部引用,然后使用一个额外的调解器组件将其与现有 BPEL 组件集成。

图 8:将第二个系统与额外的调解器组件集成并将其链接到 BPEL 组件
调解器组件将使用自己的 XSL 转换来处理 Car Rental System 中客户数据的数据模型差异。图 9 显示 Car Rental System 需要的信息比 Accounting System 少(与图 6 相比)。

图 9:到 Car Rental System 使用的 XML 数据模型的 XSL 转换
现在我们可以增强 BPEL 流程逻辑,使其包括对 Car Rental System 的调用,更确切地说是对封装它的调解器组件的调用。如图 10 所示,图中新添加了一个作用域 InformCarRentalSystem。

图 10:在编排中添加步骤
向 Customer Management System 添加对其他系统的调用并不困难。由于 BPEL 本身的灵活性,加上使用调解器组件中的数据模型转换,可以通过声明方式完成添加任务。
表 1 评估了业务扩展的正面和负面效果及其对技术解决方案的影响。
| 正面影响 | BPEL 本身的声明性使得附加服务调用成为一项轻松的任务。 |
| 负面影响 | 每次新系统需要了解新客户时,我们都必须更改客户域
|
| 负面影响 | 客户域需要了解另外两个系统/域使用的不同格式,以便实现必要的数据模型转换。
|
| 负面影响 | 尽管 Accounting System 是通过一个基于 SOAP 的标准 Web 服务集成的,但从 Customer Management System 的角度来看,这两个系统之间的耦合并非像人们希望的那么松散:
|
如何修复以上所介绍的解决方案的负面影响?如果 Customer Management System 不必将有关新客户的情况通知所有相关方,会不会更好些?如果我们只在 NewCustomer 业务流程结束时报告新客户已经成功添加到系统,又会怎样?
下一部分将使用事件驱动的交互样式对以上问题做出解答。
Oracle SOA Suite 11g 包括事件传递网络 (EDN),这是一种通过事件简化通信的机制,图 11 的中间部分显示了 EDN,左侧为可能的生产者,右侧为使用者。EDN 可以运行在 Oracle 数据库或 JMS 队列之上。

图 11:事件传递网络 (EDN) — 来自 Oracle 事件驱动的参考架构 [4]
图 12 显示了对图 1 所示集成架构的修订。不同的系统/域之间不再彼此直接通信。事件传递网络现在是架构的一部分,进一步分离了不同的系统/域。通信是通过发布/订阅机制完成的,只要客户被成功添加到系统中,Customer Management System 就通过该机制发布 NewCustomer 业务事件。所有相关系统均可订阅该业务事件,并在事件报告后立即收到通知(事件驱动消息处理设计模式的实现 [2])。

图 12:显示不同系统/域之间的事件传递网络 (EDN) 的集成架构图。
为了使用事件传递网络,首先必须创建业务事件,即发布者与 EDN 之间的合约。EDN 中的业务事件不需要 WSDL。事件是使用事件定义语言 (EDL)(Oracle SOA Suite 专用的 XML 方言)定义的。对于事件,没有公认的标准(如 WSDL),但在 SCA 规范中已经添加了用于事件处理的扩展 [5]。
图 13 显示了 JDeveloper 中事件的图形表示,清单 1 显示了对应的 XML 文档。

图 13:JDeveloper 中 NewCustomer 业务事件的图形化定义
事件的名称和内容都是通过一个 XML 模式元素定义的。我们的 NewCustomer 业务事件使用 Customer XML 模式元素,所以事件将始终传输有关客户的所有信息。因此,我们事件的粒度相当粗糙。稍后我们将讨论事件的粒度以及为什么对作为事件对象负载发送的信息量进行限制可能是一个好主意。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<definitions xmlns="http://schemas.oracle.com/events/edl"
targetNamespace="http://schemas.oracle.com/events/edl/CustomerEvent">
<schema-import namespace="http://www.mybank.org/Customer"
location="xsd/customer.xsd"/>
<event-definition name="NewCustomer">
<content xmlns:ns1="http://www.mybank.org/Customer"
element="ns1:Customer"/>
</event-definition>
</definitions> 清单 1:用事件定义语言 (EDL) 实现的业务事件的 XML 定义
有了 NewCustomer 业务事件之后,就可以更改 SCA 组合,使其不再与其他两个系统/域直接通信。如图 14 所示,已删除上个解决方案中的两个外部引用和两个调解器(对比图 8)。BPEL 组件已更改,这样它可以报告 NewCustomer 事件,如 BPEL 组件右侧的特殊闪烁图标所示。

图 14:BPEL 组件直接报告 NewCustomer 业务事件的 SCA 组合。
由 BPEL 流程内部发出的事件报告是通过 (BPEL) Invoke 活动完成的:我们将报告先前定义的业务事件,而不是通过基于 WSDL 的合作伙伴链接调用服务,如图 15 所示。

图 15:通过将 Invoke 活动用于对应的事件来报告 NewCustomer 业务事件。
仍然需要 Assign 活动 setupEvent(如图 15 所示,就在 Invoke 活动之前),它设置业务事件的负载,如图 16 所示。

图 16:使用 Assign 活动指定业务事件的负载(使用 Invoke 中指定的变量)
或者,还可以(再次)使用调解器组件报告业务事件,如图 17 所示。调解器右侧的闪烁图标再次指示报告 NewCustomer 事件。

图 17:业务事件还可以由调解器组件发布
如图 18 所示,BPEL 流程调用调解器组件上的服务操作,将事件发布委托给调解器组件。

图 18:BPEL 组件调用调解器上的服务发布业务事件
这就是 Customer Management System 上所需的全部内容。我们可以直接在 BPEL 流程中报告事件,也可以使用额外的调解器组件发布事件。如果尚无使用者/订阅者注册业务事件,这样也可以:BPEL 流程始终向 EDN 报告 NewCustomer 事件。
现在我们来添加业务事件的使用者。为此,我们创建两个新的 SCA 组合,一个针对 Accounting 域,另一个针对 Car Rental 域。
图 19 显示了 Accounting 域的 SCA 组合,其中包含一个调解器组件以及对 Accounting System 的外部引用。调解器组件已订阅 NewCustomer 事件,这同样由闪烁图标(现在位于调解器左侧)指示。

图 19:已订阅 NewCustomer 业务事件的调解器
订阅事件时,必须定义以下属性:事件名称、一致性、以发布者身份运行,以及筛选器(可选)(参见图 20)。

图 20:订阅的各种属性
可以定义筛选条件以便操作订阅事件的内容。在本例中,Filter 字段为空,因此我们将订阅所有 NewCustomer 事件。
一致性级别定义事件传递的服务质量 (QoS)。表 2 记录和描述了三种可能的级别。
| one and only one | 在自己的全局(即 JTA)事务中将一个事件传递给订阅者。订阅者在该事务中所做的任何更改都将在事件处理完成后被提交。如果订阅者失败,事务将回滚。失败的事件将按照配置的次数重新传递。 |
| guaranteed | 事件在没有全局事务的情况下以异步方式传递给订阅者。订阅者可以选择创建自己的本地事务来执行处理,但其提交独立于其余事件处理。事件保证会传递给订阅者,但因为不存在全局事务,有可能因系统失败导致事件多次传递。如果订阅者抛出异常(或发生任何失败),将记录异常,但不会重新发送事件。 |
| immediate | 事件在与发布者相同的全局事务和相同的线程中传递给订阅者。一直到所有立即订阅者完成处理之后,发布调用才会返回。如果有任何订阅者抛出异常,则不会再调用其他订阅者,并向发布者抛出异常。如果在立即处理期间发生任何错误,事务都将回滚。 |
表 2:不同级别的传递一致性 [3]
通过使用“one and only one”以及正确的错误处理策略,可以保证事件始终会成功处理,无论是直接成功还是通过多次重试。这一选择的主要结果是 Customer Domain 不再需要承担确保通知被成功传递的职责。而是每个订阅域需要负责确保业务事件被成功处理。如果给定系统无法保证与 Oracle SOA Suite 平台同样的可用性,可能需要在将事件发送到系统之前额外存储业务事件(即在 JMS 队列中)。
Rental Domain 的 SCA 组合以同样的方式完成。图 21 显示三种不同的 SCA 组合,每个域一种,它们通过事件传递网络实现分离。通知到 EDN 的事件将以透明方式传输给订阅此类事件的所有各方。

图 21:通过事件传递网络 (EDN) 实现分离的不同 SCA 组合
至此,我们通过这种事件驱动的交互实现了什么?表 3 评估了事件驱动的解决方案的优缺点。
| 正面影响 | Customer Management System 与其他系统高度分离:
|
| 正面影响 | Customer Management System 不必再将客户的表示转换成所有其他系统所用的格式。它在事件的负载中以自己的格式传递客户,然后由订阅该事件的系统负责将其转换成所需格式。 |
| 负面影响 | 事件驱动的交互只是单向的,因此不能替换请求响应交互。 |
| 负面影响 | 订阅者不是持久的;事件传递网络 (EDN) 只将通知的业务事件传递给 SCA 组合(如果它已启动并正在运行)。 |
| 负面影响 | 对将什么信息发送到哪些接收者缺乏精细控制。整个客户表示是作为事件的负载发送的。从客户管理的角度来看,我们不再了解使用者、哪个表示使用者,这些意味着控制缺失。获得控制是事件粒度的问题。 |
与设计服务功能时的粒度问题类似,我们也需要考虑事件的粒度。业务事件应包含何种信息?是否需要包含客户所有信息(如地址)的完整客户表示?还是仅传递事件订阅者(如果有的话)根据需要请求进一步信息所必需的信息量?这一问题可能同时影响系统性能和数据安全。
从 Customer Domain 的角度看,我们可能对在业务事件中发布关于客户的全部信息不感兴趣。如果只发送客户 ID,我们就可以根据不同的使用者类别/安全级别提供特殊服务合约(应用并发合约设计模式 [2]),然后订阅者可以使用该合约获取其他信息。
图 22 显示了在事件负载中只发送客户 ID 时集成架构中会发生的变化。始自 Accounting System 和 Car Rental System 的红色箭头表示对丰富事件信息的 Retrieve Customer 功能的服务调用。

图 22:丰富缺失信息的集成架构
清单 2 显示含有新的业务事件 NewCustomerMinimal 的事件定义,它在负载中仅使用客户 ID。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<definitions xmlns="http://schemas.oracle.com/events/edl"
targetNamespace="http://schemas.oracle.com/events/edl/CustomerEvent">
<schema-import namespace="http://www.mybank.org/Customer"
location="xsd/customer.xsd"/>
<event-definition name="NewCustomerMinimal">
<content xmlns:ns1="http://www.mybank.org/Customer"
element="ns1:ID"/>
</event-definition>
</definitions> 清单 2:用事件定义语言 (EDL) 实现的 NewCustomerMinimal 业务事件的 XML 定义
图 23 显示了 Accounting System 的 SCA 组合中的订阅变更。如果我们订阅 NewCustomerMinimal 事件,首先必须调用 Customer Management System 通过 RetrieveCustomerByID 操作获取其余客户信息。之后即可像以前一样调用 Accounting System。

图 23:订阅 NewCustomerMinimal 事件、丰富信息然后调用 Accounting System
图 24 显示调解器组件的路由规则。收到业务事件之后,对事件数据进行转换,然后将其发送给 RetrieveCustomerByID 操作。随后将这一请求的结果(即客户信息)转换成 Accounting System 的 AddNewCustomer 操作所认可的格式。

图 24:显示为获得丰富信息而调用 RetrieveCustomerByID 操作的调解器组件的路由规则
图 25 显示了与前面步骤中使用的转换类似的转换(与图 6 相比)。

图 25:丰富信息到 Accounting System 格式的转换
图 26 显示了 Customer Management System 的 SCA 组合,其中实现 RetrieveCustomerByID 操作并委托 CustomerEntityService 检索数据。

图 26:通过使用 CustomerEntityService 实现 RetrieveCustomerByID 操作的 Customer Management System 的 SCA 组合
此流程还使用请求驱动的交互以便操作丰富信息并调用 RetrieveCustomerByID 操作?但是,这次请求是由 Accounting Domain 而不是 Customer Management System 发起的。所以更强的耦合是在事件的使用端,以便检索有关事件的更多信息。因此,耦合被逆转,我们基本上是混合事件驱动交互与请求驱动交互以控制系统耦合。
本文介绍了两种解决方案,用于实现在 Customer Management System 中创建新客户后通知相关方的要求。
第一个解决方案演示了使用请求驱动交互实现的传统面向服务的方法。所涉及系统的耦合相当紧密,结果可能导致相继发生一系列问题。
将请求驱动的方法替换为事件驱动的交互样式(如第二个解决方案所述)有助于分离不同系统和域,从而产生更灵活、更敏捷的架构。
当然,您不应将所有请求驱动的交互替换为事件驱动的交互。松散耦合在系统边界处(即在域清单的服务域之间)尤为重要,如图 27 所示。

图 27:通过事件使服务域分离
[1] Mani Chandy 和 Roy Schulte,《Event Processing - Designing IT Systems for Agile Companies》(McGraw-Hill,2010 年)。
[2] Thomas Erl 等人,《SOA Design Patterns》(Prentice Hall PTR,2009 年)。
[3] Antony Reynolds 和 Matt Wright,《Oracle Fusion Middleware Developer's Guide for Oracle SOA Suite 11g Release 1》(Packt,2010 年)第 36 章“使用业务事件和事件传递网络”。
[4] Oracle 参考架构 — 事件驱动的架构基础,2010 年。Oracle 文档库中的 IT 战略部分。
[5] SCA 服务组件架构组合件模型规范 — 事件处理和发布/订阅扩展,2009 年。
Guido Schmutz 是 Oracle ACE 总监,并且是总部位于瑞士的独立 IT 咨询公司 Trivadis 的 SOA 及新趋势的技术经理。他与人合著了《Service-Oriented Architecture:An Integration Blueprint(Packt Publishing,2010 年)。他经常在 http://guidoschmutz.wordpress.com 上撰写博客。