# 8.1.2 使用 JmsTemplate 发送消息

在构建中有 JMS starter 依赖（无论 Artemis 还是 ActiveMQ），Spring Boot 将会自动配置 JmsTemplate，这样就可以将其注入并使用它发送和接收消息了。

JmsTemplate 是 Spring JMS 集成支持的核心。与 Spring 的其他面向模板的组件非常相似，JmsTemplate 消除了大量与 JMS 协同工作所需的样板代码。如果没有 JmsTemplate，将需要编写代码来创建与消息代理的连接和会话，并编写更多代码来处理在发送消息过程中可能抛出的任何异常。JmsTemplate 专注于真正想做的事情：发送消息。

JmsTemplate 有几个发送消息的有用方法，包括：

```java
// 发送原始消息
void send(MessageCreator messageCreator) throws JmsException;
void send(Destination destination, MessageCreator messageCreator) throws JmsException;
void send(String destinationName, MessageCreator messageCreator) throws JmsException;
// 发送转换自对象的消息
void convertAndSend(Object message) throws JmsException;
void convertAndSend(Destination destination, Object message) throws JmsException;
void convertAndSend(String destinationName, Object message) throws JmsException;
// 发送经过处理后从对象转换而来的消息
void convertAndSend(Object message, MessagePostProcessor postProcessor) throws JmsException;
void convertAndSend(Destination destination, Object message, MessagePostProcessor postProcessor) throws JmsException;
void convertAndSend(String destinationName, Object message, MessagePostProcessor postProcessor) throws JmsException;
```

实际上只有两个方法，send() 和 convertAndSend()，每个方法都被重载以支持不同的参数。如果仔细观察，会发现 convertAndSend() 的各种形式可以分为两个子类。在试图理解所有这些方法的作用时，请考虑以下细分：

* send() 方法需要一个 MessageCreator 来制造一个 Message 对象。
* convertAndSend() 方法接受一个 Object，并在后台自动将该 Object 转换为一条 Message。
* 三种 convertAndSend() 方法会自动将一个 Object 转换成一条 Message，但也会接受一个 MessagePostProcessor，以便在 Message 发送前对其进行定制。

此外，这三个方法类别中的每一个都由三个重载的方法组成，它们是通过指定 JMS 目的地（队列或主题）的方式来区分的：

* 一个方法不接受目的地参数，并将消息发送到默认目的地。
* 一个方法接受指定消息目的地的目标对象。
* 一个方法接受一个 String，该 String 通过名称指定消息的目的地。

要使这些方法工作起来，请考虑下面程序清单中的 JmsOrderMessagingService，它使用 send() 方法的最基本形式。

{% code title="程序清单 8.1 使用 send() 发送订到到默认目的地" %}

```java
package tacos.messaging;
​
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Service;
​
@Service
public class JmsOrderMessagingService implements OrderMessagingService {
    
    private JmsTemplate jms;
    
    @Autowired
    public JmsOrderMessagingService(JmsTemplate jms) {
        this.jms = jms;
    }
    
    @Override
    public void sendOrder(Order order) {
        jms.send(new MessageCreator() {
            @Override
            public Message createMessage(Session session)
                throws JMSException {
                return session.createObjectMessage(order);
            }
        });
    }
}
```

{% endcode %}

sendOrder() 方法调用 jms.send()，传递 MessageCreator 的匿名内部类实现。该实现重写 createMessage() 以从给定的 Order 对象创建新的对象消息。

我认为程序清单 8.1 中的代码虽然简单，但是有点笨拙。声明匿名内部类所涉及的过程会使简单的方法调用变得复杂。意识到 MessageCreator 是一个功能接口，这时可以用一个 lambda 表达式稍微调整一下 sendOrder() 方法：

```java
@Override
public void sendOrder(Order order) {
    jms.send(session -> session.createObjectMessage(order));
}
```

但是请注意，对 jms.send() 的调用没有指定目的地。为了实现这一点，还必须使用 spring.jms.template.default-destination 属性指定一个默认的目的地名称。例如，可以在 application.yml 中设置属性：

```yaml
spring:
  jms:
    template:
      default-destination: tacocloud.order.queue
```

在许多情况下，使用缺省目的地是最简单的选择。它让你指定一次目的地名称，允许代码只关心发送消息，而不关心消息被发送到哪里。但是，如果需要将消息发送到缺省目的地之外的目的地，则需要将该目的地指定为 send() 方法的参数。

一种方法是传递目标对象作为 send() 的第一个参数。最简单的方法是声明一个 Destination bean，然后将其注入执行消息传递的 bean。例如，下面的 bean 声明了 Taco Cloud 订单队列 Destination：

```java
public Destination orderQueue() {
    return new ActiveMQQueue("tacocloud.order.queue");
}
```

需要注意的是，这里使用的 ActiveMQQueue 实际上来自于 Artemis（来自 org.apache.activemq.artemis.jms.client 包)。如果正在使用 ActiveMQ（而不是 Artemis），那么还有一个名为 ActiveMQQueue 的类（来自 org.apache.activemq.command 包）。

如果这个 Destination bean 被注入到 JmsOrderMessagingService 中，那么可以在调用 send() 时使用它来指定目的地：

```java
private Destination orderQueue;
​
@Autowired
public JmsOrderMessagingService(JmsTemplate jms, Destination orderQueue) {
    this.jms = jms;
    this.orderQueue = orderQueue;
}
​
@Override
public void sendOrder(Order order) {
    jms.send(
        orderQueue,
        session -> session.createObjectMessage(order));
}
```

使用类似这样的 Destination 对象指定目的地，使你有机会配置 Destination，而不仅仅是目的地的名称。但是在实践中，几乎只指定了目的地名称，将名称作为 send() 的第一个参数通常更简单：

```java
@Override
public void sendOrder(Order order) {
    jms.send(
        "tacocloud.order.com",
        session -> session.createObjectMessage(order));
}
```

虽然 send() 方法并不是特别难以使用（特别是当 MessageCreator 以 lambda 形式给出时），但是提供 MessageCreator 还是会增加一些复杂性。如果只需要指定要发送的对象（以及可选的目的地），不是会更简单吗？这简要地描述了 convertAndSend() 的工作方式，让我们来看看。

**在发送前转换消息**

JmsTemplates 的 convertAndSend() 方法不需要提供 MessageCreator，从而简化了消息发布。相反，将要直接发送的对象传递给 convertAndSend()，在发送之前会将该对象转换为消息。

例如，sendOrder() 的以下重新实现使用 convertAndSend() 将 Order 发送到指定的目的地：

```java
@Override
public void sendOrder(Order order) {
    jms.convertAndSend("tacocloud.order.queue", order);
}
```

与 send() 方法一样，convertAndSend() 将接受 Destination 或 String 值来指定目的地，或者可以完全忽略目的地来将消息发送到默认目的地。

无论选择哪种形式的 convertAndSend()，传递给 convertAndSend() 的 Order 都会在发送之前转换为消息。实际上，这是通过 MessageConverter 实现的，它完成了将对象转换为消息的复杂工作。

**配置消息转换器**

MessageConverter 是 Spring 定义的接口，它只有两个用于实现的方法：

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

这个接口的实现很简单，都不需要创建自定义实现。Spring 已经提供了一些有用的实现，就像表 8.3 中描述的那样。

| 消息转换器                           | 做了什么                                                                                                                   |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| MappingJackson2MessageConverter | 使用 Jackson 2 JSON 库对消息进行与 JSON 的转换                                                                                     |
| MarshallingMessageConverter     | 使用 JAXB 对消息进行与 XML 的转换                                                                                                 |
| MessagingMessageConverter       | 使用底层 MessageConverter（用于有效负载）和JmsHeaderMapper（用于将 Jms 信息头映射到标准消息标头）将 Message 从消息传递抽象转换为 Message，并从 Message 转换为 Message |
| SimpleMessageConverter          | 将 String 转换为 TextMessage，将字节数组转换为 BytesMessage，将 Map 转换为 MapMessage，将Serializable 转换为 ObjectMessage                    |

SimpleMessageConverter 是默认的消息转换器，但是它要求发送的对象实现 Serializable 接口。这样要求可能还不错，但是可能更喜欢使用其他的消息转换器，如 MappingJackson2MessageConverter，来避免上述限制。

为了应用不同的消息转换器，需要做的是将选择的转换器声明为一个 bean。例如，下面这个 bean 声明将会使用 MappingJackson2MessageConverter 而不是 SimpleMessageConverter：

```java
@Bean
public MappingJackson2MessageConverter messageConverter() {
    MappingJackson2MessageConverter messageConverter =
        new MappingJackson2MessageConverter();
    messageConverter.setTypeIdPropertyName("_typeId");
    return messageConverter;
}
```

注意一下，你在返回 MappingJackson2MessageConverter 之前调用了 setTypeIdPropertyName()。这是非常重要的，因为它使接收者知道要将传入消息转换成什么类型。默认情况下，它将包含被转换类型的完全限定类名。但这有点不灵活，要求接收方也具有相同的类型，具有相同的完全限定类名。

为了实现更大的灵活性，可以通过调用消息转换器上的 setTypeIdMappings() 将合成类型名称映射到实际类型。例如，对消息转换器 bean 方法的以下更改将合成订单类型 ID 映射到 Order 类：

```java
@Bean
public MappingJackson2MessageConverter messageConverter() {
    MappingJackson2MessageConverter messageConverter =
        new MappingJackson2MessageConverter();
    messageConverter.setTypeIdPropertyName("_typeId");
    
    Map<String, Class<?>> typeIdMappings = new HashMap<String, Class<?>>();
    typeIdMappings.put("order", Order.class);
    messageConverter.setTypeIdMappings(typeIdMappings);
    
    messageConverter;
}
```

与在消息的 `_typeId` 属性中发送完全限定的类名不同，将发送值 order。在接收应用程序中，将配置类似的消息转换器，将 order 映射到它自己对 order 的理解。订单的实现可能在不同的包中，有不同的名称，甚至有发送者 Order 属性的一个子集。

**后期处理消息**

让我们假设，除了利润丰厚的网络业务，Taco Cloud 还决定开几家实体 Taco 连锁店。考虑到他们的任何一家餐馆也可以成为 web 业务的执行中心，他们需要一种方法来将订单的来源传达给餐馆的厨房。这将使厨房工作人员能够对商店订单采用与网络订单不同的流程。

在 Order 对象中添加一个新的 source 属性来携带此信息是合理的，可以用 WEB 来填充在线订单，用 STORE 来填充商店中的订单。但这将需要更改网站的 Order 类和厨房应用程序的 Order 类，而实际上，这些信息只需要为 taco 准备人员提供。

更简单的解决方案是在消息中添加一个自定义头信息，以承载订单的源。如果正在使用 send() 方法发送 taco 订单，这可以通过调用消息对象上的 setStringProperty() 轻松实现：

```java
jms.send("tacocloud.order.queue",
        session -> {
            Message message = session.createObjectMessage(order);
            message.setStringProperty("X_ORDER_SOURCE", "WEB");
        });
```

这里的问题是没有使用 send()。通过选择使用 convertAndSend()，Message 对象是在幕后创建的，并且不能访问它。

幸运的是，有一种方法可以在发送消息之前调整在幕后创建的 Message。通过将 MessagePostProcessor 作为最后一个参数传递给 convertAndSend()，可以在消息创建之后对其进行任何操作。下面的代码仍然使用 convertAndSend()，但是它也使用 MessagePostProcessor 在消息发送之前添加 X\_ORDER\_SOURCE 头信息：

```java
jms.convertAndSend("tacocloud.order.queue", order,
    new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message)
            throws JMSException {
            message.setStringProperty("X_ORDER_SOURCE", "WEB");
            return message;
        }
});
```

可能注意到了 MessagePostProcessor 是一个函数接口，这意味着可以使用 lambda 将其简化为匿名内部类：

```java
jms.convertAndSend("tacocloud.order.queue", order,
    message -> {
        message.setStringProperty("X_ORDER_SOURCE", "WEB");
        return message;
    });
```

尽管只需要这个特定的 MessagePostProcessor 来处理对 convertAndSend() 的调用，但是可能会发现自己使用同一个 MessagePostProcessor 来处理对 convertAndSend() 的几个不同调用。在这些情况下，也许方法引用是比 lambda 更好的选择，避免了不必要的代码重复：

```java
@GetMapping("/convertAndSend/order")
public String convertAndSendOrder() {
    Order order = buildOrder();
    jms.convertAndSend("tacocloud.order.queue", order, this::addOrderSource);
    return "Convert and sent order";
}

private Message addOrderSource(Message message) throws JMSException {
    message.setStringProperty("X_ORDER_SOURCE", "WEB");
    return message;
}
```

已经看到了几种发送消息的方法。但是，如果没有人收到信息，就没有什么用处。让我们看看如何使用 Spring 和 JMS 接收消息。
