# 8.2.3 从 RabbitMQ 接收消息

使用 RabbitTemplate 发送消息与使用 JmsTemplate 发送消息差别不大。事实证明，从 RabbitMQ 队列接收消息与从 JMS 接收消息并没有太大的不同。

与 JMS 一样，有两个选择：

* 使用 RabbitTemplate 从队列中拉取消息
* 获取被推送到 @RabbitListener 注解的方法中的消息

让我们从基于拉模型的 RabbitTemplate.receive() 方法开始。

**使用 RabbitTemplate 接收消息**

RabbitTemplate 有多个从队列中拉取消息的方法，一部分最有用的方法如下所示：

```java
// 接收消息
Message receive() throws AmqpException;
Message receive(String queueName) throws AmqpException;
Message receive(long timeoutMillis) throws AmqpException;
Message receive(String queueName, long timeoutMillis) throws AmqpException;
​
// 接收从消息转换过来的对象
Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
Object receiveAndConvert(long timeoutMillis) throws AmqpException;
Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException;
​
// 接收从消息转换过来的类型安全的对象
<T> T receiveAndConvert(ParameterizedTypeReference<T> type) throws AmqpException;
<T> T receiveAndConvert(String queueName, ParameterizedTypeReference<T> type) throws AmqpException;
<T> T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference<T> type) throws AmqpException;
<T> T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference<T> type) throws AmqpException;
```

这些方法是前面描述的 send() 和 convertAndSend() 方法的镜像。send() 用于发送原始 Message 对象，而 receive() 从队列接收原始 Message 对象。同样地，receiveAndConvert() 接收消息，并在返回消息之前使用消息转换器将其转换为域对象。

但是在方法签名方面有一些明显的区别。首先，这些方法都不以交换键或路由键作为参数。这是因为交换和路由键用于将消息路由到队列，但是一旦消息在队列中，它们的下一个目的地就是将消息从队列中取出的使用者。使用应用程序不需要关心交换或路由键，队列是在消费应用程序是仅仅需要知道一个东西。

许多方法接受一个 long 参数来表示接收消息的超时。默认情况下，接收超时为 0 毫秒。也就是说，对 receive() 的调用将立即返回，如果没有可用的消息，则可能返回空值。这与 receive() 方法在 JmsTemplate 中的行为有明显的不同。通过传入超时值，可以让 receive() 和 receiveAndConvert() 方法阻塞，直到消息到达或超时过期。但是，即使使用非零超时，代码也要准备好处理返回的 null 值。

让我们看看如何将其付诸实践。下面程序清单显示了 OrderReceiver 的一个新的基于 Rabbit 的实现，它使用 RabbitTemplate 来接收订单。**程序清单 8.6 使用 RabbitTemplate 从 RabbitMQ 拉取订单**

```java
package tacos.kitchen.messaging.rabbit;
​
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
​
@Component
public class RabbitOrderReceiver {
    
    private RabbitTemplate rabbit;
    private MessageConverter converter;
    
    @Autowired
    public RabbitOrderReceiver(RabbitTemplate rabbit) {
        this.rabbit = rabbit;
        this.converter = rabbit.getMessageConverter();
    }
    
    public Order receiveOrder() {
        Message message = rabbit.receive("tacocloud.orders");
        return message != null
            ? (Order) converter.fromMessage(message)
            : null;
    }
}
```

receiveOrder() 方法是所有操作发生的地方。它调用所注入的 RabbitTemplate 上的 receive() 方法来从 tacocloud.queue 中获取订单。它不提供超时值，因此只能假设调用立即返回 Message 或 null。如果返回一条 Message，则使用 RabbitTemplate 中的 MessageConverter 将 Message 转换为 Order。另一方面，如果 receive() 返回 null，则返回 null。

根据实际情况的不同，可能容忍一个小的延迟。例如，在 Taco Cloud 厨房项目的头顶显示器中，如果没有订单信息出现，可以等待一下，可以决定等 30 秒后再放弃。然后，可以将 receiveOrder() 方法更改为传递一个 30,000 毫秒的延迟后再调用 receive()：

```java
public Order receiveOrder() {
    Message message = rabbit.receive("tacocloud.order.queue", 30000);
    
    return message != null
        ? (Order) converter.fromMessage(message)
        : null;
}
```

如果你和我一样，看到这样一个硬编码的数字会让你有点不舒服。那么创建一个带 @ConfigurationProperties 注解的类是个好想法，这样就可以使用 Spring Boot 的配置属性来配置超时。如果不是 Spring Boot 已经提供了这样的配置属性，我也会觉得硬编码的数字很不舒服。如果希望通过配置设置超时，只需删除 receive() 调用中的超时值，并在配置中使用 spring.rabbitmq.template.receive-timeout 属性设置它：

```yaml
spring:
  rabbitmq:
    template:
      receive-timeout: 30000
```

回到 receiveOrder() 方法，请注意，必须使用 RabbitTemplate 中的消息转换器来将传入 Message 对象转换为 Order 对象。但是如果 RabbitTemplate 携带了一个消息转换器，为什么它不能进行转换呢？这正是 receiveAndConvert() 方法的用途。使用 receiveAndConvert()，可以像这样重写 receiveOrder()：

```java
public Oreder receiveOrder() {
    return (Order) rabbit.receiveAndConvert("tacocloud.order.queue");
}
```

那就简单多了，不是吗？所看到的唯一麻烦的事情就是从 Object 到 Order 的转换。不过，除了演员阵容，还有另一种选择。相反，你可以传递一个 ParameterizedTypeReference 来直接接收一个 Order 对象：

```java
public Order receiveOrder() {
    return rabbit.receiveAndConvert("tacocloud.order.queue",
               new ParameterizedTypeReference<Order>() {});
}
```

这是否比类型转换更好还值得商榷，但它是一种比类型转换更安全的方法。使用 receiveAndConvert() 的 ParameterizedTypeReference 的惟一要求是消息转换器必须是 SmartMessageConverter 的实现；Jackson2JsonMessageConverter 是唯一可以选择的开箱即用的实现。

JmsTemplate 提供的拉模型适用于许多用例，但通常最好有监听消息并在消息到达时调用的代码。让我们看看如何编写响应 RabbitMQ 消息的消息驱动 bean。

**使用监听器处理 RabbitMQ 消息**

对于消息驱动的 RabbitMQ bean，Spring 提供了 RabbitListener，相当于 RabbitMQ 中的 JmsListener。要指定当消息到达 RabbitMQ 队列时应该调用某个方法，请在相应的 bean 方法上使用 @RabbitTemplate 进行注解 。

例如，下面的程序清单显示了 OrderReceiver 的 RabbitMQ 实现，它被注解为监听订单消息，而不是使用 RabbitTemplate 来轮询订单消息。

{% code title="程序清单 8.7 声明一个方法作为 RabbitMQ 消息监听器" %}

```java
package tacos.kitchen.messaging.rabbit.listener;
​
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
​
@Component
public class OrderListener {
    
    private KitchenUI ui;
    
    @Autowired
    public OrderListener(KitchenUI ui) {
        this.ui = ui;
    }
    
    @RabbitListener(queues = "tacocloud.order.queue")
    public void receiveOrder(Order order) {
        ui.displayOrder(order);
    }
}
```

{% endcode %}

这与程序清单 8.4 中的代码非常相似。实际上，唯一改变的是监听器注解—从 @JmsListener 变为了 @RabbitListener。尽管 @RabbitListener 非常棒，但这种近乎复制的代码让我对 @RabbitListener 没什么可说的，而我之前还没有对 @JmsListener 说过。它们都非常适合编写从各自的 broker 推送给它们的消息的代码 —— JMS broker 用于 @JmsListener，RabbitMQ broker 用于 @RabbitListener。

虽然在前面的段落中可能感觉到了 @RabbitListener 不是那么让人兴奋。事实上，@RabbitListener 与 @JmsListener 的工作方式非常相似，这一点非常令人兴奋！这意味着在使用 RabbitMQ 与 Artemis 或 ActiveMQ 时，不需要学习完全不同的编程模型。同样令人兴奋的是 RabbitTemplate 和 JmsTemplate 之间的相似性。

在结束本章时，让我们继续关注 Spring 支持的另一个消息传递中间件：Apache Kafka。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v5/di-8-zhang-fa-song-yi-bu-xiao-xi/8.2-shi-yong-rabbitmq-he-amqp/untitled.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
