如果一个企业拥有许多由不同团队开发的异构应用,而这些应用之间需要互相交互,那么它们就会受到以下条件限制:
* 各个应用可能是使用不同的技术构建的,因此互相之间无法通过本地调用机制进行通信(比如J2EE应用和.Net应用)。
* 默认情况下各应用不会以可被目标应用读取的格式发送请求。而且,企业会有许多应用使用同一目标应用。
* 服务组件应该使用其固有的调用或请求机制。比如,一个既有的J2EE应用可以只接受Java消息服务(JMS)传来的请求。
* 企业正在经历一种架构的转变。在这种新的架构中,任何应用都将只接收它所能识别的消息,并且发送只能被它调用的服务识别的参数。
另外企业可能还需要一个可以在不改变应用设计的前提下实现各应用的无缝集成的基础架构。企业服务总线(ESB)便是一种实现这种企业集成架构的方式。
虽然各个企业都倾向于根据自身情况创建合适的ESB,但在设计的时候仍然要注意实现ESB的灵活性。ESB的创建没有固定的套路,它是一个优化服务提供者与服务消费之间的交互的中间层,可以应用在事件驱动、消息驱动或者服务驱动的环境中。
本文将讨论如何构建一个基于Java、能够满足各种常见需求的可扩展ESB。
对ESB的要求
ESB中的常见需求也就是ESB的共同特征:
1. 路由选择:ESB应该能提供一个有效、灵活的路由选择机制。
2. 转换:服务组件无需了解所调用的目标服务的请求规则。ESB应该能够根据服务请求者与目标的关系转换合理的请求格式,使目标服务能够识别。
3. 多协议传输:一个只能使用JMS或Web服务的ESB是没有多大价值的。ESB应该能够根据企业需求进行扩展,进而支持多种消息协议。
4. 安全:如果需要,ESB应该可以对服务访问进行认证与授权。
图1显示了ESB的主要结构。它由三大部分组成:
1. 接收器:客户端可以通过各种各样的接口向ESB发送信息,比如,使用servlet接收从HTTP发向ESB的请求。同时,你可以用MDB(Message-driven bean)监听客户端发出的JMS消息。
2. 核心:这是ESB的主要组成部分。它负责实现ESB的路由、转换和安全功能。它包含一个接收传入的请求的MDB,并能根据接收到的消息内容进行合理的转换、路由选择和安全操作。关于路由选择、传输、转换和安全的详细信息可以用XML文档进行定义(这一部分将于稍后讨论)。
3. 分配器:所有处理发送信息的程序都在这一部分。你可以在这里随意加入任何消息传输处理程序(邮件、传真、ftp服务器等)。

图1 ESB的组成结构
所有这些ESB组件通过一个列出了ESB所能进行的路由操作的XML文档构成一个整体。各种传输操作程序、转换程序和重试策略以及它们与各路由的连接都是通过这个XML文档完成的。
ESBConfiguration.xml
下面的ESBConfiguration.xml可以让我们了解一些ESB的工作机制。其主要元素(element)有:
1. Beans:它包含零个或多个Bean元素。
2. Bean:它定义了创建和设置Bean类的基本方式。它有以下属性(attribute):
* name:代表这个bean的唯一的名称。
* className:bean类的全限定名(Fully qualified name)。
每个bean可以有零个或多个Property子元素。每个Property元素都有一个唯一的属 性名以及一个存放Property值的Value类型的子元素。这些Property实际上是可以对bean类进行设置的JavaBeans类型成员。
3. RetryPolicies:它包含零个或多个RetryPolicy子元素。
4. RetryPolicy:这个元素为给定路由定义了重试(retry)规则。它有一个唯一的属性名。还有两个子元素,分别为MaxRetries和RetryInterval。
Route:是EsbConfiguration的根元素,可以包含零个或多个相同类型的子元素。它代表了ESB的一个路由。它有以下属性:
* name:代表这个route的唯一的名称。
* retryPolicyRef:Retry规则的引用。它应该与RetryPolicy元素属性名相匹配。
* transformerRef:对表示转换的bean的引用。它应该与Bean元素属性名相匹配。
Route元素可以包含零个或多个TransportHandlerRef类型的子元素,这种子元素应该指向代表这个route所使用的转换程序的bean、以及这个bean中用来发送消息的public方法名。此外,Route元素还可以包含一个DeadLetterDestination子元素,指向一个代表无效目标的路由。
下面是一个可供参考的XML文档:
<?xml version="1.0" encoding="UTF-8"?>
<EsbConfiguration xmlns="http://www.bss.org/esb/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Route name="creditService" retryPolicyRef="100-sec-retry" transformerRef="creditServiceTransform">
<TransportHandler beanName="creditJMSTransport"/>
<DeadLetterDestination routeName="DeadLetter"/>
<AuthConstraint principals="app-1"/>
</Route>
<Route name="taxCalculationService" retryPolicyRef="500-sec-retry" transformerRef="taxCalcServiceTransform">
<TransportHandler beanName="taxCalcWS"/>
<DeadLetterDestination routeName="DeadLetter"/>
<AuthConstraint principals="app-2"/>
</Route>
<Route name="RedeliveryRequest" retryPolicyRef="500-sec-retry">
<TransportHandler beanName="redeliveryRequestJMSTransport"/>
<DeadLetterDestination routeName="DeadLetter"/>
</Route>
<Route name="DeadLetter" retryPolicyRef="500-sec-retry">
<TransportHandler beanName="deadLetterJMSTransport"/>
</Route>
<Route name="Redelivery" retryPolicyRef="500-sec-retry">
<TransportHandler beanName="redeliveryJMSTransport"/>
<DeadLetterDestination routeName="DeadLetter"/>
</Route>
<Route name="Error" retryPolicyRef="500-sec-retry">
<TransportHandler beanName="errorJMSTransport"/>
</Route>
<Beans>
<!-- Transport handlers for the service components. -->
<Bean name="creditJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
<Property name="ConnectionFactory" type="java.lang.String">
<Value>qcf-1</Value>
</Property>
<Property name="Destination" type="java.lang.String">
<Value>myCreditQueue</Value>
</Property>
</Bean>
<Bean name="taxCalcWS" className="org.bss.esb.transport.webservice.WebServiceHandler">
<Property name="WsdlUrl" type="java.lang.String">
<Value>http://www.tax.com/calc</Value>
</Property>
</Bean>
<!-- Transformer beans for the service components -->
<Bean name="creditServiceTransform" className="org.bss.esb.transform.XSLTransform">
<Property name="XslUrl" type="java.lang.String">
<Value>file:///C:/temp/esb/transform/xsl/credit.xsl</Value>
</Property>
</Bean>
<Bean name="taxCalcServiceTransform" className="org.bss.esb.transform.CustomTransform">
<Property name="ConfigFileUrl" type="java.lang.String">
<Value>file:///C:/temp/esb/transform/custom/configManager.properties</Value>
</Property>
</Bean>
<!-- Transport handlers for the system queues -->
<Bean name="redeliveryJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
<Property name="ConnectionFactory" type="java.lang.String">
<Value>qcf-1</Value>
</Property>
<Property name="Destination" type="java.lang.String">
<Value>Redelivery.Queue</Value>
</Property>
</Bean>
<Bean name="deadLetterJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
<Property name="ConnectionFactory" type="java.lang.String">
<Value>qcf-1</Value>
</Property>
<Property name="Destination" type="java.lang.String">
<Value>System.DL.Queue</Value>
</Property>
</Bean>
<Bean name="errorJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
<Property name="ConnectionFactory" type="java.lang.String">
<Value>qcf-1</Value>
</Property>
<Property name="Destination" type="java.lang.String">
<Value>System.Error.Queue</Value>
</Property>
</Bean>
<Bean name="redeliveryRequestJMSTransport" className="org.bss.esb.transport.jms.EsbRedeliveryHandler">
<Property name="ConnectionFactory" type="java.lang.String">
<Value>qcf-1</Value>
</Property>
<Property name="Destination" type="java.lang.String">
<Value>Redelivery.Request.Topic</Value>
</Property>
</Bean>
</Beans>
<!-- Defines the retry policies that can be used by various route definitions. -->
<RetryPolicies>
<RetryPolicy name="100-sec-retry">
<MaxRetries>10</MaxRetries>
<RetryInterval>100</RetryInterval>
</RetryPolicy>
<RetryPolicy name="500-sec-retry">
<MaxRetries>10</MaxRetries>
<RetryInterval>500</RetryInterval>
</RetryPolicy>
</RetryPolicies>
</EsbConfiguration>
ESB的作用
ESBConfiguration文档描述了ESB的作用(behavior)。ESBRouter MDB根据描述文件里的路径载入这个XML文档,然后文档中的信息便被转换成了如图2所示的数据结构。

图2 内存中的配置数据结构
ESBRouter(通过ESBConfigManager)将使用这些信息对路由代码进行译解、应用已有的转换、并进行安全授权检验。这里要特别提一下依赖性注射技术(Dependency Injection,以及继承inheritance),它已被用来分离ESB的各种功能(比如多协议消息传输和消息转换),使ESB获得更高的扩展性和可定制性。
如类关系图中所示,ESB设计中有两个关键的接口程序:TransformHandler和TransportHandler.你可以在这里编写路由消息的转换和传输实现方法。然后,这些实现类通过ESBConfiguration里的Bean元素与路由相连。比如,在上面所例的ESBConfiguration.xml文档里,用以下bean定义描述了具体的传输处理程序:
<Bean name="creditJMSTransport" className="com.foo.esb.transport.jms.JmsHandler">
<Property name="ConnectionFactory" type="java.lang.String">
<Value>myQCF</Value>
</Property>
<Property name="Destination" type="java.lang.String">
<Value>myCreditQueue</Value>
</Property>
</Bean>
然后可以在Route节点里通过插入TransportHandler子元素来引用这个传输处理程序,如下所示:
<TransportHandler beanName="creditJMSTransport"/>
注意:本文中定义的传输与转换处理程序使用了Java接口。因此,所有新加的处理程序可能都要实现必需的接口,这可能比较繁琐。不过你可以修改ESBConfigManager,通过依赖性注射方式来调用实现类中的任意方法,从而消除编写实现接口的需要。然而ESBRouter总会传送一个javax.jms.Message实例,因此处理程序的实现类还是必须要用javax.jms.Message类型。
现在我们来慢慢看一下ESB在接收到一个发向特定的路由(服务或应用)的消息后的处理过程。简单起见,我们假设所论及的这两个应用通过JMS进行交互。
1. 首先,根据配置描述符生成ESBRouter MDB的几个基本实例。
然后调用ESBRouter的ejbCreate()方法来完成以下任务:
1. 根据“java:comp/env/EnvConfigUrl”环境项中描述的URL载入EnvConfig.xml文件。
2. 从EnvConfig.xml中读取ESBConfiguration.xml的地址(URL)并将其解析为XmlBean。
3. 创建并初始化EsbConfigManager实例——这个过程要创建并初始化EsbConfiguration.xml中所有的bean,并配置图2中的数据结构。
4. 初始化EsbRouterMonitorMBean实例。(这一步稍后详谈。现在,我们只需要知道它有利于实现ESB JMX(Java管理扩展)。)
如果一个服务组件要通过ESB向另一个服务组件发送一个消息(M),它就会把JMS消息放到ESB输入队列中,从而产生对EsbRouter的onMessage()方法的调用。主要是以下几个步骤:
1. 根据传入的M消息中描述的属性读取目标路由R。这里所描述的属性、以及后面将用到的其它属性都是可以在EnvConfig.xml中进行设置的。
2. 在EsbConfigManager实例中查找与R相对应的RouteInfo。
3. 对请求者进行授权验证,确定其向目标路由发送消息的合法性。如果不合法,消息便会被丢弃。你也可以对这个处理行为进行配置,比如把这个消息记录到一个特定的队列中,或者请示管理员。
4. 如果配置了相应的TransformHandler bean,那么就能在EsbConfigManager实例中查找到,并通过发送自变量M消息调用TransformHandler的transformMessage()方法。
5. 对于每一个与路由R相应的TransportHandler,EsbRouter都会调用它的transportMessage()方法并传送M参数。
6. (if)如果没有在EsbConfigManager中找到与路由R相应的项,(then)就会开始查找与无效路由相应的TransportHandler。然后消息M就会被发送到无效路由。
(else if)此外,如果找到了该路由的处理程序,但是TransportHandler无法传送这个消息或者出现系统故障,(then)消息M就会被放到队列中等待重新发送。重新发送等待队列将激活以下步骤:
1. 在EsbConfigManager中查找给定路由的重新发送规则。
2. 根据重新发送规则中定义的重试间隔计算出下一次尝试发送的时间。
3. 这个时间会被设置为与消息相关的MessageRedeliveryTime属性,并发送到重试队列中。
4. 另一个重新发送请求消息被发送到重新发送请求主题中。重新发送请求是一个javax.jms.ObjectMessage,它包含一个代表下一次发送时间的Long对象。
消息的再次发送:时间安排与处理
接收到再次发送消息的请求后,RedeliveryRequestProcessor MDB会创建一个RedeliveryTask实例。然后用重新发送的时间(这个时间在请求消息的Long对象中)对RedeliveryTask进行初始化,并通过RedeliveryScheduler进行安排。RedeliveryScheduler是一个独立的类,它把编排任务分派给相关的java.util.Timer(见类关系图)。预定的时间一到,RedeliveryTask中的run()方法就被会被调用,它将尝试从重新发送队列中获取预定的消息。其中,消息接收器receiver包含一个消息选择器,只接收MessageRedeliveryTime属性符合预先编排的时间的消息。然后这个消息就被发送到ESB的输入队列中。
以R为目标路由的消息M的重新发送过程如下所示:
1. 在EsbConfigManager中查找给定路由的重新发送规则。
2. 从M中读取包含“已有的发送尝试信息”的属性。(If)如果没有这种属性,(And)并且retry规则中定义的重试次数上限为零,(Then)那么消息M就会被发送到无效路由,并且不再继续进行下面的步骤。
3. (Else if)另外,如果重试次数已经超出retry规则定义的上限,(Then)M就会被发送到与R相关的无效路由。
(Else)其它情况
1. 重新发送尝试计数增加1
2. 根据重新发送规则中定义的重试间隔计算出下一次尝试发送的时间。
3. 尝试次数的计数与下一次尝试时间都设定为M的属性。
4. 消息M就会被发送到重新发送路由。然后重新发送路由从TransportHandlerCache中查找合适的TransportHandler。
5. 一个重新发送请求消息被发送到重新发送请求路由。这个消息只包含一个安排重新发送消息M的时间信息(在第二步中计算得到的时间,计算方式与java.lang.Long一样)。
如果在重新发送过程中有什么故障,消息M就会被发往与路由R相关的无效路由;如果这个过程又由于某些原因出现错误,消息M就会被发送到错误处理路由;如果发送到错误处理路由的过程又失败了,那么EsbRouter的onMessage()方法的处理操作就会回滚,结果就是消息M被放回ESB的输入队列。
注意:我们用主题(topic)代替队列(queue)发送重试请求是因为大多J2EE容器不支持标准的定时服务——虽然它们可能会提供专有的API来完成类似的任务。本文所描述的ESB实现使用以单独类方式(singleton)封装的java.util.Timer实例进行计时。那么使用主题到底有什么优势呢?假使我们使用队列代替主题。我们可以设想一下,在一个部署了RedeliveryRequestProcessor MDB的群集环境中。如果只有一个服务器能够接收到队列中的请求消息。这个服务器把RedeliveryTask编排到将来的某个时间执行,而这时这个服务器突然故障了,就可能会导致无法及时发送消息,进而导致消息失效。而使用主题的话,请求消息就会发送到集群中所有的RedeliveryRequestProcessor MDB实例。因此即使一个服务器发生故障,其它的仍然会继续进行处理。
在正式部署ESB的时候,我们可以为无效路由分配一个通用或单独的消费者(consumer,比如MDB)。这些消费者将从无效路由中提取消息并通知相关的管理员或应用程序。
UML关系图
以下UML关系图描述了ESB的动态、静态结构以及各种组件:

图3 组件关系图

图4 EsbRouter的类关系图

图5 RedeliveryRequestProcessor的类关系图

图6 EsbRouter的序列图

图7 RedeliveryRequestProcessor的序列图
运行时的管理
像ESB这样的重要系统在运行时必须是易于管理和监控的。某些活动,比如更新或添加新的路由,应用合适的转换、传输处理程序、重试规则等,应该可以通过控制台完成。为了实现这种可操作性,本文所描述的ESB的部分核心组件使用了JMX技术,因为JMX的一些关键属性与操作可以满足这种需求。
EsbRouter和RedeliveryRequestProcessor MDB使用了JMX技术。比如,以下操作就是可行的:
* 动态更新EsbConfigManager载入的配置
* 获取处理能力的数据
* 控制重新发送编排程序
如果有需要,你还可以对其它属性进行一些控制或监控操作。
总结
每个企业都对其应用集成方案(比如ESB)有独特的需求,以符合自身需求的方式进行部署。而成功部署ESB的关键就是在设计的时候要充分考虑到ESB在定制和扩展上的灵活性,使其在进行定制与扩展操作的时候不会影响到当前系统。本文描述了一个简明、可扩展的ESB设计方案,它可以实现ESB的普通功能,比如多协议消息传输、路由选择和转换。虽然本文所使用的方法没有使用控制反转技术(Inversion of Control),但这并不影响此类框架(比如Spring)在具体实现中的应用。