在本文中,作者介绍了在项目实施过程中积累的一些使用WebSphere Message Broker 6.0映射来完成消息转换功能的开发技巧和建议。
WebSphere Message Broker的消息转换功能
WebSphere Message Broker(以下简称为WMB)是IBM公司的应用整合中间件。其定位为高级企业应用服务总线(Advanced ESB)。WMB可以支持XML, COBOL, EDI, HL7, IDOC, C结构以及用户自定义格式等多种数据格式的解析,以及这些格式之间的相互转换,并可根据消息内容进行灵活的路由。基于WMQ的传输机制,保证了WMB中消息的可靠传输。基于MQ, JMS和WebService的接入方式,提供一定终端用户接入的灵活性。WebSphere Message Broker高性能的格式转换和路由,良好的稳定性和可扩展性,灵活的接入方式,能够满足企业应用互连对企业应用服务总线的要求。
数据格式转换几乎是WMB实施项目中必不可少的需求。WMB提供了两种手段来实现对多种数据格式的灵活转换的支持:Mapping节点和代码编程。本文讨论的Mapping节点,大多数情况下也包括了Mapping节点的特殊形式:DataInsert、DataUpdate、DataDelete、Extract节点。在新的WMB 6.0版本中,代码编程方式除了可以使用传统的ESQL,还可以使用Java Compute Node。
简单的说,代码编程的方式提供了最为灵活的消息转换方式。通过Compute节点或者Java Compute节点,开发人员几乎可以完全自由地操纵输出的消息和数据库记录。然而,自由的代价是,随逻辑的复杂性的增长,为了保证ESQL代码的正确、安全,开发人员在开发和测试阶段需要花的精力也是随之增长的。同时,手写代码的维护代价,也是不可忽视的。
Mapping节点通过图形化GUI的方式,让开发人员可以用可视化拖曳的方式,结合简单的XPATH代码,来设计不同消息格式之间、消息与数据库记录之间的转换。在构建工程时,Mapping节点被WMB Toolkit自动转换成相应的ESQL代码。Mapping方式更加直观,易于维护,同时更容易在未来的版本中做迁移。
在日常工作过程中,我经常遇到更倾向于手写代码来转换消息的开发人员。他们最大的困惑,就是不了解Mapping节点究竟能完成多复杂的功能,尤其是WMB 6.0的Mapping节点与以前的版本相比,有了新的变化。于是,大家很自然而然的就选择了自由度更大的代码方式。本文的主要目的,就是探讨如何最大化发掘Mapping节点的功能,从而发挥Mapping节点在设计、测试和维护等方面的优势。


|
回页首 |
|
新版本的映射设计器
在新的WMB 6.0版本中,Mapping节点的设计器有了更多改进,例如:
下面的图1展现了新的设计器的界面。
整个界面分为四个部分,上方左右1和2两个窗格分别是输入和输出的消息树,或者数据库表结构。下方是新的电子表格样式的映射脚本。脚本中的每条映射规则都是XPATH表达式,可以在电子表格中编辑,也可以使用中间的空白编辑窗格(3)来编辑。
我们在后面可以看到,电子表格样式的开发模式带来的优势在于,不管是项目经理、业务需求分析人员、架构师还是映射的开发人员,都可以方便的将节点的设计(文档)与实现很好的对应起来,从而比手写ESQL代码更方便地跟踪需求。


|
回页首 |
|
映射节点的需求整理与设计
在实现Mapping节点之前,需要对消息转换的需求做一个完整的文档化整理。
我们首先要确定输入与输出的类型,是消息还是数据库记录,是什么类型的消息(我们在Mapping里只考虑消息集和逻辑消息类型,不考虑物理格式),是什么数据库记录。
然后,我们考虑输出与输入的数量关系:一到一,一到多,多到一,多到多。在WMB 6.0里,Mapping节点只能有一个输入消息(不包括数据库),但一条消息可以被映射出一到多条消息,所以消息层面上只有一到一和一到多的关系。WMB 6.0支持在输入消息中的重复元素中迭代,所以在元素层面上,可以有一到一、一到多、多到一、多到多四种关系。
最后,我们从输出端的消息/数据库记录入手,来分析消息里每个字段(或者数据库记录的每个字段)的源数据、需要的转换逻辑。常见的映射逻辑包括:
- 拷贝:输出字段的值来自某个输入消息/数据库记录的字段值
- 合并:输出字段的值是由一个以上输入字段的值用字符串连接方式拼接而成。需要确认是否包括分隔符、各输入字段的顺序,等等
- 常量:输出字段是一个常量
- 复杂逻辑:输出字段由一个或多个输入字段计算得出
我们虚构一个转换规范,来展示了上述几种转换规则可能的表示方式。这个转换规范中,输入是FLTMsgSet消息集里的FlightInfo消息,输出是数据库表PASSENGERINFO记录。每个FlightInfo消息里记录了航班信息和搭乘该航班的每个乘客的信息。PASSENGERINFO表里每条记录对应了每个乘客。每条输入的航班消息经过转换,在数据库表中产生若干条乘客记录。这是一个典型的一到多的消息关系(如果我们把一条数据库记录也当成一条消息的话)。
我喜欢采用下面的转换规范表,来整理字段间的映射转换关系。
对于目标字段orig、gender,映射规则比较复杂,我们用下面的表格将映射逻辑记录下来,并用规则名称与上面的转换表关联。


|
回页首 |
|
映射节点的实现
整理完上节介绍的转换规范表,我们就可以开始在WMB Toolkit中开始开发映射节点了。
不管是Mapping node,还是DataInsert、DataUpdate、DataDelete、Extract node,都采用相同的编辑器和源文件,并且,Mapping节点可以随时添加数据库或消息类型作为节点的输入或输出,因此这些节点本质上来说是可以互相转换的。所以,我们统一把我们要开发的节点都称之为Mapping节点。
所谓源文件,是指与每个Mapping节点关联的、后缀是.msgmap的XML文件。在消息流里,每个Mapping节点都通过它的"属性"对话框,来关联到一个源文件。当然,如果输入、输出和映射规则都相同,不同的Mapping节点使用同一个源文件也是可以的。
Mapping的源文件由"新建消息映射"向导创建。激活该向导的方法有很多种:使用菜单File->New->Message Mapping;或者在资源导航器视图里使用右键菜单->New->Message Mapping;或者在放置好新的Mapping节点后,右键点击,选择"打开映射"。WMB Toolkit 6.0不会立即为新建的Mapping节点生成源文件。
根据上节的转换规范表,我们为每个目标字段创建转换规则。例如,对HEAD字段,我们分别选择源和目标中的相应字段,然后右击,选择"从源映射"表示目标字段拷贝源字段的值。
您会看到在映射脚本窗格里,$db:insert下面展开出各个字段,HEAD字段有了值$source/FlightInfo/Header,这是XPATH表达式。这个表达式使用$source变量和/FlightInfo/Header路径来访问输入消息中的Header字段。
对于PRIO目标字段,我们的转换规则是设为常量HIGH。在编辑器里,右键点击PRIO字段,然后选择"输入表达式"。可以看到,映射脚本窗格里的PRIO字段被高亮,光标被切换到中间的XPATH表达式编辑框里。在编辑框里输入'HIGH',就完成了这个字段的转换。单引号'包起来的字符串也是一个XPATH表达式。
对于FLT_NO字段,我们的转换规则是将AirCompanyCode和Number字段的字符串值连接在一起。在编辑器里,先点击AirCompanyCode,然后按住CTRL键,继续点击Number,然后右击FLT_NO,选择"从源映射"。在XPATH编辑框里,将生成的表达式$source/FlightInfo/AirCompanyCode + $source/FlightInfo/Number替换为 fn:concat($source/FlightInfo/AirCompanyCode,$source/FlightInfo/Number)。fn:concat($arg1, $arg2)是一个XPATH函数,将两个字符串表达式的值拼接后作为返回值返回。$arg1 + $arg2也是一个合法的XPATH表达式,但它要求运算符$arg1和$arg2是数值类型,很明显,不适用于我们的AirCompanyCode和Number字段。您也可以直接右击FLT_NO字段,然后选择"输入表达式",填入表达式。
到此,我们已经完成了三个字段的映射。如果现在执行这个映射,其结果是,每映射一条FlightInfo消息,在PASSENGERINFO表中将产生一条记录,该记录只有HEAD、PRIO、FLT_NO三个字段有值(假定表上没有主键、NOT NULL字段等等约束,否则插入会失败)。


|
回页首 |
|
调用自定义的ESQL函数实现复杂映射逻辑
对于前面的RULE_ORIG,实际上我们可以用一个XPATH表达式来实现:esql:coalesce($source/FlightInfo/Original, 'UNKNOWN')。但是,如果规则更加复杂就未必了;即便可以用XPATH表达式,考虑可读性问题,自定义的ESQL函数仍然可以是一个好的选择。
需要注意的是,使用这一功能需要将WMB Toolkit升级到6.0.0.1 + Interim Fix 003级别以上,以修补"49803 ref symbols not published for esql funcs used in RDB maps"错误。
我们可以把映射里的复杂逻辑都组织在一个代理模式里,例如MappingRoutines。在"代理应用程序"透视图里,右键点击"资源导航器"视图里的消息流工程,选择"新建"、"代理模式",然后填入名称MappingRoutines、点击"Finish"。右键点击这个代理模式,选择"新建"、"消息流ESQL文件",填入文件名(与相应的mapping同名是个好的做法),然后这个ESQL文件里编写ESQL函数。RULE_ORIG的实现代码如下:
-- RULE_ORIG
CREATE FUNCTION RULE_ORIG(IN orig CHARACTER)
RETURNS CHARACTER
BEGIN
IF orig IS NULL THEN
RETURN 'Unknown';
ELSE
RETURN orig;
END IF;
END;
|
在映射编辑器的映射脚本窗格里,点击ORIG所在的行,然后在XPATH编辑窗格里,按ALT+/,调出内容协助如下图:
您会看到自定义的ESQL函数也在这里面,其语法是XQUERY类型的,esql:是名字空间,其后是所在的代理模式(如果是缺省模式则没有),最后是函数名。选择它并按回车,填入其他参数,整个表达式是esql:MappingRoutines.RULE_ORIG($source/FlightInfo/Original)。
最后一个问题是,什么复杂程度的逻辑就应该选择使用自定义ESQL函数,即便我们可以用一个XPATH表达式来完成呢?这里没有一个绝对的标准,而是主要取决于您。记住一个原则:好的代码可以解释它自己。
至此,我们已经实现了简单和复杂的字段映射逻辑。如果你现在对比前面的消息转换需求规范表格,和我们整洁的映射编辑器,从它们的相似性中,相信您已经可以体会到,电子表格形式的映射编辑器,可以很好的表示映射的实现逻辑。您再也不用写大量的ESQL注释了。使用前面的需求规范表格和映射编辑器,消息转换的代码更有可读性和可维护性。


|
回页首 |
|
在输入消息中迭代实现一到多的数据库映射
与上节中的FLT_NO一样,NAME字段由FirstName和LastName拼接而成,而且中间用空格分隔。不同的是,输入消息中有一到多个Passenger,也就对应到相同数量的记录。怎么处理输入消息中的重复元素呢?放心,映射编辑器会为我们做大部分的工作。
左键选择FirstName字段,按住CTRL键,再选择LastName字段,然后右键点击name目标字段,在弹出菜单里选择"从源映射"。您会看到映射脚本窗格里重新折叠,出现了一行for命令。点击for左边的加号,展开如下图
映射编辑器自动意识到FirstName和LastName来自数量是"[1,无限制]"的Passenger,所以自动加上了作用在$source/FlightInfo/Passengers/Passenger上的for指令,并使其包含原来的$db:insert指令。这意味着对for指令指向的每一个$source/FlightInfo/Passengers/Passenger元素,都会执行一次$db:insert指令,并且该指令下每个字段对$source/FlightInfo/Passengers/Passenger的引用,都是for当前指向的元素。这很类似一些过程化语言中的for each指令。
为了让这些$db:insert指令能够工作,我们还需要调整NAME字段的XPATH表达式,将它改为fn:concat($source/FlightInfo/Passengers/Passenger/FirstName, $source/FlightInfo/Passengers/Passenger/FirstName)。很不幸,编辑器可以揣测到我们建立for的意图,却还不能识别到字段的类型不是number。
除了让映射编辑器自动产生for指令,我们也可以自己从头编写它,方法是,右键点击映射脚本窗口里要重复执行的指令,在弹出菜单里选择"for",然后在生成的for指令右边的"值"里,填写重复的源基础,如$source/FlightInfo/Passengers/Passenger。如果要删除for,右键点击它然后选择"Delete",for下面重复执行的指令不会被自动删除,而是回到for原来的嵌套层次。


|
回页首 |
|
在输入消息中迭代实现一到多的消息映射
那么,如果我们的目标消息不是数据库记录,而是消息呢?我们假定有下面的转换规范:
对于FlightInfo消息中的每个Passenger元素,我们要产生一条PassengerInfo消息。
如果我们直接点击输入消息的FirstName元素,再右键点击输出消息的FirstName元素,选择"从源映射",映射编辑器会自动生成下图的脚本:
这个脚本表示:
对于(For each)每个$source/FlightInfo/Passengers/Passenger元素,执行以下逻辑 如果是第一个Passenger(msgmap:occurrence(…) = 1)拷贝该元素的子元素FirstName的值到目标消息中的FirstName元素。
也就是说,这是一个多到一的有选择的映射,它并不构造多个目标消息。所以,这不是我们要的!
事实上,在映射脚本窗格中,每个$target代表了一个输出消息,或者说,代表了输出一条消息的指令。知道了这一点,您大概知道我们应该如何构造多条输出消息了吧?
是的,答案就是,让for指令包含$target。撤销前面的所有操作,然后右键点击$target,选择"For",这样,$target指令就缩进到for指令的下一个层次里了。然后在for指令右边的值里,填入$source/FlightInfo/Passengers/Passenger。之后,您就可以使用鼠标拖曳,或者选择源元素后右键点击目标元素、选择"从源映射",来映射各个元素了。编辑器不会再自动修改for指令。


|
回页首 |
|
在输出消息中实现数量确定的一到多的映射
除了使用for指令在输入消息中迭代的方法可以实现一到多地输出多条数据库记录或多条目标消息之外,我们还可以编写映射脚本,生成确定数量的目标消息。我们也可以在目标消息中生成确定数量的元素,前提是元素在消息集里的定义是可重复的。
要产生确定数量的目标消息,只需要添加相应数量的目标:右键点击源窗格或目标窗格或映射脚本窗格,然后选择"添加源和目标"。目标消息的命名是$target_1、$target_2,类型可以不同。如图:
然后,在映射脚本窗格里,编辑映射逻辑,这工作与映射单个目标消息没什么不一样。
最后,目标消息会按照它们在映射脚本里的顺序,依次被PROPAGATE到流的下一个节点。例如上图里的脚本,输出顺序会是$target、$target_1、$target_3、target_2。您可以在映射脚本窗格里拖动这些$target来改变它们的输出顺序。
如果元素是可重复的,我们可以在映射里构造多个元素的实例。例如上面的FlightInfo消息,Passengers下的Passenger元素数量可以是1至无限制。假定我们需要添加两个Passenger,只需要先右键点击映射脚本窗格里的Passengers元素,选择"填充"来创建第一个Passenger元素。如果已经存在一个Passenger元素,那么这一步可以省略。
然后,右键点击这个Passenger元素,选择"添加实例",这样就会出现第二个Passenger元素了。
如果你要添加很多重复实例,那么可以选择"添加组实例",然后输入要增加的数量。


|
回页首 |
|
有条件的映射
消息转换规则里,少不了要处理各种转换的前提条件。这些条件有结构上的,根据输入的数据不同,输出类型、结构不同的消息,例如"Body元素之前是否要有Header元素";也有内容上的,根据输入数据不同,在消息字段里输出不同的数据,例如"如果源字段Gender的值是Male,那么目标字段Sex的值是1,否则是0"。
内容上的条件,可以通过前面介绍的XPATH函数组合,以及自定义ESQL函数来实现。但结构上的条件呢?
事实上,这两种条件,都可以用映射编辑器里的If指令来表示。您只需右键点击要分支的指令,例如Header元素,然后选择"If",您会看到这个元素会缩进两层,上面出现了一个"If"和一个"条件"指令。条件指令的指缺省是fn:true(),您需要把它改成实际的XPATH条件表达式,当然也可以是返回布尔值的ESQL函数。这样,如果条件表达式运行时返回true,就会执行下面的指令。
如果要创建多个条件,可以右键点击已有的"条件",然后选择"条件",还可以选择"Else",这样就实现了下面的逻辑结构:
IF … THEN
…
ELSE IF … THEN
…
END IF
|
在映射脚本窗格中,这样的逻辑看起来像下图:
当然,这些指令还可以灵活嵌套,组合出非常复杂的分支逻辑。


|
回页首 |
|
与RouteToLabel、MQOutput节点配合使用
Mapping节点的主要功能定位是消息转换。但是,同样可以使用Mapping来重设消息头、Properties、LocalEnvironment、Environment等,实现路由逻辑。
在路由场景中,我们往往在Compute节点的ESQL中:
1. 根据不同的消息格式或内容,选择不同的路由路径:设置OutputLocalEnvironment.Destination.RouterList.DestinationData[1].labelName,然后使用RouteToLabel节点和Label节点。
2. 根据不同的消息格式或内容,选择不同的输出队列:设置OutputLocalEnvironment.Destination.MQ.DestinationData[1].queueName等属性,然后使用MQOutput节点。
使用Mapping节点,结合前面介绍的设置常量、条件映射等技巧,我们同样可以使用Mapping来完成这些功能。在这里不重复介绍具体的操作方法。


|
回页首 |
|
调试
WMB 6.0的映射增加了调试功能。在运行至映射节点前,右键点击调试视图中的调用堆栈,并选择"进入源",便可进入映射的调试模式。
映射编辑器会自动打开,映射脚本窗格中,当前执行的指令被高亮,如下图箭头所指。当您点击了其他指令时,高亮行会转到您点击的那一行上,您可以通过点击"调试"视图里的调用堆栈,来使高亮行返回到实际执行的指令上。
使用F6可以单步跟踪脚本的执行路径。如果映射规则是调用自定义ESQL,可以使用F5来跟到ESQL函数代码里。这跟您熟悉的代码跟踪没什么两样。
在映射脚本里的指令左侧空白处右击,可以增加断点。
在调试透视图里,还有变量视图,可以观察当前scope下能见的各个变量。但对Mapping来说,这个视图只显示生成的ESQL代码的变量。您可能只对DebugMessage、InputLocalEnvironment、OutputRoot、OutputLocalEnvironment感兴趣,它们分别是输入的消息、输入的LocalEnvironment树、构造过程中的$target消息、构造过程中的LocalEnvironment树。
但如果您还有钻研的兴趣,可以从项目的workspace目录下的.metadata/.plugins/com.ibm.etools.mft.builder.esqlobj/目录下,找到Mapping对应的ESQL的代码。这些代码是项目构建时临时生成的,所以文件名会随每次构建而变化,您可能需要grep这样的工具,寻找"CREATE PROCEDURE <Mapping Name>",其中<Mapping Name>是映射源文件的名字,不包括后缀".msgmap"。
通过调试,我们可以图形化地看到映射脚本中各指令是怎样被执行、生成OutputLocalEnvironment和OutputRoot的,您还可以与调试ESQL代码时一样,在调试过程中修改这些变量,而不必重新开始。


|
回页首 |
|
在ESQL代码里调用映射
尽管图形化映射方式提供了丰富的功能,但仍然有一些难以完成的工作,例如传递LocalEnvironment.Variables里的用户变量。LocalEnvironment.Variables子树是使用ESQL在运行时创建的,用于在节点间保留和传递一些用户自定义的变量。映射只能工作在结构已知的消息结构上,因此对于运行时产生的Variables子树,映射无能为力。类似这样的情况,您是否就想放弃使用映射了呢?
除了改为使用Environment子树来存取用户变量(这个方法不一定在所有场合下都适用,毕竟Environment和LocalEnvironment的特性是不同的),您还可以考虑结合ESQL和映射,使用映射来实现大部分的消息转换逻辑,再用ESQL来处理那些映射难以处理的消息结构。这样,我们就需要一个方法,在ESQL中来调用映射脚本。
事实上,您可能在上一节里就已经看出了端倪。每个映射的.msgmap源文件,都被编译成了一个ESQL Procedure,其入口如下:
CREATE PROCEDURE <Mapping Name> (IN source REFERENCE,
IN target REFERENCE,
IN InputLocalEnvironment REFERENCE,
IN OutputLocalEnvironment REFERENCE)
|
因此,在ESQL代码里调用映射,其实就是调用映射生成的ESQL Procedure,传入InputRoot、OutputRoot、InputLocalEnvironment、OutputLocalEnvironment。
对于DataInsert等数据库节点,因为没有输出,所以生成的ESQL Procedure入口是 CREATE PROCEDURE <Mapping Name>(IN source REFERENCE, IN InputLocalEnvironment REFERENCE)
调用时传入InputRoot、InputLocalEnvironment。
我们可以只创建.msgmap源文件,构建后就可以在Compute等ESQL节点里直接调用,而不需要在流编辑器里放置Mapping、DataInsert等节点。这样,我们就可以仍然用Mapping实现转换逻辑,而不用手写大量ESQL代码。


|
回页首 |
|
总结
了解和掌握了WMB V6的Mapping的强大功能,我们就可以充分发挥图形化映射编辑器的功效,开发人员可以更直观、高效、安全地实现和维护消息转换逻辑,项目管理人员和架构师可以更有效的跟踪需求。 |