17.2.2 使用 Spring 的 JMS 模板

正如我们所看到的,JMS 为 Java 开发者提供了与消息代理进行交互来发送和接收消息的标准 API,而且几乎每个消息代理实现都支持 JMS,因此我们不必因为使用不同的消息代理而学习私有的消息 API。

虽然 JMS 为所有的消息代理提供了统一的接口,但是这种接口用起来并不是很方便。使用 JMS 发送和接收消息并不像拿一张邮票并贴在信封上那么简单。正如我们将要看到的,JMS 还要求我们为邮递车加油 (只是比喻的说法)。

处理失控的 JMS 代码

在 10.3.1 小节中,我向你展示了传统的 JDBC 代码在处理连接、语句、结果集和异常时是多么冗长和繁杂。遗憾的是,传统的 JMS 使用了类似的编程模型,如下面的程序清单所示。

程序清单 17.1 使用传统的 JMS(不使用 Spring)发送消息
ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection conn = null;
Session session = null;
try {
  conn = cf.createConnection();
  session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
  Destination destination = new ActiveMQQueue("spitter.queue");
  MessageProducer producer = session.createProducer(destination);
  TextMessage message = session.createTextMessage();
  
  message.setText("Hello world!");
  producer.send(message);
} catch (JMSException e) {
  // handle exception?
} finally {
  try {
    if (session != null) {
      session.close();
    }
    
    if (conn != null) {
      conn.close();
    }
  } catch (JMSException ex) {
  }
}

再次声明这是一段失控的代码!就像 JDBC 示例一样,差不多使用了 20 行代码,只是为了发送一条 “Hello world!” 消息。实际上,其中只有几行代码是用来发送消息的,剩下的代码仅仅是为了发送消息而进行的设置。

接收端也没有好到哪里去,如下面的程序清单所示。

程序清单 17.2 使用传统的 JMS(不使用 Spring)接收消息
ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection conn = null;
Session session = null;
try {
  conn = cf.createConnection();
  conn.start();
  session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
  Destination destination = new ActiveMQQueue("spitter.queue");
  MessageConsumer consumer = session.createConsumer(destination);
  Message message = consumer.receive();
  TextMessage textMessage = (TextMessage) message;
  System.out.println("GOT A MESSAGE: " + textMessage.getText());
  conn.start();
} catch (JMSException e) {
  // handle exception?
} finally {
  try {
    if (session != null) {
      session.close();
    }
    
    if (con != null) {
      conn.close();
    }
  } catch (JMSException ex) {
  }
}

与程序清单 17.1 一样,程序清单 17.2 也是用一大段代码来实现如此简单的事情。如果我们逐行地比较,我们会发现它们几乎是完全一样的。如果查看上千个其他的 JMS 例子,我们会发现它们也是很相似的。只不过,其中一些会从 JNDI 中获取连接工厂,而另一些则是使用主题代替队列。但是无论如何,它们都大致遵循相同的模式。

因为这些样板式代码,我们每次使用 JMS 时都要不断地做很多重复工作。更糟糕的是,你会发现我们在重复编写其他开发者的 JMS 代码。

我们已经在第 10 章看到了 Spring 的 JdbcTemplate 是如何处理失控的 JDBC 样板式代码的。现在,让我来介绍一下 Spring 的 JmsTemplate 如何对 JMS 的样板式代码实现相同的功能。

使用 JMS 模版

针对如何消除冗长和重复的 JMS 代码,Spring 给出的解决方案是 JmsTemplate。JmsTemplate 可以创建连接、获得会话以及发送和接收消息。这使得我们可以专注于构建要发送的消息或者处理接收到的消息。

另外,JmsTemplate 可以处理所有抛出的笨拙的 JMSException 异常。如果在使用 JmsTemplate 时抛出 JMSException 异常,JmsTemplate 将捕获该异常,然后抛出一个非检查型异常,该异常是 Spring 自带的 JmsException 异常的子类。表 17.1 列出了标准的 JMSException 异常与 Spring 的非检查型异常之间的映射关系。

Spring(org.springframwork.jms.*)

标准的 JMS(javax.jms.*)

DestinationResolutionException

Spring 特有的 —— 当 Spring 无法解析目的地名称时抛出

IllegalStateException

IllegalStateException

InvalidClientIDException

InvalidClientIDException

InvalidDestinationException

InvalidSelectorException

InvalidSelectorException

InvalidSelectorException

JmsSecurityException

JmsSecurityException

ListenerExecutionFailedException

Spring 特有的 —— 当监听器方法执行失败时抛出

MessageConversionException

Spring 特有的 —— 当消息转换失败时抛出

MessageEOFException

MessageEOFException

MessageFormatException

MessageFormatException

MessageNotReadableException

MessageNotReadableException

MessageNotWriteableException

MessageNotWriteableException

ResourceAllocationException

ResourceAllocationException

SynchedLocalTransactionFailedException

Spring 特有的 —— 当同步的本地事务不能完成时抛出

TransactionInprogressException

TransactionInprogressException

TransactionRolledBackException

TransactionRolledBackException

UncategorizedJmsException

Spring 特有的 —— 当没有其他异常适用时抛出

对于 JMS API 来说,JMSException 的确提供了丰富且具有描述性的子类集合,让我们更清楚地知道发生了什么错误。不过,所有的 JMSException 异常的子类都是检查型异常,因此必须要捕获。JmsTemplate 为我们捕获这些异常,并重新抛出对应非检查型 JMSException 异常的子类。

为了使用 JmsTemplate,我们需要在 Spring 的配置文件中将它声明为一个 bean。如下的 XML 可以完成这项工作:

<bean id="jmsTemplate"
      class="org.springframework.jms.core.JmsTemplate"
      c:_-ref="connectionFactory" />

因为 JmsTemplate 需要知道如何连接到消息代理,所以我们必须为 connectionFactory 属性设置实现了 JMS 的 ConnectionFactory 接口的 bean 引用。在这里,我们使用在 12.2.1 小节中所声明的 connectionFactory bean 引用来装配该属性。

这就是配置 JmsTemplate 所需要做的所有工作 —— 现在 JmsTemplate 已经准备好了。让我们开始发送消息吧!

发送消息

在我们想建立的 Spittr 应用程序中,其中有一个特性就是当创建 Spittle 的时候提醒其他用户(或许是通过 E-mail)。我们可以在增加 Spittle 的地方直接实现该特性。但是搞清楚发送提醒给谁以及实际发送这些 提醒可能需要一段时间,这会影响到应用的性能。当增加一个新的 Spittle 时,我们希望应用是敏捷的,能够快速做出响应。

与其在增加 Spittle 时浪费时间发送这些信息,不如对该项工作进行排队,在响应返回给用户之后再处理它。与直接发送消息给其他用户所花费的时间相比,发送消息给队列或主题所花费的时间是微不足道 的。

为了在 Spittle 创建的时候异步发送 spittle 提醒,让我们为 Spittr 应用引入 AlertService:

package com.habuma.spittr.alerts;

import com.habuma.spittr.domain.Spittle;

public interface AlertService {
  void sendSpittleAlert(Spittle spittle);
}

正如我们所看到的,AlertService 是一个接口,只定义了一个操作 —— sendSpittleAlert()。

如程序清单 17.3 所示,AlertServiceImpl 实现了 AlertService 接口,它使用 JmsOperation(JmsTemplate 所实现的接口) 将 Spittle 对象发送给消息队列,而队列会在稍后得到处理。

package com.habuma.spittr.alerts;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsOperations;
import org.springframework.jms.core.MessageCreator;
import com.habuma.spittr.domain.Spittle;

public class AlertServicelmpl implements AlertService {
  private JmsOperations jmsOperations;
  
  @Autowired
  public AlertServicelmpl (JmsOperations jmsOperatons) {
    this.jmsOperations = jmsOperations;
  }
  
  public void sendSpittleAlert(final Spittle spittle) {
    jmsOperations.send(
      "spittie.alert.queue",
      new MessageCreator() {
        public Message createMessage(Session session)throws JMSException {
          return session.createObjectMessage(spittle);
        }
      });
  }
}

程序清单 17.3 使用 JmsTemplate 发送一个 Spittle JmsOperations 的 send() 方法的第一个参数是 JMS 目的地名称,标识消息将发送给谁。当调用 send() 方法时,JmsTemplate 将负责获得 JMS 连接、会话并代表发送者发送消息(如图 17.5 所示)。

我们使用 MessageCreator(在这里的实现是作为一个匿名内部类)来构造消息。在 MessageCreator 的 createMessage() 方法中,我们通过 session 创建了一个对象消息:传入一个 Spittle 对象,返回一个对象消息。

就是这么简单!注意,sendSpittleAlert() 方法专注于组装和发送消息。在这里没有连接或会话管理的代码,JmsTemplate 帮我们处理了所有的相关事项,而且我们也不需要捕获 JMSException 异常。JmsTemplate 将捕获抛出的所有 JMSException 异常,然后重新抛出表 17.1 所列的某一种非检查型异常。

设置默认目的地

在程序清单 17.3 中,我们明确指定了一个目的地,在 send() 方法中将 Spittle 消息发向此目的地。当我们希望通过程序选择一个目的地时,这种形式的 send() 方法很适用。但是在 AlertServiceImpl 案例中,我们总是将 Spittle 消息发给相同的目的地,所以这种形式的 send() 方法并不能带来明显的好处。

与其每次发送消息时都指定一个目的地,不如我们为 JmsTemplate 装配一个默认的目的地:

<bean id="jmsTemplate"
      class="org.springframework.jms.core.JmsTemplate"
      c:_-ref="connectionFactory"
      p:defaultDestinationName="spittle.alert.queue" />

在这里,将目的地的名称设置为 spittle.alert.queue,但它只是一个名称:它并没有说明你所处理的目的地是什么类型。如果已经存在该名称的队列或主题的话,就会使用已有的。如果尚未存在的话,将会创建一个新的目的地(通常会是队列)。但是,如果你想指定要创建的目的地类型的话,那么你可以将之前创建的队列或主题的目的地 bean 装配进来:

<bean id="jmsTemplate"
      class="org.springframework.jms.core.JmsTemplate"
      c:_-ref="connectionFactory"
      p:defaultDestination="spittleTopic" />

现在,调用 JmsTemplate 的 send() 方法时,我们可以去除第一个参数了:

jmsOperations.send(
  new MessageCreator() {
    ...
  }
);

这种形式的 send() 方法只需要传入一个 MessageCreator。因为希望消息发送给默认目的地,所以我们没有必要再指定特定的目的地。

在调用 send() 方法时,我们不必再显式指定目的地能够让任务得以简化。但是如果我们使用消息转换器的话,发送消息会更加简单。

在发送时,对消息进行转换

除了 send() 方法,JmsTemplate 还提供了 convertAndSend() 方法。与 send() 方法不同,convertAndSend() 方法并不需要 MessageCreator 作为参数。这是因为 convertAndSend() 会使用内置的消息转换器(message converter)为我们创建消息。

当我们使用 convertAndSend() 时,sendSpittleAlert() 可以减少到方法体中只包含一行代码:

public void sendSpittleAlert(Spittle spittle) {
  jmsOperations.convertAndSend(spitlle);
}

就像变魔术一样,Spittle 会在发送之前转换为 Message。不过就像所有的魔术一样,JmsTemplate 内部会进行一些处理。它使用一个 MessageConverter 的实现类将对象转换为 Message。

MessageConverter 是 Spring 定义的接口,只有两个需要实现的方法:

public interface MessageConverter {
  Message toMessage(Object object, Session session) throws JMSException, MessageConversionException;
  
  Object fromMessage(Message message) throws JMSException, MessageConversionException;
}

尽管这个接口实现起来很简单,但我们通常并没有必要创建自定义的实现。Spring 已经提供了多个实现,如表 17.2 所示。

消息转换器

功能

MappingJacksonMessageConverter

使用 Jackson JSON 库实现消息与 JSON 格式之间的相互转换

MappingJackson2MessageConverter

使用 Jackson 2 JSON 库实现消息与 JSON 格式之间的相互转换

MarshallingMessageConverter

使用 JAXB 库实现消息与 XML 格式之间的相互转换

SimpleMessageConverter

实现 String 与 TextMessage 之间的相互转换,字节数组与 BytesMessage 之间的相互转换,Map 与 MapMessage 之间的相互转换以及 Serializable 对象与 ObjectMessage 之间的相互转换

默认情况下,JmsTemplate 在 convertAndSend() 方法中会使用 SimpleMessage Converter。但是通过将消息转换器声明为 bean 并将其注入到 JmsTemplate 的 messageConverter 属性中,我们可以重写这种行为。例如,如果你想使用 JSON 消息的话,那么可以声明一个 MappingJacksonMessageConverter bean:

<bean id="messageConverter"
      class="org.springframework.jms.support.converter.MappingJacksonMessageConverter" />

然后,我们可以将其注入到 JmsTemplate 中,如下所示:

<bean id="jmsTemplate"
      class="org.springframework.jms.core.JmsTemplate"
      c:_-ref="connectionFactory"
      p:defaultDestinationName="spittle.alert.queue"
      p:messageConverter-ref="messageConverter" />

各个消息转换器可能会有额外的配置,进而实现转换过程的细粒度控制。例如,MappingJacksonMessageConverter 能够让我们配置转码以及自定义 JacksonObjectMapper。可以查阅每个消息转换器的 JavaDoc 以了解如何更加细粒度地配置它们。

接收消息

现在我们已经了解了如何使用 JmsTemplate 发送消息。但如果我们是接收端,那要怎么办呢?JmsTemplate 是不是也可以接收消息呢?

没错,的确可以。事实上,使用 JmsTemplate 接收消息甚至更简单,我们只需要调用 JmsTemplate 的 receive() 方法即可,如程序清单 12.4 所示。

当调用 JmsTemplate 的 receive() 方法时,JmsTemplate 会尝试从消息代理中获取一个消息。如果没有可用的消息,receive() 方法会一直等待,直到获得消息为止。图 17.6 展示了这个交互过程。

程序清单 17.4 使用 JmsTemplate 接收消息
public Spittle receiveApittleAlert() {
  try {
    ObjectMessage receivedMessage = (ObjectMessage) jmsOperations.receive();
    return (Spittle) receivedMessage.getObject();
  } catch (JMSException jmsException) {
    throws JmsUtils.convertJmsAccessException(jmsException);
  }
}

因为我们知道 Spittle 消息是作为一个对象消息来发送的,所以它可以在到达后转型为 ObjectMessage。然后,我们调用 getObject() 方法把 ObjectMessage 转换为 Spittle 对象并返回此对象。

但是这里存在一个问题,我们不得不对可能抛出的 JMSException 进行处理。正如我已经提到的,JmsTemplate 可以很好地处理抛出的 JmsException 检查型异常,然后把异常转换为 Spring 非检查型异常 JmsException 并重新抛出。但是它只对调用 JmsTemplate 的方法时才适用。JmsTemplate 无法处理调用 ObjectMessage 的 getObject() 方法时所抛出的 JMSException 异常。

因此,我们要么捕获 JMSException 异常,要么声明本方法抛出 JMSException 异常。为了遵循 Spring 规避检查型异常的设计理念,我们不建议本方法抛出 JMSException 异常,所以我们选择捕获该异常。在 catch 代码块中,我们使用 Spring 中 JmsUtils 的 convertJmsAccessException() 方法把检查型异常 JMSException 转换为非检查型异常 JmsException。这其实是在其他场景中由 JmsTemplate 为我们做的事情。

在 receiveSpittleAlert() 方法中,我们可以改善的一点就是使用消息转换器。在 convertAndSend() 中,我们已经看到了如何将对象转换为 Message。不过,它们还可以用在接收端,也就是使用 JmsTemplate 的 receiveAndConvert():

public Spittle retrieveSpittleAlert() {
  return (Spittle) jmsOperation.receiveAndConverter();
}

现在,没有必要将 Message 转换为 ObjectMessage,也没有必要通过调用 getObject() 来获取 Spittle,更无需担心检查型的 JMSException 异常。这个新的 retrieveSpittleAlert() 简洁 了许多。但是,依然还有一个很小且不容易察觉的问题。

使用 JmsTemplate 接收消息的最大缺点在于 receive() 和 receiveAndConvert() 方法都是同步的。这意味着接收者必须耐心等待消息的到来,因此这些方法会一直被阻塞,直到有可用消息(或者直到超时)。同步接收异步发送的消息,是不是感觉很怪异?

这就是消息驱动 POJO 的用武之处。让我们看看如何使用能够响应消息的组件异步接收消息,而不是一直等待消息的到来。

Last updated