跳到主要内容
版本:4.1.1

回复管理

Doubao 中英对照 Reply Management

MessageListenerAdapter 中已有的支持已经让你的方法可以具有非 void 返回类型。在这种情况下,调用结果会被封装在一条消息中,发送到原始消息的 ReplyToAddress 消息头所指定的地址,或发送到监听器上配置的默认地址。你可以使用 messaging 抽象的 @SendTo 注解来设置该默认地址。

假设我们的 processOrder 方法现在应当返回一个 OrderStatus,我们可以如下编写它以自动发送回复:

@RabbitListener(destination = "myQueue")
@SendTo("status")
public OrderStatus processOrder(Order order) {
// order processing
return status;
}

如果你需要以与传输无关的方式设置额外的消息头,则可以改为返回一个 Message,如下所示:

@RabbitListener(destination = "myQueue")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
// order processing
return MessageBuilder
.withPayload(status)
.setHeader("code", 1234)
.build();
}

或者,你可以在容器工厂属性 beforeSendReplyMessagePostProcessors 中使用 MessagePostProcessor 来添加更多消息头。从 2.2.3 版开始,被调用的 bean/方法会在回复消息中可用,消息后处理器可以利用它把信息回传给调用方:

factory.setBeforeSendReplyPostProcessors(msg -> {
msg.getMessageProperties().setHeader("calledBean",
msg.getMessageProperties().getTargetBean().getClass().getSimpleName());
msg.getMessageProperties().setHeader("calledMethod",
msg.getMessageProperties().getTargetMethod().getName());
return m;
});

从 2.2.5 版开始,你可以配置一个 ReplyPostProcessor,在回复消息发送之前对其进行修改;它是在 correlationId 消息头被设置为与请求匹配之后调用的。

@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoCustomHeader")
public String capitalizeWithHeader(String in) {
return in.toUpperCase();
}

@Bean
public ReplyPostProcessor echoCustomHeader() {
return (req, resp) -> {
resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader"));
return resp;
};
}

从 3.0 版开始,你可以在容器工厂上(而非注解上)配置后处理器。

factory.setReplyPostProcessorProvider(id -> (req, resp) -> {
resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader"));
return resp;
});

id 参数就是监听器 id。

注解上的设置会覆盖工厂设置。

@SendTo 的值被假定为一对回复 exchange 和 routingKey,遵循 exchange/routingKey 模式,其中任一部分都可以省略。有效值如下:

  • thing1/thing2:replyTo 交换机和 routingKey。thing1/:replyTo 交换机和默认(空)routingKey。thing2 或 /thing2:replyTo routingKey 和默认(空)交换机。/ 或空:replyTo 默认交换机和默认 routingKey。

此外,你可以使用不带 value 属性的 @SendTo。这种情况等同于一个空的 sendTo 模式。仅当入站消息没有 replyToAddress 属性时才会使用 @SendTo。

从 1.5 版开始,@SendTo 的值可以是一个 bean 初始化 SpEL 表达式,如下例所示:

@RabbitListener(queues = "test.sendTo.spel")
@SendTo("#{spelReplyTo}")
public String capitalizeWithSendToSpel(String foo) {
return foo.toUpperCase();
}
...
@Bean
public String spelReplyTo() {
return "test.sendTo.reply.spel";
}

该表达式必须解析为一个 String,它可以是一个简单的队列名(发送到默认交换机),或采用前面示例之前所讨论的 exchange/routingKey 形式。

备注

#{…​} 表达式在初始化期间只求值一次。

对于动态回复路由,消息发送方应当包含一个 reply_to 消息属性,或使用另一种运行时 SpEL 表达式(在下一个示例之后描述)。

从 1.6 版开始,@SendTo 可以是一个 SpEL 表达式,在运行时针对请求和回复求值,如下例所示:

@RabbitListener(queues = "test.sendTo.spel")
@SendTo("!{'some.reply.queue.with.' + result.queueName}")
public Bar capitalizeWithSendToSpel(Foo foo) {
return processTheFooAndReturnABar(foo);
}

SpEL 表达式的运行时性质由 !{…​} 分隔符表示。该表达式的求值上下文 #root 对象有三个属性:

  • request:o.s.amqp.core.Message 请求对象。
  • source:转换之后的 o.s.messaging.Message<?>。
  • result:方法返回结果。

该上下文有一个 map 属性访问器、一个标准类型转换器和一个 bean 解析器,后者让你可以引用上下文中的其他 bean(例如 @someBeanName.determineReplyQ(request, result))。

总之,#{…​} 在初始化期间只求值一次,此时 #root 对象是应用上下文。bean 通过其名称引用。!{…​} 在运行时为每条消息求值,此时根对象具有前面列出的属性。bean 通过其名称引用,并以 @ 为前缀。

从 2.1 版开始,还支持简单的属性占位符(例如 ${some.reply.to})。对于更早的版本,可以使用以下方式作为变通办法,如下例所示:

@RabbitListener(queues = "foo")
@SendTo("#{environment['my.send.to']}")
public String listen(Message in) {
...
return ...
}