17.2.3 创建消息驱动的 POJO

在学校时的一个暑假期间,我得到了在黄石国家公园工作的机会。这个工作并不是公园巡逻者或者开关老忠实泉(Old Faithful)这样的高级工作,而是在老忠实泉酒店进行更换床单、清理卫生间以及打扫地板等家务工作。虽然不是很吸引人,但至少我是在这个世界上最美丽的地方工作。

每天工作之后,我都到当地的邮局看看是否有我的邮件。我已经离家好几个星期了,所以能收到学校朋友的来信或者明信片是一件非常美好的事情。我没有自己的邮箱,所以必须走着去邮局,并询问坐在柜台后的工作人员是否有我的邮件。接着就是开始等待。

要知道,柜台后的那个人大约有195岁了。像他这个岁数的人,走动起来很费时间。他从椅子上站起来,慢慢走过地板,消失在隔墙后。过了一会儿,他出现了,慢慢回到柜台,坐到椅子上,然后看着我说:“今天没有邮件”。

JmsTemplate 的 receive() 方法与这个上了年纪的邮局雇员很像。当我们调用 receive() 方法时,JmsTemplate 会查看队列或主题中是否有消息,直到收到消息或者等待超时才会返回。这期间,应用无法处理任何事情,只能等待是否有消息。如果应用能够继续进行其他业务处理,当消息到达时再去通知它,不是更好吗?

EJB2 规范的一个重要内容是引入了消息驱动 bean(message-driven bean,MDB)。MDB 是可以异步处理消息的 EJB。换句话说,MDB 将 JMS 目的地中的消息作为事件,并对这些事件进行响应。而与之相反的是,同步消息接收者在消息可用前会一直处于阻塞状态。

MDB 是 EJB 中的一个亮点。即使那些狂热的 EJB 反对者也认为 MDB 可以优雅地处理消息。EJB 2 MDB 的唯一缺点是它们必须要实现 java.ejb.MessageDrivenBean。此外,它们还必须实现一些 EJB 生命周期的回调方法。简而言之,EJB 2 MDB 不是纯的 POJO。

在 EJB 3 规范中,MDB 进一步简化了,使其更像 POJO。我们不再需要实现 MessageDrivenBean 接口,而是实现更通用的 javax.jms.MessageListener 接口,并使用 @MessageDriven 注解标注 MDB。

Spring 2.0 提供了它自己的消息驱动 bean 来满足异步接收消息的需求,这种形式与 EJB 3 的 MDB 很相似。在本节中,我们将学习到 Spring 是如何使用消息驱动 POJO(我们将其简称为 MDP)来支持异步接收消息的。

创建消息监听器

如果使用 EJB 的消息驱动模型来创建 Spittle 的提醒处理器,我们需要使用 @MessageDriven 注解进行标注。即使它不是严格要求的,但 EJB 规范还是建议 MDB 实现 MessageListener 接口。Spittle 的提醒处理器最终可能是这样的:

@MessageDriven(mappedName="jms/spittle.alert.queue")
public class SpittleAlertHandler implements MessageListener {
  @Resource
  private MessageDrivenContext mdc;
  
  public void onMessage(Message message) {
    ...
  }
}

想象一下,如果消息驱动组件不需要实现 MessageListener 接口,世界将是多么的简单。在这里,天是蔚蓝的,鸟儿唱着我们喜欢的歌,我们不再需要实现 onMessage() 方法或者注入 MessgeDrivenContext。

好吧,可能 EJB 3 规范所要求的 MDB 也算不上太麻烦。但是事实上,SpittleAlertHandler 的 EJB 3 实现太依赖于 EJB 的消息驱动 API,并不是我们所希望的 POJO。理想情况下,我们希望提醒处理器能够处理消息,但是不用编码,就好像它知道应该做什么。

Spring 提供了以 POJO 的方式处理消息的能力,这些消息来自于 JMS 的队列或主题中。例如,基于 POJO 实现 SpittleAlertHandler 就足以做到这一点。

程序清单 17.5 Spring MDP 异步接收和处理消息
package com.habuma.spittr.alerts;

import com.habuma.spittr.domain.Spittle;

public class SpittleAlertHandler {

  public void handleSpittleAlert(Spittle spittle) {
    // ... implementation goes here ...
  }
}

虽然改变天空的颜色和训练鸟儿歌唱超出了 Spring 的范围,但程序清单 17.5 所展示的现实与我描绘的理想世界非常接近。我们稍后会编写 handleSpittleAlert() 方法的具体内容。现在,程序清单 17.5 所展示的 SpittleAlertHandler 没有任何 JMS 的痕迹。从任意一个 角度观察,它都是一个纯粹的 POJO。它仍然可以像 EJB 那样处理消息,只不过它还需要一些 Spring 的配置。

配置消息监听器

POJO 赋予消息接收能力的诀窍是在 Spring 中把它配置为消息监听器。Spring 的 jms 命名空间为我们提供了所需要的一切。首先,让我们先把处理器声明为 bean:

<bean id="spittleHandler"
      class="com.habuma.spittr.alerts.SpittleAlertHandler" />

然后,为了把 SpittleAlertHandler 转变为消息驱动的 POJO,我们需要把这个 bean 声明为消息监听器:

<jms:listener-container connection-factory="connectionFactory">
  <jms:listener destination="spitter.alert.queue"
                ref="spittleHandler"
                method="handleSpittleAlert" />
</jms:listener-container>

在这里,我们在消息监听器容器中包含了一个消息监听器。消息监听器容器(message listener container)是一个特殊的 bean,它可以监控 JMS 目的地并等待消息到达。一旦有消息到达,它取出消息,然后把消息传给任意一个对此消息感兴趣的消息监听器。如图 17.7 展示了 这个交互过程。

为了在 Spring 中配置消息监听器容器和消息监听器,我们使用了 Spring jms 命名空间中的两个元素。 <jms:listener-container> 中包含了 <jms:listener> 元素。这里的 connection-factory 属性配置了对 connectionFactory 的引用,容器中的每个 <jms:listener> 都使用这个连接工厂进行消息监听。在本示例中,connection-factory 属性可以移除,因为该属性的默认值就是 connectionFactory。

对于 <jms:listener> 元素,它用于标识一个 bean 和一个可以处理消息的方法。为了处理 Spittle 提醒消息,ref 元素引用了 spittleHandler bean。当消息到达 spitter.alert.queue 队列 (通过 destination 属性配置)时,spittleHandlerbean 的 handleSpittleAlert() 方法(通过 method 属性指定的)会被触发。

值得一提的是,如果 ref 属性所标示的 bean 实现了 MessageListener,那就没有必要再指定 method 属性了,默认就会调用 onMessage() 方法。

Last updated