跳到主要内容
版本:4.1.1

使用 @SendTo 转发监听器结果

Doubao 中英对照 Forwarding Listener Results using @SendTo

从版本 2.0 开始,如果你还用 @SendTo 注解标注了一个 @KafkaListener,并且方法调用返回了结果,则该结果会被转发到 @SendTo 所指定的主题。

@SendTo 的值可以有多种形式:

  • @SendTo("someTopic") 路由到字面主题。
  • @SendTo("#{someExpression}") 路由到在应用上下文初始化期间对表达式求值一次所确定的主题。
  • @SendTo("!{someExpression}") 路由到在运行时对表达式求值所确定的主题。用于求值的 #root 对象有三个属性:
    • request:入站 ConsumerRecord(对于批监听器则为 ConsumerRecords 对象)。
    • source:由 request 转换而来的 org.springframework.messaging.Message<?>。
    • result:方法返回结果。
  • @SendTo(无属性):这被视为 !{source.headers['kafka_replyTopic']}(自版本 2.1.3 起)。

从版本 2.1.11 和 2.2.1 开始,属性占位符会在 @SendTo 值内被解析。

表达式求值的结果必须是一个表示主题名的 String。以下示例展示了使用 @SendTo 的各种方式:

@KafkaListener(topics = "annotated21")
@SendTo("!{request.value()}") // runtime SpEL
public String replyingListener(String in) {
...
}

@KafkaListener(topics = "${some.property:annotated22}")
@SendTo("#{myBean.replyTopic}") // config time SpEL
public Collection<String> replyingBatchListener(List<String> in) {
...
}

@KafkaListener(topics = "annotated23", errorHandler = "replyErrorHandler")
@SendTo("annotated23reply") // static reply topic definition
public String replyingListenerWithErrorHandler(String in) {
...
}
...
@KafkaListener(topics = "annotated25")
@SendTo("annotated25reply1")
public class MultiListenerSendTo {

@KafkaHandler
public String foo(String in) {
...
}

@KafkaHandler
@SendTo("!{'annotated25reply2'}")
public String bar(@Payload(required = false) KafkaNull nul,
@Header(KafkaHeaders.RECEIVED_KEY) int key) {
...
}

}
important

为了支持 @SendTo,必须为监听器容器工厂提供一个 KafkaTemplate(在其 replyTemplate 属性中),用于发送回复。这应该是一个 KafkaTemplate,而不是在客户端用于请求/回复处理的 ReplyingKafkaTemplate。使用 Spring Boot 时,它会自动将模板装配到工厂中;当你配置自己的工厂时,必须如下例所示进行设置。

从版本 2.2 开始,你可以向监听器容器工厂添加一个 ReplyHeadersConfigurer。它会被查阅以确定你想在回复消息中设置哪些 header。以下示例展示了如何添加 ReplyHeadersConfigurer:

@Bean
public ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf());
factory.setReplyTemplate(template());
factory.setReplyHeadersConfigurer((k, v) -> k.equals("cat"));
return factory;
}

如果你愿意,也可以添加更多 header。以下示例展示了如何这样做:

@Bean
public ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf());
factory.setReplyTemplate(template());
factory.setReplyHeadersConfigurer(new ReplyHeadersConfigurer() {

@Override
public boolean shouldCopy(String headerName, Object headerValue) {
return false;
}

@Override
public Map<String, Object> additionalHeaders() {
return Collections.singletonMap("qux", "fiz");
}

});
return factory;
}

当你使用 @SendTo 时,必须在 ConcurrentKafkaListenerContainerFactory 的 replyTemplate 属性中配置一个 KafkaTemplate 来执行发送。Spring Boot 会自动装配其自动配置的模板(如果存在单个实例,则装配任意一个)。

备注

除非你使用请求/回复语义,否则只会使用简单的 send(topic, value) 方法,因此你可能希望创建一个子类来生成分区或 key。以下示例展示了如何这样做:

@Bean
public KafkaTemplate<String, String> myReplyingTemplate() {
return new KafkaTemplate<String, String>(producerFactory()) {

@Override
public CompletableFuture<SendResult<String, String>> send(String topic, String data) {
return super.send(topic, partitionForData(data), keyForData(data), data);
}

...

};
}
important

如果监听器方法返回 Message<?> 或 Collection<Message<?>>,则由监听器方法负责为回复设置消息 header。例如,当处理来自 ReplyingKafkaTemplate 的请求时,你可能会这样做:

@KafkaListener(id = "messageReturned", topics = "someTopic")
public Message<?> listen(String in, @Header(KafkaHeaders.REPLY_TOPIC) byte[] replyTo,
@Header(KafkaHeaders.CORRELATION_ID) byte[] correlation) {
return MessageBuilder.withPayload(in.toUpperCase())
.setHeader(KafkaHeaders.TOPIC, replyTo)
.setHeader(KafkaHeaders.KEY, 42)
.setHeader(KafkaHeaders.CORRELATION_ID, correlation)
.setHeader("someOtherHeader", "someValue")
.build();
}

:::

使用请求/回复语义时,目标分区可以由发送方请求。

备注

你即使在没有返回结果时也可以用 @SendTo 注解标注一个 @KafkaListener 方法。这是为了允许配置一个 errorHandler,它可以将关于失败消息投递的信息转发到某个主题。以下示例展示了如何这样做:

@KafkaListener(id = "voidListenerWithReplyingErrorHandler", topics = "someTopic",
errorHandler = "voidSendToErrorHandler")
@SendTo("failures")
public void voidListenerWithReplyingErrorHandler(String in) {
throw new RuntimeException("fail");
}

@Bean
public KafkaListenerErrorHandler voidSendToErrorHandler() {
return (m, e) -> {
return ... // some information about the failure and input data
};
}

有关更多信息,请参阅处理异常。
:::

备注

如果监听器方法返回一个 Iterable,默认情况下会为每个元素作为 value 发送一条记录。从版本 2.3.5 开始,将 @KafkaListener 上的 splitIterables 属性设置为 false,整个结果将作为单个 ProducerRecord 的 value 发送。这需要在回复模板的生产者配置中有合适的序列化器。但是,如果回复是 Iterable<Message<?>>,则该属性被忽略,每条消息单独发送。