JMS 构成了企业 Java 应用程序中消息传递的基础,但它一直以来都将点对点消息传递和发布/订阅消息传递当作完全独立的域来对待,这两种域的消息传递目标的类型截然不同。JMS 1.0.2 API 对同时使用这两种域的应用程序只提供很有限的支持,对开发与这两种域的目标能一起工作得同样好的可重用框架则不提供任何支持。JMS 1.1 统一了这两种域,从而克服了这一缺点。请与 J2EE 设计师和编写者 Bobby Woolf 一道,他将带您探讨使用 JMS 的最新版来开发 JMS 客户机代码是多么容易。
Java 消息服务(Java Message Service,JMS)API 是 J2EE 平台的构成元素。JMS 1.0.2 定义了两种类型的消息传递域(它们是相互独立的),即点对点和发布/订阅。JMS 的最新版本,即版本 1.1,将成为 J2EE 1.4 的一部分,EJB 2.1 也将会需要它,JMS 统一了这两个域。有了 JMS 1.1,客户机就不再必须专门针对这个或那个域进行实现了。相反,JMS 客户机可以实现为能与来自任一域的目标一起工作。这极大地简化了 JMS API,并为开发者创建更通用、重用性更好的消息传递代码提供了机会。一旦有实现 JMS 1.1 的 JMS 提供程序和 J2EE 容器可供使用,JMS 客户机代码的开发者就应该可以开始在他们的新代码中使用版本 1.1 的 API,并且也可以从将他们现有的 JMS 1.0.2 代码移植到新的 API 中受益。
我们将直接进入对新规范的讨论,所以,如果您要温习一下 JMS,请参阅 JMS 快捷介绍。
三组接口 在 JMS 1.0.2 中,三个通道接口 — Destination、Queue 和 Topic — 不幸地造成了 API 中的接口数量增加到三倍的后果。为了发送或接收消息,客户机使用连接生成器来获得一个连接,客户机用这个连接来创建会话并获得所期望的队列和主题上的消息生产者和消费者。唯一的问题是每种目标都有它自己的生成器、连接、会话、生产者和消费者接口(基本上,就是它自己的接口域),表 1(出自 JMS Javadocs)总结了这一情况。
表 1. 点对点和发布/订阅接口的关系
JMS 公共 PTP 域 Pub/Sub 域
ConnectionFactory QueueConnectionFactory TopicConnectionFactory
Connection QueueConnection TopicConnection
Destination Queue Topic
Session QueueSession TopicSession
MessageProducer QueueSender TopicPublisher
MessageConsumer QueueReceiver TopicSubscriber
此外,为了与 XA(分布式)事务兼容,JMS 还提供了另一组接口。这意味着除了上表所示的 18 个接口之外,JMS 还有另外九个接口 — 生成器、连接和会话类型的 XA 版本 — 类型的数量达实际所需的三倍之多。
JMS 1.1 的业界支持 Java 2 平台,企业版(Java 2 Platform,Enterprise Edition)的下一个主要发行版(J2EE 1.4;目前处于公共草案状态)将包含 JMS 1.1,所以诸如 IBM WebSphere 和 BEA WebLogic 之类的 J2EE 容器(应用程序服务器)将需要实现 JMS 1.1,从而才能被认证为与 J2EE 1.4 兼容。类似的,Enterprise JavaBeans 规范的下一个版本(EJB 2.1;目前也处于公共草案状态)将需要 JMS 1.1,以支持其 JMS 消息驱动的 bean。而且 J2EE 1.4 将包含 EJB 2.1,因此这些规范和 JMS 1.1 将融合到一起。
有许多产品都实现了 JMS 1.0.2,但支持 JMS 1.1 或打算支持 JMS 1.1 的情况如何呢?
目前有两个以盈利为目的的消息传递系统(AshnaMQ 2.1 和 Sun ONE Message Queue,Platform Edition 3.0)和一个开放源代码的消息传递系统(JORAM 3.1.0)实现了 JMS 1.1,但没有任何其它 JMS/J2EE 供应商,包括 IBM、BEA Systems、TIBCO Software 和 Sonic Software 已经宣布打算在它们产品的未来发行版中支持 JMS 1.1。鉴于目前都支持 JMS 1.0.2 或 J2EE 1.2 或 1.3,提供对 JMS 1.1 的支持看来只是一个时间问题。
在上述 27 个接口中,只有六个公共接口(在第一列中的那些)是发送和接收消息所真正需要的。Queue 和 Topic 有时对于区别点对点方式和发布/订阅方式而言很有用。表中的其它十个接口和九个 XA 接口中的至少六个实际上是不需要的,至少对于编写客户机代码来说是不需要的。
举例来说,使用 JMS 1.0.2,如果开发者要访问一个 Queue,他将使用 QueueConnectionFactory 来获取一个 QueueConnection,以创建一个 QueueSession,这个 QueueSession 用来创建一个 QueueSender 或 QueueReceiver。但这样的代码是特定于队列的,将不能与 Topic 一起工作。这种办法在 JMS 1.1 中也行得通,但直接使用 ConnectionFactory、Connection、Session 和 MessageProducer 或 MessageConsumer,会简单些,这后一种办法能与 Topic 和 Queue 一起工作。因此,开发者应使用公共接口并避免使用特定于域的接口,因为公共接口使得代码更简单、更通用,重用性也更好。
非统一接口 在 JMS 1.0.2 中,客户机代码必须使用特定于域的队列和主题接口。公共接口在很大程度上仅仅是一个形式,并不声明客户机所需要的许多方法。例如:Destination 压根不实现任何方法;MessageProducer 不实现 send 方法;MessageConsumer 的确实现 receive,但 Session 客户机无法创建 MessageConsumer;如此等等。
这样,对于一个要发送和接收 Queue 中的消息的客户机,它必须使用表 1 的第二列中的类,而使用 Topic 的类似客户机则必须使用表 1 的第三列中的类来实现很相似的代码。公共接口中缺乏这种多态性使得不可能通过主题重用队列客户机,反过来重用也不可能,这导致了如清单 1 所示的那种重复性代码。
域统一化 JMS 1.1 中的主要变化是添加了新的 API,以支持能同时与点对点或发布/订阅域一起工作的客户机代码。最新的发行版在公共接口中添加了一些方法,从而使主题和主题扩展具有多态性。
例如,MessageProducer 如今实现了 send,所以,客户机可以给目标发送消息而不必知悉该目标是一个队列还是一个主题。类似地,MessageConsumer 声明了 receive 方法,所以,相同的客户机代码不论是使用队列接收程序还是主题订阅程序都能工作。
清单 2 显示了如何使用这个统一接口获取生产者和消费者。清单 1 所示的 JMS 1.0.2 代码需要四个方法(一对用于队列,另一对用于主题),而 JMS 1.1 代码则只需要两个方法(一对用于目标)。
这些新的 API 使得开发者能够编写重用性好得多的 JMS 客户机代码。JMS 客户机代码只是访问并使用目标而无须知道哪些目标是队列,哪些目标是主题。被编写用来访问队列的代码还可以不加修改就以相同方式被重用来访问主题,反之亦然。这对于 JMS 1.0.2 来说是不可能的。
因为公共接口现在几乎能够执行它们的特定于域的扩展所能做的所有相同任务(例如,MessageProducer 几乎可以做 QueueSender 和 TopicPublisher 能做的所有任务),所以,特定于域的子接口实际上已不再需要。这些接口在 JMS 的未来版本中很可能会被去掉。
共享事务 将消息传递域统一起来的微妙但重要的结果是,队列和主题现在可以通过同一个会话(从而在同一个事务中)进行访问。在 JMS 1.0.2 中,QueueSession 可以访问不止一个 Queue,TopicSession 可以访问多个 Topic,但单个会话不能既访问队列又访问主题。因为会话管理着事务,所以单个事务不能既用来控制队列,又用来控制主题。
在 JMS 1.1 中,不但单个 Session 可以访问 Queue 或 Topic(任一类型的 Destination),而且单个会话实例可以用来操纵一个或多个队列以及一个或多个主题,一切都在单个事务中进行。这意味着单个会话可以(例如)创建队列和主题中的生产者,然后使用单个事务来同时发送队列和主题中的消息。因为单个事务跨越两个目标,所以,要么队列和主题的消息都得到发送,要么都未得到发送。类似地,单个事务可以用来接收队列中的消息并将消息发送到主题上,反过来也可以,如清单 3 所示。
考虑一个客户跟踪应用程序,它给要使用客户信息的其它应用程序(其中之一是送货应用程序)提供信息。跟踪应用程序可能需要告知其它应用程序某个特定客户的地址已经变更。如果使用消息传递,则该应用程序将把这项变更发布到主题 CustomerChanges 上,所有要使用客户信息的应用程序将订阅这个主题。客户跟踪应用程序可能要求每个订阅者回报该订阅者是否能够成功处理该项变更。客户跟踪应用程序将指定一个队列 ChangeProcessed,每个订阅者应用程序将使用这个队列来发送应答。
订阅者应用程序,如发货应用程序将接收 CustomerChanges 主题上的变更消息,处理这个消息,然后发送一个应答到 CustomerChanges 队列上。为了确保没有任何变更被丢失,这三个步骤(接收请求、处理请求和发送应答)应该在单个原子事务中进行。这意味着发货应用程序必须使用同一个会话来访问 CustomerChanges 主题和 ChangeProcessed 队列。在 JMS 1.0.2 中,会话不能既访问主题又访问队列,但在 JMS 1.1 中则可以这样做。
结束语 JMS 1.0.2 受害于子类型的爆炸式增长;因为对于每一个类型,都有该类型本身的一个接口、一个队列扩展和一个主题扩展。三倍的接口意味着开发者要学习三倍的 API。客户机代码必须使用这个域或那个域,要么使用队列那组扩展,要么使用主题那组扩展,所以针对一个而编写的代码不能用于另一个。
JMS 1.1 统一了域,从而每个公共接口可以用来取代其特定于域的扩展。这意味着开发者要学习的接口更少了,并且同样的客户机代码既可以用于队列,又可以用于主题。此外,JMS 事务现在可以更容易地跨越主题和队列,这在域被统一起来之前是不可能的。