http://www.infoq.com/cn/articles/jms-spring-messaging-interop
简介
在.NET和Java间存在许多进行互操作的解决方案。其中应用最广泛的是,使用能同时在两种环境下工作的Web服务和WSDL片段以及XML Schema。如你所料,Web服务最适合基于Internet的应用。如果你在开发一个内部网的应用系统,它将被应用在一个单独的部门或机构的局域网中,那么中间件技术就变重要了。尤其是基于消息的中间件(message oriented middleware,MOM),它们早已成为公司在不同系统间进行集成的一个主流选择。本文的场景是一个本地局域网内运行的简单的证券系统,本文通过该场景来介绍.NET客户端与Java中间层间的互操作,其中MOM被作为.NET与Java间通讯的基础。系统实现用到了Spring框架(既有.NET 版也有Java版)的JMS支持,来提供连接.NET客户端与Java中间层的通用编程模型。
为何使用消息服务?
在许多情况中,基于消息的中间件都被认为是解决互操作问题的元老。IBM和TIBCO等供应商都已经提供消息中间件产品达20年以上,这些消息中间件都能够工作于各种不同的平台,并存在多种语言的版本。1999年消息服务领域迎来了新的春天,Java消息服务(JMS)规范定义了一系列API的通用集合和消息中间件供应商所需实现的行为。考虑到Java的特点,毫无疑问,JMS的实现可以在绝大部分系统(不是全部,因为还需要.NET API 1)上运行。本文涉及的例子用到了TIBCO的JMS实现,它的实现方式提供了对Java、.NET、.NET Compact Framework和C++客户端API的支持。顺便说明一下,即使你选择的供应商不提供.NET客户端,仍旧有一些方法可以使你通过.NET来访问 JMS。方法之一是使用一个互操作性产品,如JNBridge2 或者Codemesh3。
互操作性为使用消息服务提供了一个不容忽视的理由,在许多领域中消息服务都颇具吸引力,即使它这不完全属实,消息服务也会成为系统架构的一个选择。简而言之,MOM擅长于提供流程间的异步通讯、发布-订阅(一对多) 消息来传递语义和保证高可靠性。如果你的应用能够从这些特点中获益的话,你将总能找到一个合适的消息解决方案。如果你的公司已经因为某些理由使用了消息服务,但还局限于Java或C++应用的话,将.NET客户端作为一个逻辑扩展纳入到现有的架构中去吧。撇开冲动的原因,创建基于JMS的应用的过程存在它自己的学习曲线和大量的最佳实践。本文通过在.NET和Java上使用Spring框架,来展示如何最快的开始创建JMS应用。本文还为.NET和 Java间的消息通讯中出现的问题提供指导。
Spring.NET介绍
大部分人对用于构建Java应用系统的Spring框架并不陌生,与此同时,许多人未必听说它的.NET版本,Spring.NET4。Spring.NET 是Spring框架的.NET版,它完全用C#编写,为.NET提供了基于Spring的设计并将它付诸于应用系统开发的实践。它的核心功能与Java版相同,例如反转控制、面向方面编程、Web框架、RPC Exporter、声明式事务管理和一个ADO.NET框架。简而言之,如果你已经是一个Java版Spring的使用者,那你就能自如地使用 Spring.NET。
Java版的Spring在底层与许多第三方库绑定,与它不同,Spring.NET将对第三方库支持独立为可下载的模块。这些模块之一提供了基于TIBCO JMS实现的JMS支持。如果你希望运行本文中的示例,你需要从TIBCO的网站5下载评估版的TIBCO EMS(Enterprise Message Service)。
可能产生的疑问有“为什么Spring.NET只支持TIBCO的JMS而不支持<供应商名称>的?”其它供应商并非因为原则性的原因而不支持JMS实现。真实的理由是,因为每个供应商需要在.NET中去实现的并非真正的JMS API。基于这一点,每个供应商不再开发Java JMS API的.NET版本。开源项目.NET消息服务API(NMS)的目标是提供这种通用的API,它极可能未来在Spring.NET 6中的扮演JMS的角色。因为我非常熟悉TIBCO的JMS产品,出于方便性的考虑,我将它用于我的Spring.NET示例中。
基于Spring框架的消息服务
Spring JMS支持的目标是,提升使用JMS时的抽象性和支持消息服务的最佳实践。通过提供易于使用的消息类,Spring达成了这些目标。这其中,消息类能够使通用操作变得简单,并能通过使用MessageConverters来传递消息,从而创建“plain old objects”(POJO或POCO)编程模型。MessageConverters有责任完成JMS消息与“plain old objects”之间的转换,它在本质上与XML/对象间映射器无异,但它支持JMS转换。将JMS产品与应用最外层分离的过程中,使用消息转换器将使你受益,从而使你的应用尽可能地摆脱对技术的依赖。当你打算切换中间件时,如果在业务流程中应用JMS MapMessage的话,所需做的重构工作就要少很多。
像JDBC一样,JMS是一个底层API,它需要你为JMS的绝大部分基础任务来创建并管理中间对象。对发送消息而言,Spring的JmsTemplate管理你的行为所产生的中间对象,并使得通用的JMS操作保持一致。在接收端,Spring MessageListenerContainer的实现允许你简单地创建基本结构,来支持异步并发的消息消耗。JmsTemplate和MessageListenerContainer都与MessageConverter相关联,转换于JMS和POJO/POCO的世界。
JmsTemplate,MessageListenerContainers和MessageConverters是Spring JMS支持中的核心部分。本文大量地应用了它们,并从细节上解释了它们,但没有为它们提供定义的参考。更多细节请参照Spring参考文档和其它Spring资源。
Spring JMS中的Hello World
在深入Spring JMS和互操作性的细节之前,来看看以下示例,.NET利用JmsTemplate将“Hello World”的TextMessage发送到命名为“test.queue”的JMS队列。
ConnectionFactory factory = new ConnectionFactory("tcp://localhost:7222");
JmsTemplate template = new JmsTemplate(factory);
template.ConvertAndSend("test.queue", "Hello world!");
如果你很熟悉JMS API,那么你立刻会发现,与直接使用JMS相比,使用JmsTemplate有多么的简单。这主要是因为你不再需要去写代码了,JmsTemplate 已经提供了JMS连接、会话和MessageProducter的所有模板资源管理。它还为我们提供了一些额外的便利,例如针对Java中未检验情况的已检验转换异常,处理基于JMS点到点对象的字符串,委任MessageConverter来将对象转换为JMS消息。如果你打算不进行转换就发送消息,JmsTemplate提供了一个简单的Send方法。
当调用ConvertAndSend方法时,JmsTemplate使用默认的MessageConverter实现(SimpleMessageConverter)来将字符串“Hello World”转化为JMS TextMessage。SimpleMessageConverter还支持byte数组与ByteMessage,以及hash表与JMS MapMessage间的转换。要提供你自己定制的MessageConverter实现,这取决于复杂Spring JMS应用系统的创建过程。通常情况是创建一个针对对象的转换器,这些对象是你已经在系统代码中使用到的,并且你只是想将它们marshall或者 unmarshall成为JMS(译者注:marshall/unmarshall,指将对象序列化为消息对象以及逆向过程。)。
JMS互操作性简述
Spring是同时支持.NET和Java的通用框架,与它相关的围绕互操作性的问题可归纳为,实现MessageConverter和在. NET和Java间交换的“plain old objects”的兼容。为了方便,我们将这些“plain old objects”称为“业务对象”(business objects)。它们可能实际上都是标准的域对象(domain objects),数据转换对象(data transfer objects)或一个用于展示的域对象(domain objects)的UI优化版。在本应用中,业务对象实际上成为了两层之间的数据契约。字段和属性被作为数据契约的一部分,依赖于转换器的实现。需要注意的是,具体的数据类型是没有被明确共享的。涉及的数据类型都需要彼此适应,尽可能使它们所关联的消息转换器正常工作。
尽管本应用肯定不如在web services中使用契约来的规范,但存在一些技术,它们用来在很大程度上管理被交换的数据并减少小错误。基于这个考虑,一个能起到帮助的非技术问题是,在局域网或部门应用中,常常是同一个小组(甚至个人)既开发.NET客户端又开发Java中间层。沟通和持续集成测试能够被替代为对数据契约的大范围使用,这种方式正在被不同的开发小组所采用。如果你乐于使用正式的数据契约来定义层与层之间的交互,那么,正在申请的JMS提案就不太可能让你满意。这就是说,这样松耦合但更不标准的具有互操作性的应用,在我的印象中,已经被成功应用于多个项目中了。
接下来的章节将拿一个逐渐成熟的应用举例,该应用使用了MessageConverter,并在.NET和Java之间同步地保持业务对象。示例应用在一开始就利用Spring中的SimpleMessageConverter来转换哈希表的数据。然后我们创建了一个简单业务对象的.NET和 Java实现,以及一对相应的自定义MessageConverter。最终,我们将使用一些技术,通过使用源代码翻译器和通用的 MessageConverter(不随意转换对象类型),来减少创建converter与业务对象所产生的冗余效果。每个应用的优缺点都被讨论到了。
股票交易示例
本文中的例子是简化了的股票交易系统。我们的应用有三大主要功能——实时市场数据信息的分发,新交易的创建,以及证券交易的获取。
我们从市场数据的分发入手,我们创建JMS基本结构,利用Java中间层向.NET客户端发送信息。在Java中间层中,JmsTemplate被创建来发送消息。在客户端,一个SimpleMessageListenerContainer用于接收消息。这两个类都默认使用了一个SimpleMessageConverter的实例。因为市场数据显然是键值对的集合(例如PRICE=28.5, TICKER="CSCO"),完全有理由使用默认的转换器,并在.NET与Java之间利用简单的哈希表来进行数据交换。
JmsTemplate与SimpleMessageListenerContainer的配置中都需要一个JMS的连接工厂和JMS的目的对象名称及类型。SimpleMessageListenerContainer还需要一个引用,引用指向JMS MessageListener进行消息处理的回调方法的实现。Spring提供了一个MessageListenerAdapter类,它实现了JMS MessageListener接口,并使用一个MessageConverter来将接收的JMS消息转换为一个对象。 MessageListenerAdapter随后用这个对象来调用某个方法,用户提供的具有相应方法签名的处理类实现了该方法。
前述的流程是对例子的最好解释。如果MessageListenerAdapter被配置来使用SimpleMessageConverter,那么传入的JMS MapMessage将被转换为一个.NET IDictionary,并且处理类上方法签名为“void handle(IDictionary data)”的方法将被激活。如果转换器产生了一个交易类型的对象,随后处理类必须包含一个名为handle(Trade trade)的方法。下面的顺序图展示了事件的流程。
上述框架展示了,在Java世界中,“消息驱动POJO”或“消息驱动对象”通常是如何被理解的。因为无论“消息驱动POJO”或“消息驱动对象” 都是一个“plain old object”,而不是一个执行消息回调的JMS MessageListener。此应用的一个优点是,你可以轻松地创建集成形式的测试用例,这些测试将通过直接调用处理器方法的方式演练这个应用的流程。另一个优点是,在消息应用中,你经常可以发现MessageListenerAdapter扮演了if/else或switch/case块替代品的角色。SimpleMessageListenerContainer还有许多其它的特点,例如自动发送基于处理器返回值的回应消息,成为自定义流程的子集等。更多细节请查看Spring参考手册文档。
这些对象的Spring配置如下:
中间层发布者:
客户端消费者
需要注意的是,Spring.NET在 XML中使用’object’标签来代替’bean’标签定义对象。从.NET的角度来看,处理器对象的 “ DelegateObject” 这个名字是不幸的。不过此属性与.NET中delegate的涵义完全无关。
所有发送到客户端的数据都在名为APP.STOCK的JMS主题中,而所有从客户端发送到中间层的数据都位于名为APP.STOCK.REQUEST的JMS队列中。
下面展示的是一个直接发送的JMS消息,它们包含了一些设定的市场数据。
Map marketData = new HashMap();
marketData.Add("TICKER","CSCO");
marketData.Add("PRICE", new Float(23.54));
... jmsTemplate.send(marketData);
在接收端,StockAppHandler的handle方法完成了所有处理过程。
{
HandleObject( data)
{
log.InfoFormat(, data[], data[]);
}
}
应用执行时所需要的不过是这寥寥数行代码。然而,尽管基于哈希表的数据交换很简单并且切实可行,但它只适合应用于最简单的互操作场景中。我所说的 “简单”是指少于5项且每次键值对少于10个的数据交换。它不适合更复杂场景,理由很明显,数据契约太松散,它无法确保两层之间的键值对不出现错误的匹配。这些类能够确保大部分数据内容的正确性,例如提供带参数的构造器,或使用第三方验证库。撇开验证的问题,与引入哈希表来满足 MessageConverter的需求相比,直接在中间层为业务流程使用对象更简单。正因如此,为了直接使用这些对象,自定义消息转换器需要被创建并以插件形式纳入Spring的JMS体系结构中。
使用自定义转换器
Spring的MessageConverter接口非常简单。它包含以下两个方法,
{
ToMessage( objectToConvert, . session);
FromMessage( message);
}
转换消息失败时将抛出MessageConversionException。
我们将创建一个自定义消息转换器,来发送一个从客户端到中间层的关于交易创建的请求消息。TradeRequest类记录用户在填写一个创建交易的表单时的输入信息,并且还提供一个验证方法。TradeRequest类包括以下属性,股票代码、分红、价格、订单类型、帐号名称、操作(买入或卖出)、 requestid以及用户名。转换器的实现是直接编码的,简单地将这些属性添加为JMS MapMessage的对应字段。客户端“ToMessage”的实现代码如下:
{
ToMessage( objectToConvert, session)
{
tradeRequest = objectToConvert
(tradeRequest == )
{
( +
objectToConvert.GetType());
}
{
mm = session.CreateMapMessage();
mm.SetString(, tradeRequest.AccountName);
mm.SetBoolean(, tradeRequest.BuyRequest);
mm.SetString(, tradeRequest.OrderType);
mm.SetDouble(, tradeRequest.Price);
mm.SetLong(, tradeRequest.Quantity);
mm.SetString(, tradeRequest.RequestId);
mm.SetString(, tradeRequest.Ticker);
mm.SetString(, tradeRequest.UserName);
mm;
} ( e)
{
(, e);
}
}
... (FromMessage not shown)
}
“FromMessage” 的实现简单地创建了一个TradeRequest对象,利用消息中获取的值来设定它的属性。具体细节请查看代码。值得注意的是,如果你打算使用属性来 marshall或unmarshall数据的话,请确认在一个marshall过程的上下文中,对这些属性的调用不会有任何副作用。
为了使客户端向中间层发送数据,我们需要为此前的JMS结构创建镜像,分别在客户端创建一个JmsTemplate,在中间层创建一个 SimpleMessageListenerContainer。中间层的消息容器被配置来使用一个Java版的 TradeRequestConverter和一个名为StockAppHandler的消息处理类,该处理类提供了一个方法签名为“void handle(TradeRequest)”的方法。客户端的配置如下:
硬编码后的客户端使用模板的方式如下:
SendTradeRequest()
{
tradeRequest = ();
tradeRequest.AccountName = "";
tradeRequest.BuyRequest = ;
tradeRequest.OrderType = "";
tradeRequest.Quantity = 314000000;
tradeRequest.RequestId = "";
tradeRequest.Ticker = "";
tradeRequest.UserName = "";
jmsTemplate.ConvertAndSend(tradeRequest);
}
在客户端发送消息的顺序图如下:
使用一个简单的POJO消息处理器实现时,中间层的配置如下:
这样做的优势是,客户端和中间层的开发者能“共享”同一个类,例如TradeRequest,在之前的例子中它既含有数据又提供功能。尤其对于大项目而言,类共享的一个缺点是,创建成对的业务对象和转换器时会出现冗余。如果层与层之间的数据交换很稳定,那么这种冗余就是一次性消耗。然而,如果数据交换每周或每天都在调整,就像项目还在开发一样的话,这就成了一项乏味的任务。
一个有效处理这类问题的方式是,创建一个单独的通用MessageConverter,由它来处理多种消息类型,然后首先用Java编写业务对象,随后使用源代码转换工具来生成C#版的业务对象。下一节对这个应用的讨论涉及更多细节。
通用消息转换器
正如你在前面TradeRequestConverter的代码列表中看到的,它的实现很繁琐,应该不由手工来编码。使用代码生成机制或反射机制的解决方案能够替代人工。示例代码包含一个'XStream'(译者注:XStream是一个开源项目,用于序列化对象与XML对象之间的相互转换),并应用了基于转换器ReflectionMessageConverter的反射机制,该转换器能够转化许多对象。这个转换器的特点及其局限性如下:
- 利用所有字段成员的反射,依靠字段值将所有对象序列化为消息对象。这个选择可以避免由于为属性编写setter和 getter的附加代码,从而产生的副作用。(以添加支持的方式对包含或排除哪些字段进行控制,这是一种进步。这种控制要么基于外部配置,要么利用属性或 annotation,这些属性或annotation类似于WCF的DataContract或DataMember的属性。)
- 支持的字段类型:原生类型 (int、 string等)、Date、Timestamp、以及原生类型的组合类型、hashmap、对象集合(非范型)。还包括循环引用。
- 被传递的消息呈现为一个格式易于理解的JMS MapMessage对象。这将使其它不使用该对象的转换器的流程也能加入到JMS消息交换中来。
- JMS提供者必须支持嵌入的映射消息来对集合类进行转换。
- 提供为任意对象类型注册额外的转换器的功能。
- 降低.NET与Java之间的耦合程度,因为一个转换器并不需要知道消息的类型。消息中的类型标识表明应该创建何种类型。在.NET或Java中,类型标识都将被单独映射为一个具体的数据类型。本案例中,所有的业务对象都以相似的形式命名,只是在命名空间或包上有所区别。为了方便,这个只进行了最简单的配置。
对这个转换器的开发一直在进行中,不久后Spring.NET网站就会提供下载。
独立的通用MessageConverter中实现了许多交互的应用,困难的工作都被交给成熟的marshall技术来完成了。例如,你可以使用一个XML/Object转换器,并将XML字符串作为JMS消息的有效负载进行发送。最近Tangosol提出了一种平台和语言无关的轻量级对象格式(Portable Object Format,POF),它同样可以被用于这种目的。
示例应用使用ReflectionMessageConverter将Trade对象发送到相应TradeRequest的客户端。一个发送更复杂对象的例子是,客户端发送PortfolioRequest,并接收一个Portfolio对象, User对象和Trade对象列表被包含在其中作为响应。这个转换器的配置文件如下:
上述对TypeMapper简单的配置风格将完全限定类型名的最后一部分作为类型标识,在marshall的过程中放入了被传输的消息中。在 unmarshall的过程中,DefaultNamespace和DefaultAssemblyName属性都被用于构建完全限定类型名。 Spring的Java版中对mapper的相应定义配置如下:
IdTypeMapping或IdClassMapping的属性(被标注为注释的)展示了你如何能避免使用类的完整名称,以及如何使用任意的标识符来指定类型。
共享业务对象
在保持业务对象时,能通过保持对象同步来减轻效果的一项技术是,使用Java语言转换器(JLCA)来自动将Java对象转换为C#7对象。当这个工具被用于对Java代码的一次性转换时,它被归入自动化构建过程,用于在Java和.NET间同步业务对象。业务对象实际上是转换器的候补,因为它们不包含特定技术的API,例如数据访问API或Web编程API,而这些API在不进行后期手工调整的情况下,很难正确转换。
然而,JLCA并非没有瑕疵的。尽管存在一些限制和古怪之处,但你仍然可以建立复杂的C#类,并且在不需要手工调整的情况下成功将其转换为Java 类。最值得注意的古怪之处是方法名都被转换成了小写字母,并且JavaBean的get和set方法被转换成了.NET的属性。其它限制是, annotation不能被转换为属性,并且缺少对范型的支持。命名空间被作为java的包名,不过简单的正则匹配过程就能够轻松地解决这个问题。转换器还需要创建所需的一些支持类的C#实现,例如C#版的java.util.Set。通过少许实践你就会明白应该如何将这项技术应用到你的项目中。 Gaurav Seth博客8上用一个"cheat sheet"总结了该转换器的功能。最后来看看提供JLCA的公司ArtinSoft,这个公司同时还销售自己的产品JLCA Companion,该产品允许你添加或调整转换的规则9。
在本示例中,对Java类运行JLCA的效果很好。你可以通过在.NET解决方案中包括或排除”Bo”和”Jlca”目录,从而切换使用手工编码的 C#业务对象或JLCA生成的业务对象。尤其可以查看或修改TradeRequest类中的验证方法,这个验证方法用到了简单的条件逻辑和对集合类的控制。在示例中提供了一个ant脚本,用于在Java业务对象上运行JLCA,并将包名改为正确的.NET命名空间。
客户端在接收少量市场数据事件后,同时发送了一个TradeRequest和一个PortfolioRequest,以下是这个场景的截图:
总结
如果你已经开始使用消息服务,或者打算使用消息服务的一些特性,例如异步通信和发布/订阅的投递,那么在Java和.NET中使用Spring的 JMS支持将为你新建互操作性解决方案提供一个很高的起点。Spring在使用协议确保JMS生产者与消费者间的兼容性方面并不那么规范,但它提供了 MessageConverter这个简单的扩展,使你能够为你的应用去定制协议。成熟的转换器和相关联的对象能够适应你应用系统复杂性的要求。这个股票交易系统和ReflectionMessageConverter构成你的这个简单实验的基础。
再次提到一个广为流传的关于Spring框架的描述——“它使简单的东西实现起来更简单,使困难的东西具有了实现的可能”。在.NET与Java的混合环境中,Spring的JMS支持同样符合这种说法,我希望你对这个观点会认同。本文到此即将结束,无论你为互操作性选择哪一条路线(.NET或 Java),在.NET和Java上使用Spring都能使你受益,因为在这两个技术领域中,同样的编程模型和最佳实践都能轻松共享。
本文相关的代码请点击这里下载。
2 http://www.jnbridge.com/,JMS示例: http://www.jnbridge.com/blog/?p=6
3 http://www.codemesh.com/,JMS示例: http://codemesh.com/products/juggernet/examples/jms.html
4 www.springframework.net
5 http://www.tibco.com/software/messaging/enterprise_messaging_service/default.jsp
6 http://incubator.apache.org/activemq/nms.html
7 主站: http://msdn2.microsoft.com/en-us/vjsharp/aa718346.aspx,培训: http://msdn.microsoft.com/vstudio/java/migrate/workshop/
8 http://blogs.msdn.com/gauravseth/default.aspx
9 http://www.artinsoft.com/pr_jlca.aspx
查看英文原文: Messaging Interop with JMS & Spring.NET
译者简介:魏泉,具有多年企业级开发经验,曾担任过博文视点出版公司的技术编辑,是 《Spring技术手册》和 《Spring专业开发指南》的责任编辑。 武汉大学Google Camp 的创建者之一,关注Web发展的最新趋势。