发送消息
本节介绍如何发送消息。
使用 KafkaTemplate
本节介绍如何使用 KafkaTemplate
发送消息。
概述
KafkaTemplate
封装了一个生产者,并提供方便的方法将数据发送到 Kafka 主题。以下清单显示了 KafkaTemplate
中的相关方法
CompletableFuture<SendResult<K, V>> sendDefault(V data);
CompletableFuture<SendResult<K, V>> sendDefault(K key, V data);
CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, K key, V data);
CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, Long timestamp, K key, V data);
CompletableFuture<SendResult<K, V>> send(String topic, V data);
CompletableFuture<SendResult<K, V>> send(String topic, K key, V data);
CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, V data);
CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, Long timestamp, K key, V data);
CompletableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record);
CompletableFuture<SendResult<K, V>> send(Message<?> message);
Map<MetricName, ? extends Metric> metrics();
List<PartitionInfo> partitionsFor(String topic);
<T> T execute(ProducerCallback<K, V, T> callback);
<T> T executeInTransaction(OperationsCallback<K, V, T> callback);
// Flush the producer.
void flush();
interface ProducerCallback<K, V, T> {
T doInKafka(Producer<K, V> producer);
}
interface OperationsCallback<K, V, T> {
T doInOperations(KafkaOperations<K, V> operations);
}
有关更多详细信息,请参阅 Javadoc。
在 3.0 版本中,以前返回 ListenableFuture 的方法已更改为返回 CompletableFuture 。为了便于迁移,2.9 版本添加了一个方法 usingCompletableFuture() ,该方法提供了具有 CompletableFuture 返回类型的相同方法;此方法不再可用。
|
sendDefault
API 要求模板已提供默认主题。
该 API 接收一个 timestamp
作为参数,并将此时间戳存储在记录中。用户提供的时间戳的存储方式取决于 Kafka 主题上配置的时间戳类型。如果主题配置为使用 CREATE_TIME
,则记录用户指定的时间戳(如果未指定,则生成)。如果主题配置为使用 LOG_APPEND_TIME
,则忽略用户指定的时间戳,并由代理添加本地代理时间。
要使用模板,您可以配置一个生产者工厂并在模板的构造函数中提供它。以下示例展示了如何操作
@Bean
public ProducerFactory<Integer, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// See https://kafka.apache.org/documentation/#producerconfigs for more properties
return props;
}
@Bean
public KafkaTemplate<Integer, String> kafkaTemplate() {
return new KafkaTemplate<Integer, String>(producerFactory());
}
从 2.5 版本开始,您现在可以覆盖工厂的 ProducerConfig
属性,以从同一个工厂创建具有不同生产者配置的模板。
@Bean
public KafkaTemplate<String, String> stringTemplate(ProducerFactory<String, String> pf) {
return new KafkaTemplate<>(pf);
}
@Bean
public KafkaTemplate<String, byte[]> bytesTemplate(ProducerFactory<String, byte[]> pf) {
return new KafkaTemplate<>(pf,
Collections.singletonMap(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class));
}
请注意,ProducerFactory<?, ?>
类型的 bean(例如由 Spring Boot 自动配置的 bean)可以使用不同的缩小泛型类型进行引用。
您也可以使用标准的 <bean/>
定义来配置模板。
然后,要使用模板,您可以调用其方法之一。
当您使用带有 Message<?>
参数的方法时,主题、分区、键和时间戳信息将提供在一个消息头中,其中包含以下项目
-
KafkaHeaders.TOPIC
-
KafkaHeaders.PARTITION
-
KafkaHeaders.KEY
-
KafkaHeaders.TIMESTAMP
消息有效负载是数据。
可选地,您可以使用 ProducerListener
配置 KafkaTemplate
,以获得异步回调,其中包含发送结果(成功或失败),而不是等待 Future
完成。以下清单展示了 ProducerListener
接口的定义
public interface ProducerListener<K, V> {
void onSuccess(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata);
void onError(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata,
Exception exception);
}
默认情况下,模板配置了 LoggingProducerListener
,它记录错误,并在发送成功时不执行任何操作。
为了方便起见,如果只想实现其中一种方法,则提供了默认方法实现。
请注意,发送方法返回 CompletableFuture<SendResult>
。您可以使用侦听器注册回调,以异步接收发送结果。以下示例展示了如何操作
CompletableFuture<SendResult<Integer, String>> future = template.send("myTopic", "something");
future.whenComplete((result, ex) -> {
...
});
SendResult
具有两个属性,ProducerRecord
和 RecordMetadata
。有关这些对象的详细信息,请参阅 Kafka API 文档。
Throwable
可以被强制转换为 KafkaProducerException
;它的 producerRecord
属性包含失败的记录。
如果您希望阻塞发送线程以等待结果,您可以调用 future 的 get()
方法;建议使用带有超时的该方法。如果您设置了 linger.ms
,您可能希望在等待之前调用 flush()
,或者为了方便起见,模板有一个带有 autoFlush
参数的构造函数,该参数会导致模板在每次发送时都 flush()
。只有在您设置了 linger.ms
生产者属性并且想要立即发送部分批次时才需要刷新。
示例
本节展示了将消息发送到 Kafka 的示例
public void sendToKafka(final MyOutputData data) {
final ProducerRecord<String, String> record = createRecord(data);
CompletableFuture<SendResult<Integer, String>> future = template.send(record);
future.whenComplete((result, ex) -> {
if (ex == null) {
handleSuccess(data);
}
else {
handleFailure(data, record, ex);
}
});
}
public void sendToKafka(final MyOutputData data) {
final ProducerRecord<String, String> record = createRecord(data);
try {
template.send(record).get(10, TimeUnit.SECONDS);
handleSuccess(data);
}
catch (ExecutionException e) {
handleFailure(data, record, e.getCause());
}
catch (TimeoutException | InterruptedException e) {
handleFailure(data, record, e);
}
}
请注意,ExecutionException
的原因是 KafkaProducerException
,它具有 producerRecord
属性。
使用 RoutingKafkaTemplate
从版本 2.5 开始,您可以使用 RoutingKafkaTemplate
根据目标 topic
名称在运行时选择生产者。
路由模板**不支持**事务、execute 、flush 或 metrics 操作,因为这些操作的主题是未知的。
|
模板需要一个 java.util.regex.Pattern
到 ProducerFactory<Object, Object>
实例的映射。此映射应该是有序的(例如 LinkedHashMap
),因为它按顺序遍历;您应该在开头添加更具体的模式。
以下简单的 Spring Boot 应用程序提供了一个使用相同模板向不同主题发送消息的示例,每个主题使用不同的值序列化器。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context,
ProducerFactory<Object, Object> pf) {
// Clone the PF with a different Serializer, register with Spring for shutdown
Map<String, Object> configs = new HashMap<>(pf.getConfigurationProperties());
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);
DefaultKafkaProducerFactory<Object, Object> bytesPF = new DefaultKafkaProducerFactory<>(configs);
context.registerBean("bytesPF", DefaultKafkaProducerFactory.class, () -> bytesPF);
Map<Pattern, ProducerFactory<Object, Object>> map = new LinkedHashMap<>();
map.put(Pattern.compile("two"), bytesPF);
map.put(Pattern.compile(".+"), pf); // Default PF with StringSerializer
return new RoutingKafkaTemplate(map);
}
@Bean
public ApplicationRunner runner(RoutingKafkaTemplate routingTemplate) {
return args -> {
routingTemplate.send("one", "thing1");
routingTemplate.send("two", "thing2".getBytes());
};
}
}
此示例的相应 @KafkaListener
在 注释属性 中显示。
有关实现类似结果的另一种技术,但具有将不同类型发送到相同主题的额外功能,请参见 委托序列化器和反序列化器。
使用 DefaultKafkaProducerFactory
如 使用 KafkaTemplate
中所见,ProducerFactory
用于创建生产者。
在不使用 事务 的情况下,默认情况下,DefaultKafkaProducerFactory
会创建一个由所有客户端使用的单例生产者,如 KafkaProducer
JavaDocs 中所建议。但是,如果您在模板上调用 flush()
,这可能会导致使用相同生产者的其他线程延迟。从版本 2.3 开始,DefaultKafkaProducerFactory
有一个新的属性 producerPerThread
。当设置为 true
时,工厂将为每个线程创建(并缓存)一个单独的生产者,以避免此问题。
当 producerPerThread 为 true 时,用户代码**必须**在不再需要生产者时在工厂上调用 closeThreadBoundProducer() 。这将物理关闭生产者并将其从 ThreadLocal 中删除。调用 reset() 或 destroy() 不会清理这些生产者。
|
另请参见 KafkaTemplate
事务性和非事务性发布。
在创建 DefaultKafkaProducerFactory
时,键和/或值 Serializer
类可以通过调用仅接受属性映射的构造函数从配置中获取(请参阅 使用 KafkaTemplate
中的示例),或者 Serializer
实例可以传递给 DefaultKafkaProducerFactory
构造函数(在这种情况下,所有 Producer
都共享相同的实例)。或者,您可以提供 Supplier<Serializer>
(从版本 2.3 开始),这些 Supplier
将用于为每个 Producer
获取单独的 Serializer
实例。
@Bean
public ProducerFactory<Integer, CustomValue> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs(), null, () -> new CustomValueSerializer());
}
@Bean
public KafkaTemplate<Integer, CustomValue> kafkaTemplate() {
return new KafkaTemplate<Integer, CustomValue>(producerFactory());
}
从版本 2.5.10 开始,您现在可以在创建工厂后更新生产者属性。例如,如果您需要在凭据更改后更新 SSL 密钥/信任库位置,这可能很有用。更改不会影响现有的生产者实例;调用 reset()
以关闭任何现有的生产者,以便使用新的属性创建新的生产者。注意:您不能将事务性生产者工厂更改为非事务性,反之亦然。
现在提供了两种新方法
void updateConfigs(Map<String, Object> updates);
void removeConfig(String configKey);
从版本 2.8 开始,如果您以对象形式提供序列化器(在构造函数中或通过设置器),工厂将调用 configure()
方法以使用配置属性对其进行配置。
使用 ReplyingKafkaTemplate
版本 2.1.3 引入了 KafkaTemplate
的一个子类,以提供请求/回复语义。该类名为 ReplyingKafkaTemplate
,并具有两个附加方法;以下显示了方法签名
RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record);
RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record,
Duration replyTimeout);
(另请参阅 使用 Message<?>
进行请求/回复)。
结果是一个 CompletableFuture
,它会异步填充结果(或异常,对于超时)。结果还具有一个 sendFuture
属性,它是调用 KafkaTemplate.send()
的结果。您可以使用此 future 来确定发送操作的结果。
在版本 3.0 中,这些方法(及其 sendFuture 属性)返回的 future 已更改为 CompletableFuture ,而不是 ListenableFuture 。
|
如果使用第一个方法,或者 replyTimeout
参数为 null
,则使用模板的 defaultReplyTimeout
属性(默认情况下为 5 秒)。
从版本 2.8.8 开始,模板有一个新的方法 waitForAssignment
。如果回复容器配置为 auto.offset.reset=latest
,这将很有用,以避免在容器初始化之前发送请求和回复。
当使用手动分区分配(没有组管理)时,等待的持续时间必须大于容器的 pollTimeout 属性,因为通知将在第一个轮询完成后才会发送。
|
以下 Spring Boot 应用程序显示了如何使用该功能的示例
@SpringBootApplication
public class KRequestingApplication {
public static void main(String[] args) {
SpringApplication.run(KRequestingApplication.class, args).close();
}
@Bean
public ApplicationRunner runner(ReplyingKafkaTemplate<String, String, String> template) {
return args -> {
if (!template.waitForAssignment(Duration.ofSeconds(10))) {
throw new IllegalStateException("Reply container did not initialize");
}
ProducerRecord<String, String> record = new ProducerRecord<>("kRequests", "foo");
RequestReplyFuture<String, String, String> replyFuture = template.sendAndReceive(record);
SendResult<String, String> sendResult = replyFuture.getSendFuture().get(10, TimeUnit.SECONDS);
System.out.println("Sent ok: " + sendResult.getRecordMetadata());
ConsumerRecord<String, String> consumerRecord = replyFuture.get(10, TimeUnit.SECONDS);
System.out.println("Return value: " + consumerRecord.value());
};
}
@Bean
public ReplyingKafkaTemplate<String, String, String> replyingTemplate(
ProducerFactory<String, String> pf,
ConcurrentMessageListenerContainer<String, String> repliesContainer) {
return new ReplyingKafkaTemplate<>(pf, repliesContainer);
}
@Bean
public ConcurrentMessageListenerContainer<String, String> repliesContainer(
ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {
ConcurrentMessageListenerContainer<String, String> repliesContainer =
containerFactory.createContainer("kReplies");
repliesContainer.getContainerProperties().setGroupId("repliesGroup");
repliesContainer.setAutoStartup(false);
return repliesContainer;
}
@Bean
public NewTopic kRequests() {
return TopicBuilder.name("kRequests")
.partitions(10)
.replicas(2)
.build();
}
@Bean
public NewTopic kReplies() {
return TopicBuilder.name("kReplies")
.partitions(10)
.replicas(2)
.build();
}
}
请注意,我们可以使用 Boot 的自动配置容器工厂来创建回复容器。
如果对回复使用非平凡的反序列化器,请考虑使用 ErrorHandlingDeserializer
,它委托给您配置的反序列化器。当这样配置时,RequestReplyFuture
将异常完成,您可以捕获 ExecutionException
,其 cause
属性中包含 DeserializationException
。
从版本 2.6.7 开始,除了检测 DeserializationException
之外,模板还会调用 replyErrorChecker
函数(如果提供)。如果它返回异常,则 future 将异常完成。
这是一个示例。
template.setReplyErrorChecker(record -> {
Header error = record.headers().lastHeader("serverSentAnError");
if (error != null) {
return new MyException(new String(error.value()));
}
else {
return null;
}
});
...
RequestReplyFuture<Integer, String, String> future = template.sendAndReceive(record);
try {
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
ConsumerRecord<Integer, String> consumerRecord = future.get(10, TimeUnit.SECONDS);
...
}
catch (InterruptedException e) {
...
}
catch (ExecutionException e) {
if (e.getCause instanceof MyException) {
...
}
}
catch (TimeoutException e) {
...
}
模板设置一个头(默认情况下名为KafkaHeaders.CORRELATION_ID
),服务器端必须回显该头。
在这种情况下,以下@KafkaListener
应用程序会响应。
@SpringBootApplication
public class KReplyingApplication {
public static void main(String[] args) {
SpringApplication.run(KReplyingApplication.class, args);
}
@KafkaListener(id="server", topics = "kRequests")
@SendTo // use default replyTo expression
public String listen(String in) {
System.out.println("Server received: " + in);
return in.toUpperCase();
}
@Bean
public NewTopic kRequests() {
return TopicBuilder.name("kRequests")
.partitions(10)
.replicas(2)
.build();
}
@Bean // not required if Jackson is on the classpath
public MessagingMessageConverter simpleMapperConverter() {
MessagingMessageConverter messagingMessageConverter = new MessagingMessageConverter();
messagingMessageConverter.setHeaderMapper(new SimpleKafkaHeaderMapper());
return messagingMessageConverter;
}
}
@KafkaListener
基础设施会回显相关 ID 并确定回复主题。
有关发送回复的更多信息,请参见使用@SendTo
转发监听器结果。模板使用默认头KafKaHeaders.REPLY_TOPIC
来指示回复发送到的主题。
从 2.2 版开始,模板尝试从配置的回复容器中检测回复主题或分区。如果容器配置为监听单个主题或单个TopicPartitionOffset
,则它将用于设置回复头。如果容器以其他方式配置,则用户必须设置回复头。在这种情况下,将在初始化期间写入一条INFO
日志消息。以下示例使用KafkaHeaders.REPLY_TOPIC
record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, "kReplies".getBytes()));
当您使用单个回复TopicPartitionOffset
配置时,只要每个实例监听不同的分区,您就可以对多个模板使用相同的回复主题。当使用单个回复主题配置时,每个实例必须使用不同的group.id
。在这种情况下,所有实例都会收到每个回复,但只有发送请求的实例会找到相关 ID。这可能对自动缩放有用,但会带来额外的网络流量开销和丢弃每个不需要的回复的小成本。当您使用此设置时,我们建议您将模板的sharedReplyTopic
设置为true
,这会将意外回复的日志级别降低到 DEBUG,而不是默认的 ERROR。
以下是如何将回复容器配置为使用相同的共享回复主题的示例。
@Bean
public ConcurrentMessageListenerContainer<String, String> replyContainer(
ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {
ConcurrentMessageListenerContainer<String, String> container = containerFactory.createContainer("topic2");
container.getContainerProperties().setGroupId(UUID.randomUUID().toString()); // unique
Properties props = new Properties();
props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); // so the new group doesn't get old replies
container.getContainerProperties().setKafkaConsumerProperties(props);
return container;
}
如果您有多个客户端实例,并且您没有按照上一段中讨论的方式配置它们,则每个实例都需要一个专用的回复主题。另一种方法是设置KafkaHeaders.REPLY_PARTITION 并为每个实例使用一个专用分区。Header 包含一个四字节 int(大端)。服务器必须使用此头将回复路由到正确的分区(@KafkaListener 会执行此操作)。但是,在这种情况下,回复容器不能使用 Kafka 的组管理功能,并且必须配置为监听固定分区(通过在其ContainerProperties 构造函数中使用TopicPartitionOffset )。
|
DefaultKafkaHeaderMapper 需要 Jackson 在类路径上(用于@KafkaListener )。如果它不可用,则消息转换器没有头映射器,因此您必须使用SimpleKafkaHeaderMapper 配置一个MessagingMessageConverter ,如前所示。
|
默认情况下,使用 3 个头
-
KafkaHeaders.CORRELATION_ID
- 用于将回复与请求关联起来 -
KafkaHeaders.REPLY_TOPIC
- 用于告诉服务器在哪里回复 -
KafkaHeaders.REPLY_PARTITION
- (可选)用于告诉服务器回复到哪个分区
这些头名称由 @KafkaListener
基础设施用于路由回复。
从版本 2.3 开始,您可以自定义头名称 - 模板有 3 个属性 correlationHeaderName
、replyTopicHeaderName
和 replyPartitionHeaderName
。如果您的服务器不是 Spring 应用程序(或不使用 @KafkaListener
),这将很有用。
相反,如果请求应用程序不是 Spring 应用程序并将关联信息放在不同的头中,从版本 3.0 开始,您可以在监听器容器工厂上配置一个自定义 correlationHeaderName ,该头将被回显。以前,监听器必须回显自定义关联头。
|
使用 Message<?>
的请求/回复
版本 2.7 在 ReplyingKafkaTemplate
中添加了方法来发送和接收 spring-messaging
的 Message<?>
抽象
RequestReplyMessageFuture<K, V> sendAndReceive(Message<?> message);
<P> RequestReplyTypedMessageFuture<K, V, P> sendAndReceive(Message<?> message,
ParameterizedTypeReference<P> returnType);
这些将使用模板的默认 replyTimeout
,还有可以接受方法调用中超时的重载版本。
在版本 3.0 中,这些方法(及其 sendFuture 属性)返回的 future 已更改为 CompletableFuture ,而不是 ListenableFuture 。
|
如果消费者的 Deserializer
或模板的 MessageConverter
可以通过配置或回复消息中的类型元数据来转换有效负载,则使用第一个方法。
如果您需要为返回类型提供类型信息以帮助消息转换器,则使用第二种方法。这也允许同一个模板接收不同的类型,即使回复中没有类型元数据,例如服务器端不是 Spring 应用程序时。以下是一个后者的例子
-
Java
-
Kotlin
@Bean
ReplyingKafkaTemplate<String, String, String> template(
ProducerFactory<String, String> pf,
ConcurrentKafkaListenerContainerFactory<String, String> factory) {
ConcurrentMessageListenerContainer<String, String> replyContainer =
factory.createContainer("replies");
replyContainer.getContainerProperties().setGroupId("request.replies");
ReplyingKafkaTemplate<String, String, String> template =
new ReplyingKafkaTemplate<>(pf, replyContainer);
template.setMessageConverter(new ByteArrayJsonMessageConverter());
template.setDefaultTopic("requests");
return template;
}
@Bean
fun template(
pf: ProducerFactory<String?, String>?,
factory: ConcurrentKafkaListenerContainerFactory<String?, String?>
): ReplyingKafkaTemplate<String?, String, String?> {
val replyContainer = factory.createContainer("replies")
replyContainer.containerProperties.groupId = "request.replies"
val template = ReplyingKafkaTemplate(pf, replyContainer)
template.messageConverter = ByteArrayJsonMessageConverter()
template.defaultTopic = "requests"
return template
}
-
Java
-
Kotlin
RequestReplyTypedMessageFuture<String, String, Thing> future1 =
template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(),
new ParameterizedTypeReference<Thing>() { });
log.info(future1.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString());
Thing thing = future1.get(10, TimeUnit.SECONDS).getPayload();
log.info(thing.toString());
RequestReplyTypedMessageFuture<String, String, List<Thing>> future2 =
template.sendAndReceive(MessageBuilder.withPayload("getThings").build(),
new ParameterizedTypeReference<List<Thing>>() { });
log.info(future2.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString());
List<Thing> things = future2.get(10, TimeUnit.SECONDS).getPayload();
things.forEach(thing1 -> log.info(thing1.toString()));
val future1: RequestReplyTypedMessageFuture<String?, String?, Thing?>? =
template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(),
object : ParameterizedTypeReference<Thing?>() {})
log.info(future1?.sendFuture?.get(10, TimeUnit.SECONDS)?.recordMetadata?.toString())
val thing = future1?.get(10, TimeUnit.SECONDS)?.payload
log.info(thing.toString())
val future2: RequestReplyTypedMessageFuture<String?, String?, List<Thing?>?>? =
template.sendAndReceive(MessageBuilder.withPayload("getThings").build(),
object : ParameterizedTypeReference<List<Thing?>?>() {})
log.info(future2?.sendFuture?.get(10, TimeUnit.SECONDS)?.recordMetadata.toString())
val things = future2?.get(10, TimeUnit.SECONDS)?.payload
things?.forEach(Consumer { thing1: Thing? -> log.info(thing1.toString()) })
回复类型 Message<?>
当 @KafkaListener
返回 Message<?>
时,在 2.5 之前的版本中,有必要填充回复主题和关联 ID 头。在这个例子中,我们使用请求中的回复主题头
@KafkaListener(id = "requestor", topics = "request")
@SendTo
public Message<?> messageReturn(String in) {
return MessageBuilder.withPayload(in.toUpperCase())
.setHeader(KafkaHeaders.TOPIC, replyTo)
.setHeader(KafkaHeaders.KEY, 42)
.setHeader(KafkaHeaders.CORRELATION_ID, correlation)
.build();
}
这也展示了如何在回复记录上设置键。
从版本 2.5 开始,框架将检测这些头是否丢失并用主题填充它们 - 可能是从 @SendTo
值确定的主题,也可能是传入的 KafkaHeaders.REPLY_TOPIC
头(如果存在)。它还会回显传入的 KafkaHeaders.CORRELATION_ID
和 KafkaHeaders.REPLY_PARTITION
(如果存在)。
@KafkaListener(id = "requestor", topics = "request")
@SendTo // default REPLY_TOPIC header
public Message<?> messageReturn(String in) {
return MessageBuilder.withPayload(in.toUpperCase())
.setHeader(KafkaHeaders.KEY, 42)
.build();
}
聚合多个回复
在 使用 ReplyingKafkaTemplate
中的模板严格用于单一请求/回复场景。对于单个消息有多个接收者返回回复的情况,可以使用 AggregatingReplyingKafkaTemplate
。这是 散布-收集企业集成模式 客户端的实现。
与 ReplyingKafkaTemplate
一样,AggregatingReplyingKafkaTemplate
构造函数接受一个生产者工厂和一个监听器容器来接收回复;它还有一个第三个参数 BiPredicate<List<ConsumerRecord<K, R>>, Boolean> releaseStrategy
,每次收到回复时都会咨询该参数;当谓词返回 true
时,ConsumerRecord
的集合将用于完成 sendAndReceive
方法返回的 Future
。
还有一个额外的属性 returnPartialOnTimeout
(默认值为 false)。当将其设置为 true
时,它不会用 KafkaReplyTimeoutException
完成 future,而是用部分结果正常完成 future(只要至少收到一条回复记录)。
从版本 2.3.5 开始,谓词也会在超时后被调用(如果 returnPartialOnTimeout
为 true
)。第一个参数是当前的记录列表;第二个参数是 true
,如果此调用是由于超时造成的。谓词可以修改记录列表。
AggregatingReplyingKafkaTemplate<Integer, String, String> template =
new AggregatingReplyingKafkaTemplate<>(producerFactory, container,
coll -> coll.size() == releaseSize);
...
RequestReplyFuture<Integer, String, Collection<ConsumerRecord<Integer, String>>> future =
template.sendAndReceive(record);
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
ConsumerRecord<Integer, Collection<ConsumerRecord<Integer, String>>> consumerRecord =
future.get(30, TimeUnit.SECONDS);
请注意,返回值是 ConsumerRecord
,其值为 ConsumerRecord
的集合。 "外部" ConsumerRecord
不是 "真实" 记录,它是模板合成的,作为接收到的请求的实际回复记录的占位符。当正常释放发生(释放策略返回 true)时,主题将设置为 aggregatedResults
;如果 returnPartialOnTimeout
为 true,并且发生超时(并且至少收到一条回复记录),则主题将设置为 partialResultsAfterTimeout
。模板为这些 "主题" 名称提供了常量静态变量。
/**
* Pseudo topic name for the "outer" {@link ConsumerRecords} that has the aggregated
* results in its value after a normal release by the release strategy.
*/
public static final String AGGREGATED_RESULTS_TOPIC = "aggregatedResults";
/**
* Pseudo topic name for the "outer" {@link ConsumerRecords} that has the aggregated
* results in its value after a timeout.
*/
public static final String PARTIAL_RESULTS_AFTER_TIMEOUT_TOPIC = "partialResultsAfterTimeout";
Collection
中的真实 ConsumerRecord
包含接收回复的实际主题。
回复的监听器容器 **必须** 配置为 AckMode.MANUAL 或 AckMode.MANUAL_IMMEDIATE ;消费者属性 enable.auto.commit 必须为 false (从版本 2.3 开始的默认值)。为了避免任何可能的消息丢失,模板只在没有未完成的请求时提交偏移量,即当最后一个未完成的请求被释放策略释放时。在重新平衡后,可能会出现重复的回复传递;对于任何正在进行的请求,这些都会被忽略;当收到已释放回复的重复回复时,您可能会看到错误日志消息。
|
如果您使用 ErrorHandlingDeserializer 与此聚合模板一起使用,框架不会自动检测 DeserializationException 。相反,记录(带有 null 值)将完整返回,并且反序列化异常在标头中。建议应用程序调用实用程序方法 ReplyingKafkaTemplate.checkDeserialization() 方法来确定是否发生了反序列化异常。有关更多信息,请参阅其 JavaDocs。replyErrorChecker 也不适用于此聚合模板;您应该对回复的每个元素执行检查。
|