随着面向服务的体系结构
(SOA) 越来越受到人们的重视,以及 Web服务规范系列的复杂性不断增加,对于如雷贯耳的企业服务总线
(ESB) 之类的术语难免会感到困惑。它是一个产品?还仅是一个营销术语?它是否代表了可以实际使用的实实在在的东西?它如何与已经存在的其他所有
Web 服务和 SOA 概念相适应?在本文中,我们将就此进行讨论,至少解决其中的一部分问题。
本文说明了 ESB 在应用程序集成和面向服务的体系结构中所扮演的角色。本文并不专门讨论特定的功能、API
或产品,而主要讨论可以封装在 ESB 内并由其进行处理的集成体系结构的责任。本文说明了 ESB 对于架构师的意义。
面向服务的体系结构模式
首先,让我们澄清这个模棱两可的概念——所谓的企业服务总线 (ESB)。首先,它不是一个产品;其次,它不是一个虚幻的营销术语,不是专门用来诱导您购买更多软件的。相反,企业服务总线这一术语指的是可以应用于基于
SOA 的应用程序的实现的一种体系结构样式。为了说明这一点,有必要简要概述创建基于服务的系统的各种方式。
Web 服务
谈到“Web 服务”,人们通常会想到面向对象样式的接口,其方法是通过 HTTP POST 操作,使用来回同步传递的
XML 编码的 SOAP 消息进行调用的。服务器上的某个对象提供某些方法,每个方法都有一组输入参数和一个输出值。客户机创建编码输入参数的
SOAP 消息,并将此消息发送到某处的 HTTP 端点;然后,某段服务器代码将对输入参数进行解码,调用基础对象中的某个方法,并将该方法的返回值编码为另一个
SOAP 消息,该消息将在 HTTP 响应消息的主体中返回。这一远程过程调用 (RPC) 流程(如图 1
所示)通常称为远程过程调用样式(RPC 样式)的 Web 服务。
图 1:一个简单的 RPC 样式的 Web 服务
RPC 样式的 Web 服务模式具有以下几个特点:
1. 所有操作都是同步的。与对面向对象的方法或过程函数调用操作一样,在处理请求并生成响应时,将使用 RPC
样式的 Web 服务体系结构模式块调用操作。只有在接收到响应后,才会继续执行线程。
2. 所有操作通常都是由一个单独的对象实例提供支持。RPC 样式的 Web 服务通常都采用相同的基本实现模式。某个对象定义一系列方法。当所谓的
SOAP 引擎接收到请求时,它将对其进行分析,并识别要调用其方法的对象。对象的一个实例将被动态创建,并将在操作完成后被销毁,或者使用独立实例(并重用于每个请求)。
3. SOAP 消息对参数进行编码,并返回值。来回传递的 SOAP 消息是参数集合和返回值的 XML
编码表示。消息的结构通常紧密地绑定到由基础对象实现调用的方法的签名。
考虑到这些特点,RPC 样式的 Web 服务最佳的处理方法通常是,将其当作开发人员可用的其他类型对象进行处理。唯一的区别在于,在对象和其客户机之间插入了一个独立于平台的通信层。
服务接口
在传统的面向对象 (OO) 设计中,通常让多个对象实例实现相同的接口。每个实现类本质上都是不同的提供者,均提供与接口的其他实现相关的独特功能。例如,清单
1 显示了四个均实现 Math 接口的类。
清单 1:面向对象的接口
interface Math {
double invoke(double x, double y);
}
class Add implements Math {
double invoke(double x, double y) {
return x + y;
}
}
class Subtract implements Math {
double invoke(double x, double y) {
return x - y;
}
}
class Multiply implements Math {
double invoke(double x, double y) {
return x * y;
}
}
class Divide implements Math {
double invoke(double x, double y) {
return x / y;
}
}
在我们的客户机代码中,只要希望使用 Math 接口,就需要确定要绑定到哪个特定的实现实例上。这个选择将在很大程度上取决于我们要实现的目标:如果希望执行加法操作,将绑定到
Add 类的实例;如果希望执行减法操作,则绑定到 Subtract 类;依此类推。图 2 所示的客户机多次调用了同一个
Math 接口;每次调用均绑定到不同的实现。
图 2:具有多个接口实现类的典型对象序列关系图
现在,假设我们在处理相当典型的 RPC 样式的 Web 服务,该服务实现了检查给定股票当前价格的操作。假如让三个不同的服务提供者实现同一个
WSDL 定义端口类型(假定服务提供者分别来自是股票交易所 Schwab、Fidelity 和 E*TRADE)。如果忽略方法调用是通过在
HTTP 连接上来回发送 SOAP 消息实现这一事实,客户机和服务提供者之间的交互实际上与图 3 所示的情况完全相同。
图 3:具有多个服务提供者的典型 RPC 样式的 Web 服务调用
发现与集成
还需要明确一点,RPC 样式的 Web 服务所面临的发现与集成问题与传统的对象方法调用模式所遇到的问题完全一样,即:
1. 客户机如何识别给定通用接口的多个实现类?
2. 如何根据各种运行时选择标准动态地选择多个实现类?
3. 如何分离操作请求和响应,以使得在接收到响应之前不必阻塞线程的执行?
4. 在出现不完全符合由面向对象的方法模式所公开的典型同步请求-响应模式的情况时,如何处理?
熟悉 OO 设计原则的人都知道,很久以前就通过设计模式的创建与应用对这些问题进行了处理。例如,作为对对象实例调用某个
GetQuote 方法的备选方案,客户机代码可以将 GetQuote 消息传递到输入队列中。侦听发送到输入队列的消息的某个后端守护程序进程将接收此消息,并将其分派到可以处理此消息的某段代码。处理代码将处理请求,并准备要发送到输出队列的响应消息。客户机代码将轮询输出队列以获得响应,或等待响应消息交付时的回调通知。
图 4 显示了一个客户机调用方法,它将请求放入输入队列,并从输出队列获得响应。同时,一个调度器将检测请求,并调用实现,该实现运行方法,并将输出放到输出队列中。
图 4:典型的消息队列序列关系图
这种形式的面向消息的异步编程模型很早就提出了,当然在企业应用程序开发中广泛实现。这主要是因为,对于大型应用程序而言,这个模型比直接对象方法调用更易于扩展,更具灵活性。
总的来说,与面向对象的开发(开发人员在实现应用程序时可以使用很多不同的体系结构模式)一样,SOA 也提供了很多不同的方法,用以实现面向服务的应用程序,而这些方法并没有采用在
RPC 样式的 Web 服务中所看到的同步请求-响应模式。ESB 就是这些可选实现模式之一。
ESB 体系结构模式
ESB 是调用服务的客户机和这些服务的提供者之间的中介,它负责处理它们之间的连接任务,从而简化了客户机和提供者。IBM
的标准与开放源代码副总裁 Robert Sutor 在其博客中描述了 ESB 必须体现的八个定义原则:
通用性——提供跨整个扩展企业环境的连接层
异构性——提供面向消息的多平台、多协议和多 API 支持层,能够整合异构系统
互操作性——提供对开放协议的支持,支持来自多个供应商的系统之间的互操作
增量集成——提供按需求动态扩展系统的能力
服务质量——提供各种服务质量,如安全、性能、可靠性、可伸缩性等等
替换——使用开放 API 以确保能够替换供应商的实现
事件定位——提供将生成业务事件的应用程序和提交与响应这些事件的应用程序分离的能力
服务定位——通过遵循 SOA 设计方法论,以将关键功能抽象为服务的方式来提供分离应用的功能。
在技术层面上,这八点实际上表明,不要采取让客户机直接与服务提供者通信的方式,而要通过面向消息和事件的中间件系统对请求进行路由,该中间件系统将处理定位服务提供者、与服务提供者协商集成、与服务提供者交互等全部具体细节。
图 5 显示了一个在调用服务的客户机和该服务的提供者之间进行协调的 ESB。
图 5:企业服务总线序列简图
但“处理全部具体细节”到底是什么意思呢?通过许多企业 Java 开发人员都非常熟悉的一个类似概念,可以很好地回答这个问题。
接口的选择和实现
选择要调用的服务提供者与选择要使用的接口实现非常相似。回到前面提到的 Math 接口示例中。在该示例中,一个接口由四个不同的类实现。当
Java 开发人员使用 Math 接口的一个或多个实现时,首先必须知道接口有哪些实现可用。选项如下:
1. 编写代码时,文档的某个部分(如 javadocs)会提供实现类列表。我从中选择一个,并创建一个代码要使用的对象实例,如下所示:
Math add = new Add();
2. 我们以特定的方式编写代码,以在运行时根据选择标准动态地选择 Math 接口的具体实现,如下所示:
Math math = Math.getInstance("add");
3. 我们以特定的方式编写另一段代码,使其负责选择代码将要使用的 Math 接口实现:
Math math = Math.getInstance();
4. 我们以特定的方式编写代码,以调用抽象方法,使某个基础隐藏逻辑负责将请求调度到恰当的实现,如下所示:
Math.invoke("add", x, y);
在任何相对复杂的应用程序中,Java 开发人员将可能组合使用这些方法。例如,如清单 2 所示,使用 Java
XML API (javax.xml.*) 的客户机代码从来不必指定要使用哪个 XML 分析器实现。该决定使用第三个方法实现——该决定在工厂内部配置,对客户机隐藏。
清单 2:使用 Java XML API
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
在 XML API 采用的方法中,开发人员不知道 JVM 可以使用哪个 XML 分析器,因此降低了代码的总体复杂性,使其可移植性更好,也更易于管理。如果采用上面的第
1 个选项,开发人员要负责发现和绑定所需接口的恰当实现。而采用第 2、3、4 个选项,基础隐藏代码将定位所需接口的恰当实现实例。
ESB 中的服务发现
现在,让我们回到面向服务的应用程序和 Web 服务。服务使用者发现服务提供者始终非常重要。采用的方法是由第三方注册中心维护一个已知服务提供者的数据库。客户机在运行时动态地查询该注册中心,或者,在开发时定位符合所定义的标准的一个或多个提供者(更可能采取后一种方式)。例如,客户机可能要定位所有实现
GetQuote 端口类型的服务提供者。在 Web 服务中完成此任务的传统方法是:向 UDDI 服务器提交一个查询,分析结果,选择提供者,然后调用服务,如图
6 所示。
图 6:典型的 UDDI 发现交互
在开发时执行时,此过程与上面列出的第一个选项非常相似,我们在此选择中显式地选择 Add 类的 Math
接口实现。采用此方法所得到的代码如清单 3 所示。
清单 3:指定特定提供者
Service service =
new Service(
"http://example.schwab.com/services/stocks");
Call call = service.createCall(...);
...
call.invoke(...);
在本例中,我们的客户机代码可能不关心承载 Schwab 股票报价服务的开发平台和操作系统,但仍然紧密地绑定到
GetQuote 端口类型及其提供者的具体实现。如果 Schwab 更改了其 GetQuote 服务的
URL(相当于更改了 Add 实现类的 Java 包),每个按照这种方式绑定到服务的客户机都会受影响,因而必须进行更新。确保客户机使用的是服务的当前绑定信息的唯一办法就是在每个
GetQuote 操作调用前重新查询 UDDI 注册中心。这将使客户机浪费大量的时间重复 UDDI 查询。
简化此问题的一个方法就是引入代理,作为调用 Web 服务的中间层。客户机不再直接调用服务提供者,而调用代理中的服务代理,该服务代理将对服务提供者进行调用,如图
7 所示。这相当于上面所列出的第四个选项,在该选项中对中间对象调用静态对象。该中间对象的任务就是定位恰当的实现,以使代码不必自己进行发现操作。(客户机可能要执行某种类型的发现操作,以定位服务代理,也可能不需要执行。代理的
URI 应该为静态的,一旦被发现,客户机通常就可以跨多个服务调用可靠地使用它。)
图 7:通过代理发现服务提供者
此处的基本观念是,使用 ESB 模式可以将选择恰当服务提供者的负担从客户机代码转到中间件上,从而为服务使用者提供一个简单得多的体系结构。
服务代理不仅免除了客户机定位服务提供者的负担,还提供了一种灵活的方法,可以提高服务调用过程的效率,如果采用其他方式,客户机就不能获得这种灵活性。例如,服务代理可以缓存
UDDI 数据,以提高服务提供者选择过程的性能。或者,服务代理可以封装业务逻辑,以根据已建立的服务级别协议或业务规则改变服务提供者的选择。
以下就是企业服务总线模型的优势:可以管理服务调用过程,并同时简化使用,且不会给提供者代理带来负担。
那么让我们回头来看看前面列出的 ESB 定义原则。作为封装代理模型的自然扩展,封装服务代理的这个模型体现了很多
ESB 原则(服务质量、服务提供者和供应商实现的替换、服务定位、通用性、异构性)。换句话说,通过将企业应用程序组件间的交互的具体细节封装到中间件代理,ESB
支持以松散耦合的方式集成这些组件。
ESB 中的异步消息传递
ESB 体系结构模式的另一个重要方面就是其强调组件间面向消息和面向事件的通信。RPC 样式的 Web
服务使用了在 OO 样式的对象方法调用中常见的同步请求-响应模式。不过,ESB 推荐使用异步消息总线样式的交互模式(如前面的图
4 所示)。图 8 显示了应用到服务调用的消息队列方法,其中的细节以我们的股票报价服务为例。
图 8:基于客户机与中间件代理间交互的异步消息队列
通过这种方法,客户机和中间件之间的全部交互都会使用一个输入和输出消息队列集合异步进行。当响应可用时,中间件要么触发事件回调,将响应通知客户机,要么依赖客户机定期轮询输出队列以获得响应。中间件与服务提供者件的交互可以采用异步模式,也可以不采用异步模式。
客户机从不知道是哪个提供者处理它的请求。客户机所做的仅是将请求放入消息队列,然后从输出队列获得响应(如果有)。服务代理的操作、基础服务提供者的选择以及中间层要执行的其他过程都完全对客户机透明。
服务提供者可以采用完全相同的方式连接到服务总线(侦听队列并将结果发送到响应队列),它们可能从不知道提供同一个服务的其他提供者,甚至连发出请求的客户机的身份也不知道。提供者只是传递要进行处理的请求。
因此,RPC 样式的 Web 服务与 OO 方法调用很相似:客户机使用 SOAP 信封(编码输入参数和返回值)对对象同步调用方法。使用
ESB,客户机可以使用消息队列发送请求消息和接收响应消息,与 OO 方法调用相比,这与 Java 消息服务
(JMS) 更为相似。
ESB 模式的其他特点
正如我们所讨论的,ESB 体系结构样式最为重要的两个方面是:它将业务逻辑封装到了中间件中,支持更为灵活的面向消息和面向事件交互模式。另外,还有其他几个特点值得一提。
传统的面向消息的中间件解决方案(例如,Java 消息服务 (JMS))的一个不足之处在于它们很少是自描述的。从应用程序的观点而言,会有很多不同的队列接收或交付消息。但哪个才是调用特定服务的正确通道呢?消息的格式又是怎样的呢?调用方不能将所有的请求消息格式都放置在任何一个请求通道上;它必须准确地知道希望调用的特定服务的正确消息格式和通道。否则,虽然最终用户真正希望是一本书,但最后买到的却是一张飞机票。而且,应用程序知道了要使用的请求通道和消息格式,它还需要知道要侦听的正确响应通道和应有的响应格式。
ESB 模式基于基础 SOA 设计原则。因此,通过使用 Web 服务描述语言 (WSDL) 等机制,通道自然具有自描述能力。与特定通道关联的
WSDL 文件将说明该通道提供的功能以及通道支持的消息格式。WSDL 文件还可以进一步用于向通道附加策略和声明额外的限制和属性,而采用别的方式,这些就很难硬编码到应用程序中。
另外,由于 ESB 中使用的输入和输出队列可以将自己描述为服务,因此可以像其他类型的服务一样对其进行发现。客户机可以使用各种选择,包括硬编码请求的位置和响应队列,以及使用
UDDI 或 DNS 端点发现等机制来动态地解析服务的位置等等。如前面讨论使用服务代理进行发现封装时所述,与业务服务相比,这种请求-响应队列的位置通常被假定为相对静止的。
最后,到目前为止,异步请求-响应模式是 ESB 实现最常采用的方法,大部分 ESB 实现都支持各种服务样式。其中包括异步请求-响应服务,该服务与常见的大部分
RPC 样式的 Web 服务相似。
结束语
本文说明了企业服务总线体系结构模式的一般特征和原理,并讲解了 ESB 如何适应集成体系结构。作者指出,ESB
除了引入了中间服务代理(可以封装服务交互的很多具体细节,如恰当提供者端点的发现和对不同通信协议的支持)之外,没有别的新概念。而且,ESB
提供了更为灵活的交互模式,超越了传统 RPC 样式的简单请求-响应模式,该模式通常与 Web 服务相关联。最重要的是,ESB
并没有提出任何新“事物”或新“产品”来进行打包、销售、安装等。相反地,它体现了面向服务的体系结构的基本原则在现有中间件设计理念中的应用。
|