写点什么

JMS 与 Spring.NET 间的消息互操作

2007 年 6 月 04 日

简介

在.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。方法之一是使用一个互操作性产品,如 JNBridge 2 或者 Codemesh 3

互操作性为使用消息服务提供了一个不容忽视的理由,在许多领域中消息服务都颇具吸引力,即使它这不完全属实,消息服务也会成为系统架构的一个选择。简而言之,MOM 擅长于提供流程间的异步通讯、发布 - 订阅 (一对多) 消息来传递语义和保证高可靠性。如果你的应用能够从这些特点中获益的话,你将总能找到一个合适的消息解决方案。如果你的公司已经因为某些理由使用了消息服务,但还局限于 Java 或 C++ 应用的话,将.NET 客户端作为一个逻辑扩展纳入到现有的架构中去吧。撇开冲动的原因,创建基于 JMS 的应用的过程存在它自己的学习曲线和大量的最佳实践。本文通过在.NET 和 Java 上使用 Spring 框架,来展示如何最快的开始创建 JMS 应用。本文还为.NET 和 Java 间的消息通讯中出现的问题提供指导。

Spring.NET 介绍

大部分人对用于构建 Java 应用系统的 Spring 框架并不陌生,与此同时,许多人未必听说它的.NET 版本,Spring.NET 4 。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 的世界。

JmsTemplateMessageListenerContainersMessageConverters是 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");<br></br>JmsTemplate template = new JmsTemplate(factory);<br></br>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 配置如下:

中间层发布者:

<span color="#006699"> <span color="#660033">id</span>=<span color="#0000ff">"jmsTemplate"</span> <span color="#660033">class</span>=<span color="#0000ff">"org.springframework.jms.core.JmsTemplate"</span>><br></br><span color="#006699"> <span color="#660033">name</span>=<span color="#0000ff">"connectionFactory"</span><span color="#006699">> <span color="#660033">bean</span>=<span color="#0000ff">"connectionFactory"</span><span color="#006699">/></span><br></br><span color="#006699"> <span color="#660033">name</span>=<span color="#0000ff">"pubSubDomain"</span> <span color="#660033">value</span>=<span color="#0000ff">"true"</span>/><br></br><span color="#006699"> <span color="#660033">name</span>=<span color="#0000ff">"defaultDestinationName"</span> <span color="#660033">value</span>=<span color="#0000ff">"APP.STOCK"</span>/><br></br><span color="#006699"></span></span></span></span></span></span>客户端消费者

<span color="#800000"> id=<span color="#0000ff">"jmsContainer"</span><br></br> type=<span color="#0000ff">"Spring.Messaging.Tibco.Ems.Listener.SimpleMessageListenerContainer, Spring.Messaging.Tibco.Ems"></span><br></br><span color="#800000"> name=<span color="#0000ff">"ConnectionFactory"</span> ref=<span color="#0000ff">"connectionFactory"</span>/><br></br><span color="#800000"> name=<span color="#0000ff">"PubSubDomain"</span> value=<span color="#0000ff">"true"</span>/><br></br><span color="#800000"> name=<span color="#0000ff">"DestinationName"</span> value=<span color="#0000ff">"APP.STOCK"</span>/><br></br><span color="#800000"> name=<span color="#0000ff">"ConcurrentConsumers"</span> value=<span color="#0000ff">"1"</span>/><br></br><span color="#800000"> name=<span color="#0000ff">"MessageListener"</span> ref=<span color="#0000ff">"messageListenerAdapter"</span>/><br></br><span color="#800000"><p> id=<span color="#0000ff">"messageListenerAdapter"</span></p><br></br> type=<span color="#0000ff">"Spring.Messaging.Tibco.Ems.Listener.Adapter.MessageListenerAdapter, Spring.Messaging.Tibco.Ems"</span>><br></br><span color="#800000"> name=<span color="#0000ff">"DelegateObject"</span>><br></br><span color="#800000"> type=<span color="#0000ff">"Spring.JmsInterop.Handlers.SimpleHandler, Spring.JmsInterop"</span>/><br></br><span color="#800000"></span><br></br><span color="#800000"> name=<span color="#0000ff">"DefaultListenerMethod"</span> value=<span color="#0000ff">"HandleObject"</span>/><br></br><span color="#800000"></span> </span></span></span></span></span></span></span></span></span></span>需要注意的是,Spring.NET 在 XML 中使用’object’标签来代替’bean’标签定义对象。从.NET 的角度来看,处理器对象的 “ DelegateObject” 这个名字是不幸的。不过此属性与.NET 中 delegate 的涵义完全无关。

所有发送到客户端的数据都在名为 APP.STOCK 的 JMS 主题中,而所有从客户端发送到中间层的数据都位于名为 APP.STOCK.REQUEST 的 JMS 队列中。

下面展示的是一个直接发送的 JMS 消息,它们包含了一些设定的市场数据。

Map marketData = new HashMap();<br></br> marketData.Add("TICKER","CSCO");<br></br> marketData.Add("PRICE", new Float(23.54));<br></br> ... jmsTemplate.send(marketData);在接收端,StockAppHandler 的 handle 方法完成了所有处理过程。

<span color="#0000ff">public class</span> <span color="#006699">SimpleHandler</span><br></br>{<br></br><span color="#0000ff">public void</span> HandleObject(<span color="#006699">IDictionary</span> data)<br></br> {<br></br> log.InfoFormat(<span color="#800000">"Received market data. Ticker = {0}, Price = {1}"</span>, data[<span color="#800000">"TICKER"</span>], data[<span color="#800000">"PRICE"</span>]);<p><span color="#006600">// 直接发送给控制器来更新视图 <br></br> . . .</span> }</p><br></br>}应用执行时所需要的不过是这寥寥数行代码。然而,尽管基于哈希表的数据交换很简单并且切实可行,但它只适合应用于最简单的互操作场景中。我所说的“简单”是指少于 5 项且每次键值对少于 10 个的数据交换。它不适合更复杂场景,理由很明显,数据契约太松散,它无法确保两层之间的键值对不出现错误的匹配。这些类能够确保大部分数据内容的正确性,例如提供带参数的构造器,或使用第三方验证库。撇开验证的问题,与引入哈希表来满足 MessageConverter 的需求相比,直接在中间层为业务流程使用对象更简单。正因如此,为了直接使用这些对象,自定义消息转换器需要被创建并以插件形式纳入 Spring 的 JMS 体系结构中。

使用自定义转换器

Spring 的 MessageConverter 接口非常简单。它包含以下两个方法,

<span color="#0000ff">public interface</span> <span color="#006699">IMessageConverter</span><br></br>{<br></br><span color="#006699">Message</span> ToMessage(<span color="#0000ff">object</span> objectToConvert, .<span color="#006699">Session</span> session);<br></br><span color="#0000ff">object</span> FromMessage(<span color="#006699">Message</span> message);<br></br>}转换消息失败时将抛出MessageConversionException

我们将创建一个自定义消息转换器,来发送一个从客户端到中间层的关于交易创建的请求消息。TradeRequest 类记录用户在填写一个创建交易的表单时的输入信息,并且还提供一个验证方法。TradeRequest 类包括以下属性,股票代码、分红、价格、订单类型、帐号名称、操作(买入或卖出)、requestid 以及用户名。转换器的实现是直接编码的,简单地将这些属性添加为 JMS MapMessage 的对应字段。客户端“ToMessage”的实现代码如下:

<span color="#0000ff">public class</span> <span color="#006699">TradeRequestConverter : IMessageConverter</span><br></br>{<br></br><span color="#0000ff">public</span> <span color="#006699">Message</span> ToMessage(<span color="#0000ff">object</span> objectToConvert, <span color="#006699">Session</span> session)<br></br> {<br></br><span color="#006699">TradeRequest</span> tradeRequest = objectToConvert <span color="#0000ff">as</span> <span color="#006699">TradeRequest;</span><br></br><span color="#0000ff">if</span> (tradeRequest == <span color="#0000ff">null</span>)<br></br> {<br></br><span color="#0000ff">throw new</span> <span color="#006699">MessageConversionException</span>(<span color="#800000">"TradeRequestConverter can not convert object of type "</span> +<br></br> objectToConvert.GetType());<br></br> }<p><span color="#0000ff">try</span> {</p><br></br><span color="#006699">MapMessage</span> mm = session.CreateMapMessage();<br></br> mm.SetString(<span color="#800000">"accountName"</span>, tradeRequest.AccountName);<br></br> mm.SetBoolean(<span color="#800000">"buyRequest"</span>, tradeRequest.BuyRequest);<br></br> mm.SetString(<span color="#800000">"orderType"</span>, tradeRequest.OrderType);<br></br> mm.SetDouble(<span color="#800000">"price"</span>, tradeRequest.Price);<br></br> mm.SetLong(<span color="#800000">"quantity"</span>, tradeRequest.Quantity);<br></br> mm.SetString(<span color="#800000">"requestId"</span>, tradeRequest.RequestId);<br></br> mm.SetString(<span color="#800000">"ticker"</span>, tradeRequest.Ticker);<br></br> mm.SetString(<span color="#800000">"username"</span>, tradeRequest.UserName);<p><span color="#0000ff">return</span> mm;</p><p> } <span color="#0000ff">catch</span> (<span color="#006699">Exception</span> e)</p><br></br> {<br></br><span color="#0000ff">throw new</span> <span color="#006699">MessageConversionException</span>(<span color="#800000">"Could not convert TradeRequest to message"</span>, e);<br></br> }<br></br> }<p> ... (FromMessage not shown)</p><br></br>}“FromMessage” 的实现简单地创建了一个 TradeRequest 对象,利用消息中获取的值来设定它的属性。具体细节请查看代码。值得注意的是,如果你打算使用属性来 marshall 或 unmarshall 数据的话,请确认在一个 marshall 过程的上下文中,对这些属性的调用不会有任何副作用。

为了使客户端向中间层发送数据,我们需要为此前的 JMS 结构创建镜像,分别在客户端创建一个 JmsTemplate,在中间层创建一个 SimpleMessageListenerContainer。中间层的消息容器被配置来使用一个 Java 版的 TradeRequestConverter 和一个名为 StockAppHandler 的消息处理类,该处理类提供了一个方法签名为“void handle(TradeRequest)”的方法。客户端的配置如下:

<span color="#800000"> name="<span color="#0000ff">jmsTemplate</span>" type="<span color="#0000ff">Spring...JmsTemplate, Spring.Messaging.Tibco.Ems</span>"><br></br><span color="#800000"> name="<span color="#0000ff">ConnectionFactory</span>" ref="<span color="#0000ff">connectionFactory</span>"/><br></br><span color="#800000"> name="<span color="#0000ff">DefaultDestinationName</span>" value="APP.STOCK.REQUEST"/><br></br><span color="#800000"> name="<span color="#0000ff">MessageConverter</span>"><br></br><span color="#800000"> type="<span color="#0000ff">Spring.JmsInterop.Converters.TradeRequestConverter, Spring.JmsInterop</span>"/><br></br><span color="#800000"><br></br></span> </span></span></span></span></span>硬编码后的客户端使用模板的方式如下:

<span color="#0000ff">public void</span> SendTradeRequest()<br></br>{<br></br><span color="#006699">TradeRequest</span> tradeRequest = <span color="#0000ff">new</span> <span color="#006699">TradeRequest</span>();<br></br> tradeRequest.AccountName = "<span color="#800000">ACCT-123</span>";<br></br> tradeRequest.BuyRequest = <span color="#0000ff">true</span>;<br></br> tradeRequest.OrderType = "<span color="#800000">MARKET</span>";<br></br> tradeRequest.Quantity = 314000000;<br></br> tradeRequest.RequestId = "<span color="#800000">REQ-1</span>";<br></br> tradeRequest.Ticker = "<span color="#800000">CSCO</span>";<br></br> tradeRequest.UserName = "<span color="#800000">Joe Trader</span>";<p> jmsTemplate.ConvertAndSend(tradeRequest);</p><br></br>}在客户端发送消息的顺序图如下:

使用一个简单的 POJO 消息处理器实现时,中间层的配置如下:

<span color="#006699"> id="<span color="#0000ff">jmsContainer</span>" class="<span color="#0000ff">org.springframework.jms.listener.SimpleMessageListenerContainer</span>"><br></br><span color="#006699"> name="<span color="#0000ff">connectionFactory</span>" ref="<span color="#0000ff">connectionFactory</span>"/><br></br><span color="#006699"> name="<span color="#0000ff">destinationName</span>" value="<span color="#0000ff">APP.STOCK.REQUEST</span>"/><br></br><span color="#006699"> name="<span color="#0000ff">concurrentConsumers</span>" value="<span color="#0000ff">10</span>"/><br></br><span color="#006699"> name="<span color="#0000ff">messageListener</span>" ref="<span color="#0000ff">messageListenerAdapter</span>"/><br></br><span color="#006699"></span><p><span color="#006699"> id="<span color="#0000ff">messageListenerAdapter</span>" class="<span color="#0000ff">org.springframework.jms.listener.adapter.MessageListenerAdapter</span>"><br></br><span color="#006699"> name="<span color="#0000ff">delegate</span>"><br></br><span color="#006699"> class="<span color="#0000ff">org.spring.jmsinterop.handlers.StockAppHandler</span>"/><br></br><span color="#006699"></span><br></br><span color="#006699"> name="<span color="#0000ff">defaultListenerMethod</span>" value="<span color="#0000ff">handleObject</span>"/><br></br><span color="#006699"> name="<span color="#0000ff">messageConverter</span>"><br></br><span color="#006699"> class="<span color="#0000ff">org.spring.jmsinterop.converters.TradeRequestConverter</span>"/><br></br><span color="#006699"></span><br></br><span color="#006699"></span><p><span color="#660033"><strong>public class</strong></span> StockAppHandler {</p><br></br><span color="#660033"><strong>protected final</strong></span> Log <span color="#0000ff">logger</span> = LogFactory.getLog(getClass());<br></br><span color="#660033"><strong>public void</strong></span> handleObject(TradeRequest tradeRequest)<br></br> {<br></br><span color="#0000ff">logger</span>.info(<span color="#0000ff">"Recieved TradeRequest object"</span>);<br></br> }<br></br>}</span></span></span></span></span></span></p></span></span></span></span></span>这样做的优势是,客户端和中间层的开发者能“共享”同一个类,例如 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 对象列表被包含在其中作为响应。这个转换器的配置文件如下:

<span color="#800000"> name="<span color="#0000ff">reflectionMessageConverter</span>"<br></br> type="<span color="#0000ff">Spring.JmsInterop.Converters.ReflectionMessageConverter, Spring.JmsInterop</span>"><br></br><span color="#800000">property</span> name="<span color="#0000ff">ConversionContext</span>" ref="<span color="#0000ff">conversionContext</span>"/><br></br><span color="#800000"></span><p><span color="#800000"> name="<span color="#0000ff">conversionContext</span>" type="<span color="#0000ff">Spring.JmsInterop.Converters.ConversionContext, Spring.JmsInterop</span>"><br></br><span color="#800000"> name="<span color="#0000ff">TypeMapper</span>" ref="<span color="#0000ff">typeMapper</span>"/><br></br><span color="#800000"></span><p><span color="#800000"> name="<span color="#0000ff">typeMapper</span>" type="<span color="#0000ff">Spring.JmsInterop.Converters.SimpleTypeMapper, Spring.JmsInterop</span>"><br></br><span color="#800000"> name="<span color="#0000ff">DefaultNamespace</span>" value="<span color="#0000ff">Spring.JmsInterop.Bo</span>"/><br></br><span color="#800000"> name="<span color="#0000ff">DefaultAssemblyName</span>" value="<span color="#0000ff">Spring.JmsInterop</span>"/></span></span></span></p></span></span></p></span>上述对 TypeMapper 简单的配置风格将完全限定类型名的最后一部分作为类型标识,在 marshall 的过程中放入了被传输的消息中。在 unmarshall 的过程中,DefaultNamespace 和 DefaultAssemblyName 属性都被用于构建完全限定类型名。Spring 的 Java 版中对 mapper 的相应定义配置如下:

<span color="#006699"> id="classMapper" class="org.spring.jmsinterop.converters.SimpleClassMapper"><br></br><span color="#006699"> name="defaultPackage" value="org.spring.jmsinterop.bo"/></span></span>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 都能使你受益,因为在这两个技术领域中,同样的编程模型和最佳实践都能轻松共享。

本文相关的代码请点击这里下载


查看英文原文: Messaging Interop with JMS & Spring.NET - - - - - -

译者简介:魏泉,具有多年企业级开发经验,曾担任过博文视点出版公司的技术编辑,是《Spring 技术手册》《Spring 专业开发指南》的责任编辑。武汉大学 Google Camp 的创建者之一,关注 Web 发展的最新趋势。

2007 年 6 月 04 日 00:403081

评论

发布
暂无评论
发现更多内容

CAP Theorem

dongge

架构师训练营第 6 周作业二

不谈

第六周作业

秦宝齐

学习 极客大学架构师训练营

java 后端博客系统文章系统——No5

猿灯塔

Java

互联网大厂根本没有题库!了解这些却能让你掌握“隐形题库”

互联网架构师小马

程序员 面试 面试题 Java 面试 找工作

分布式系统架构作业

qihuajun

面向对象编程学习

一叶知秋

详解区块链应用市场与落地应用现状

CECBC区块链专委会

第6周总结

andy

用Roslyn做个JIT的AOP

八苦-瞿昙

技术 随笔杂谈 aop 代理 框架

你与30W奖金只差一个 Apache Flink 极客挑战赛的报名

Apache Flink

flink

MySQL 连接查询超全详解

X先生

MySQL 数据库

数据结构学习心得

程李文华

没错,用三方 Github 做授权登录就是这么简单!(OAuth2.0实战)

程序员内点事

Java GitHub oauth2.0

博睿宏远获颁“2020开发与技术企业服务奖”

BonreeAPM

运维自动化 开发工具 博睿宏远

指数 | 2020年6月北京BGP机房网络质量评测报告

BonreeAPM

评测 博睿宏远 指数

HashMap学习总结

大刘

hashmap hash

第6周作业

andy

第6周课后练习-请简述CAP原理

Dawn

极客大学架构师训练营

架构师训练营第六周总结

一剑

架构师训练营第六周作业

0x12FD16B

极客大学架构师训练营

JDK1.8新特性(一):JDK1.8究竟有哪些新特性呢

xcbeyond

jdk8 新特性

Spring循环依赖及解决方式

张sir

Java spring 循环依赖

现在微服务这么火,你还不了解吗?阿里P8推荐的微服务学习指南

互联网架构师小马

Docker 微服务 Spring Cloud Spring Boot dubbo

架构师训练营第六周作业

一剑

路过,凌晨2点的南京

小天同学

总结 思考 个人感悟 夜归人

你要的《Spring系列源码解读》PDF它来了

z小赵

Java spring

Rust所有权,可转可借

袁承兴

rust 指针 函数调用 引用 内存管理

CAP原理

chenzt

2020-07-11-第六周作业

路易斯李李李

对CAP的理解

朱月俊

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

JMS与Spring.NET间的消息互操作-InfoQ