此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring for Apache Kafka 3.3.0spring-doc.cn

发送消息

本节介绍如何发送消息。spring-doc.cn

KafkaTemplate

本节介绍如何使用 发送消息。KafkaTemplatespring-doc.cn

概述

它包装了一个生产者,并提供了将数据发送到 Kafka 主题的便捷方法。 下面的清单显示了 :KafkaTemplateKafkaTemplatespring-doc.cn

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);

}

有关更多详细信息,请参阅 Javadocspring-doc.cn

API 要求已向模板提供默认主题。sendDefaultspring-doc.cn

API 将 a 作为参数,并将此时间戳存储在记录中。 用户提供的时间戳的存储方式取决于在 Kafka 主题上配置的时间戳类型。 如果主题配置为使用 ,则会记录用户指定的时间戳(如果未指定,则生成)。 如果主题配置为使用 ,则忽略用户指定的时间戳,并且代理会添加本地代理时间。timestampCREATE_TIMELOG_APPEND_TIMEspring-doc.cn

and 方法委托给底层 Producer 上的相同方法。 该方法提供对底层 Producer 的直接访问。metricspartitionsForexecutespring-doc.cn

要使用该模板,您可以配置生产者工厂并在模板的构造函数中提供它。 以下示例显示了如何执行此操作:spring-doc.cn

@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 开始,您现在可以覆盖工厂的属性,以从同一工厂创建具有不同生产者配置的模板。ProducerConfigspring-doc.cn

@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));
}

请注意,可以使用不同的狭窄泛型类型引用类型的 bean(例如由 Spring Boot 自动配置的 bean)。ProducerFactory<?, ?>spring-doc.cn

您还可以使用标准定义配置模板。<bean/>spring-doc.cn

然后,要使用模板,您可以调用其方法之一。spring-doc.cn

当您使用带有参数的方法时,主题、分区、键和时间戳信息将在消息标头中提供,其中包括以下项目:Message<?>spring-doc.cn

消息有效负载是数据。spring-doc.cn

(可选)您可以使用 a 配置 ,以获取包含发送结果(成功或失败)的异步回调,而不是等待 完成。 下面的清单显示了接口的定义:KafkaTemplateProducerListenerFutureProducerListenerspring-doc.cn

public interface ProducerListener<K, V> {

    default void onSuccess(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata) {
	}

    default void onError(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata, Exception exception) {
	}

}

默认情况下,模板配置了 ,该模板会记录错误,并在发送成功时不执行任何操作。LoggingProducerListenerspring-doc.cn

为方便起见,如果您只想实现其中一种方法,则提供了默认方法实现。spring-doc.cn

请注意,send 方法返回一个 . 您可以向侦听器注册回调,以异步接收发送结果。 以下示例显示了如何执行此操作:CompletableFuture<SendResult>spring-doc.cn

CompletableFuture<SendResult<Integer, String>> future = template.send("myTopic", "something");
future.whenComplete((result, ex) -> {
    ...
});

SendResult具有两个属性 a 和 。 有关这些对象的信息,请参阅 Kafka API 文档。ProducerRecordRecordMetadataspring-doc.cn

可以转换为 ;其属性包含 Failed 记录。ThrowableKafkaProducerExceptionproducerRecordspring-doc.cn

如果你希望阻塞发送线程等待结果,你可以调用 future 的方法;建议使用带有 timeout 的方法。 如果您设置了 ,您可能希望在等待之前调用 ,或者为方便起见,模板具有一个构造函数,该构造函数带有一个参数,该参数会导致模板在每次发送时进行。 仅当您设置了 producer 属性并希望立即发送部分批处理时,才需要刷新。get()linger.msflush()autoFlushflush()linger.msspring-doc.cn

例子

本节介绍向 Kafka 发送消息的示例:spring-doc.cn

示例 1.非阻塞 (异步)
public void sendToKafka(final MyOutputData data) {
    final ProducerRecord<String, String> record = createRecord(data);

    CompletableFuture<SendResult<String, String>> future = template.send(record);
    future.whenComplete((result, ex) -> {
        if (ex == null) {
            handleSuccess(data);
        }
        else {
            handleFailure(data, record, ex);
        }
    });
}
阻止 (Sync)
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);
    }
}

请注意,原因在于属性。ExecutionExceptionKafkaProducerExceptionproducerRecordspring-doc.cn

RoutingKafkaTemplate

从版本 2.5 开始,您可以使用 a 在运行时根据目标名称选择生产者。RoutingKafkaTemplatetopicspring-doc.cn

路由模板不支持 transactions、 、 或 操作,因为这些操作的主题未知。executeflushmetrics

该模板需要 的 映射到实例。 此 map 应排序(例如 a ),因为它是按顺序遍历的;您应该在开始时添加更具体的模式。java.util.regex.PatternProducerFactory<Object, Object>LinkedHashMapspring-doc.cn

以下简单的 Spring Boot 应用程序提供了一个示例,说明如何使用同一模板发送到不同的主题,每个主题使用不同的值序列化器。spring-doc.cn

@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());
        };
    }

}

此示例的相应 显示在 Annotation Properties 中。@KafkaListenerspring-doc.cn

有关实现类似结果的另一种技术,但具有将不同类型的发送到同一主题的附加功能,请参阅 Delegating Serializer 和 Deserializerspring-doc.cn

DefaultKafkaProducerFactory

使用 KafkaTemplate 所示,a 用于创建生产者。ProducerFactoryspring-doc.cn

当不使用 Transactions 时,默认情况下,它会创建一个供所有客户端使用的单例生产者,如 JavaDocs 中的建议。 但是,如果您调用模板,这可能会导致使用同一生产者的其他线程出现延迟。 从版本 2.3 开始,它有一个新属性 。 当设置为 时,工厂将为每个线程创建(并缓存)一个单独的 producer,以避免此问题。DefaultKafkaProducerFactoryKafkaProducerflush()DefaultKafkaProducerFactoryproducerPerThreadtruespring-doc.cn

When is 时,当不再需要 producer 时,用户代码必须在工厂上调用。 这将以物理方式关闭生产者并将其从 . 调用 or 不会清理这些生产者。producerPerThreadtruecloseThreadBoundProducer()ThreadLocalreset()destroy()

创建时,可以通过调用仅接受属性Map的构造函数(请参阅使用KafkaTemplate中的示例)从配置中选取键和/或值类,或者可以将实例传递给构造函数(在这种情况下,所有s共享相同的实例)。 或者,您可以提供 s (从版本 2.3 开始),该 s 将用于为每个实例获取单独的实例:DefaultKafkaProducerFactorySerializerSerializerDefaultKafkaProducerFactoryProducerSupplier<Serializer>SerializerProducerspring-doc.cn

@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 开始,您现在可以在创建工厂后更新 producer 属性。 这可能很有用,例如,如果您必须在凭证更改后更新 SSL 密钥/信任存储位置。 这些更改不会影响现有的生产者实例;调用 以关闭任何现有 Producer,以便使用新属性创建新的 Producer。reset()spring-doc.cn

您不能将事务性生产者工厂更改为非事务性工厂,反之亦然。

现在提供了两种新方法:spring-doc.cn

void updateConfigs(Map<String, Object> updates);

void removeConfig(String configKey);

从版本 2.8 开始,如果你提供序列化器作为对象(在构造函数中或通过 setter),工厂将调用该方法以使用配置属性配置它们。configure()spring-doc.cn

ReplyingKafkaTemplate

版本 2.1.3 引入了一个子类 of 来提供请求/回复语义。 该类已命名,并且具有两个附加方法;方法签名如下:KafkaTemplateReplyingKafkaTemplatespring-doc.cn

RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record);

RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record,
    Duration replyTimeout);

结果是异步填充结果(或异常,用于超时)。 结果还有一个属性,它是调用 . 您可以使用此 future 来确定 send 操作的结果。CompletableFuturesendFutureKafkaTemplate.send()spring-doc.cn

如果使用第一种方法,或者参数为 ,则使用模板的属性(默认为 5 秒)。replyTimeoutnulldefaultReplyTimeoutspring-doc.cn

从版本 2.8.8 开始,模板具有新方法 。 如果 reply 容器配置为避免在初始化容器之前发送请求和发送回复,则这非常有用。waitForAssignmentauto.offset.reset=latestspring-doc.cn

当使用手动分区分配(无组管理)时,等待的持续时间必须大于容器的属性,因为只有在第一次轮询完成之后才会发送通知。pollTimeout

以下 Spring Boot 应用程序显示了如何使用该功能的示例:spring-doc.cn

@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 的自动配置的容器工厂来创建回复容器。spring-doc.cn

如果 non-tanvial deserializer 用于回复,请考虑使用 ErrorHandlingDeserializer 委托给配置的 deserializer。 配置后,将异常完成,您可以捕获 ,并在其属性中使用 。RequestReplyFutureExecutionExceptionDeserializationExceptioncausespring-doc.cn

从版本 2.6.7 开始,除了检测 s 之外,模板还将调用该函数(如果提供)。 如果它返回异常,则 future 将异常完成。DeserializationExceptionreplyErrorCheckerspring-doc.cn

下面是一个示例:spring-doc.cn

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) {
    ...
}

模板设置一个 header(默认命名),该 header 必须由服务器端回显。KafkaHeaders.CORRELATION_IDspring-doc.cn

在这种情况下,以下应用程序会响应:@KafkaListenerspring-doc.cn

@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;
    }

}

基础架构回显相关 ID 并确定回复主题。@KafkaListenerspring-doc.cn

有关发送回复的更多信息,请参阅使用 @SendTo 转发侦听器结果。 该模板使用默认标头来指示回复转到的主题。KafKaHeaders.REPLY_TOPICspring-doc.cn

从版本 2.2 开始,模板会尝试从配置的回复容器中检测回复主题或分区。 如果容器配置为侦听单个主题或单个 ,则用于设置回复标头。 如果容器配置为其他方式,则用户必须设置回复标头。 在这种情况下,将在初始化期间写入日志消息。 以下示例使用 :TopicPartitionOffsetINFOKafkaHeaders.REPLY_TOPICspring-doc.cn

record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, "kReplies".getBytes()));

使用单个回复进行配置时,只要每个实例侦听不同的分区,就可以对多个模板使用相同的回复主题。 使用单个回复主题进行配置时,每个实例都必须使用不同的 . 在这种情况下,所有实例都会收到每个回复,但只有发送请求的实例会找到相关 ID。 这对于自动扩展可能很有用,但会产生额外网络流量的开销,并且丢弃每个不需要的回复的成本很小。 使用此设置时,我们建议您将模板的 设置为 ,这样可以降低对 DEBUG 而不是默认 ERROR 的意外回复的日志记录级别。TopicPartitionOffsetgroup.idsharedReplyTopictruespring-doc.cn

以下是配置回复容器以使用相同的共享回复主题的示例:spring-doc.cn

@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;
}
如果您有多个客户端实例,并且未按照上一段所述配置它们,则每个实例都需要一个专用的回复主题。 另一种方法是为每个实例设置 并使用专用分区。 它包含一个四字节的 int (big-endian)。 服务器必须使用此标头将回复路由到正确的分区( 执行此操作)。 但是,在这种情况下,回复容器不能使用 Kafka 的组管理功能,并且必须配置为侦听固定分区(在其构造函数中使用 a)。KafkaHeaders.REPLY_PARTITIONHeader@KafkaListenerTopicPartitionOffsetContainerProperties
要求 Jackson 位于 Classpath 上(对于 )。 如果不可用,则消息转换器没有 Headers 映射器,因此您必须使用 ,如前所述。DefaultKafkaHeaderMapper@KafkaListenerMessagingMessageConverterSimpleKafkaHeaderMapper

默认情况下,使用 3 个标头:spring-doc.cn

  • KafkaHeaders.CORRELATION_ID- 用于将回复与请求相关联spring-doc.cn

  • KafkaHeaders.REPLY_TOPIC- 用于告诉服务器在哪里回复spring-doc.cn

  • KafkaHeaders.REPLY_PARTITION- (可选)用于告诉服务器要回复哪个分区spring-doc.cn

基础设施使用这些标头名称来路由回复。@KafkaListenerspring-doc.cn

从版本 2.3 开始,您可以自定义标头名称 - 模板具有 3 个属性、 、 和 。 如果您的服务器不是 Spring 应用程序(或不使用 ),这将非常有用。correlationHeaderNamereplyTopicHeaderNamereplyPartitionHeaderName@KafkaListenerspring-doc.cn

相反,如果请求应用程序不是 Spring 应用程序,并且将相关信息放在不同的 Headers 中,则从版本 3.0 开始,您可以在侦听器容器工厂上配置自定义,并且该 Headers 将被回显。 以前,侦听器必须回显自定义关联标头。correlationHeaderName

请求/回复Message<?>

版本 2.7 向 send 和 receive 的抽象添加了方法:ReplyingKafkaTemplatespring-messagingMessage<?>spring-doc.cn

RequestReplyMessageFuture<K, V> sendAndReceive(Message<?> message);

<P> RequestReplyTypedMessageFuture<K, V, P> sendAndReceive(Message<?> message,
        ParameterizedTypeReference<P> returnType);

这些将使用模板的 default ,还有一些重载版本可能会在方法调用中超时。replyTimeoutspring-doc.cn

如果使用者或模板可以通过回复消息中的配置或类型元数据转换有效负载,而无需任何其他信息,请使用第一种方法。DeserializerMessageConverterspring-doc.cn

如果需要为 return 类型提供类型信息,请使用第二种方法来帮助消息转换器。 这也允许同一个模板接收不同的类型,即使回复中没有类型元数据,例如当服务器端不是 Spring 应用程序时。 以下是后者的示例:spring-doc.cn

模板 Bean
@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
}
使用模板
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<?>

当 返回 ,且版本低于 2.5 时,必须填充回复主题和相关 ID 标头。 在此示例中,我们使用请求中的回复主题标头:@KafkaListenerMessage<?>spring-doc.cn

@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();
}

这还显示了如何在回复记录上设置键。spring-doc.cn

从版本 2.5 开始,框架将检测是否缺少这些 Headers 并使用主题填充它们 - 根据值确定的主题或传入的 Headers(如果存在)。 它还将回显传入的 和 ,如果存在。@SendToKafkaHeaders.REPLY_TOPICKafkaHeaders.CORRELATION_IDKafkaHeaders.REPLY_PARTITIONspring-doc.cn

@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();
}

回复中的原始记录键

从版本 3.3 开始,来自传入请求的 Kafka 记录密钥(如果存在)将保留在回复记录中。 这仅适用于单记录请求/回复方案。 当侦听器是 batch 或返回类型是集合时,由应用程序通过将回复记录包装在类型中来指定要使用的键。Messagespring-doc.cn

聚合多个回复

使用 ReplyingKafkaTemplate 中的模板严格适用于单个请求/回复场景。 如果一条消息的多个接收者返回回复,您可以使用 . 这是 Scatter-Gather Enterprise Integration Pattern 的客户端实现。AggregatingReplyingKafkaTemplatespring-doc.cn

与 一样,构造函数采用 producer 工厂和侦听器容器来接收回复;它有第三个参数,每次收到回复时都会查询该参数;当谓词返回时,s 的集合用于完成该方法返回的 s。ReplyingKafkaTemplateAggregatingReplyingKafkaTemplateBiPredicate<List<ConsumerRecord<K, R>>, Boolean> releaseStrategytrueConsumerRecordFuturesendAndReceivespring-doc.cn

还有一个附加属性 (default false)。 当此设置为 时,而不是使用 完成 future ,部分结果将正常完成 future(只要至少收到一条回复记录)。returnPartialOnTimeouttrueKafkaReplyTimeoutExceptionspring-doc.cn

从版本 2.3.5 开始,谓词也会在超时后调用(如果为 )。 第一个参数是当前记录列表;第二个原因是此调用是否由于超时。 谓词可以修改记录列表。returnPartialOnTimeouttruetruespring-doc.cn

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);

请注意,返回类型是 a,其值是 s 的集合。 “外部”不是“真实”记录,它由模板合成,作为为请求收到的实际回复记录的持有者。 当发生正常发布时(发布策略返回 true),主题设置为 ;如果为 true,并且发生超时(并且至少收到了一条回复记录),则主题将设置为 。 该模板为这些 “topic” 名称提供常量静态变量:ConsumerRecordConsumerRecordConsumerRecordaggregatedResultsreturnPartialOnTimeoutpartialResultsAfterTimeoutspring-doc.cn

/**
 * 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";

中的 real s 包含从中接收回复的实际主题。ConsumerRecordCollectionspring-doc.cn

回复的侦听器容器必须配置为 或 ;consumer 属性必须是(自 2.3 版以来的默认值)。 为避免任何丢失消息的可能性,模板仅在未完成请求为零时(即当 release 策略释放最后一个未完成的请求时)提交 offsets。 重新平衡后,可能会出现重复的回复投放;对于任何正在进行的请求,这些都将被忽略;当收到已发布回复的重复回复时,您可能会看到错误日志消息。AckMode.MANUALAckMode.MANUAL_IMMEDIATEenable.auto.commitfalse
如果你将 ErrorHandlingDeserializer 与此聚合模板一起使用,框架将不会自动检测 s。 相反,记录(带有值)将完整返回,但 Headers 中存在反序列化异常。 建议应用程序调用 utility 方法方法来确定是否发生了反序列化异常。 有关更多信息,请参阅其 JavaDocs。 此聚合模板也不要求 the;您应该对回复的每个元素执行检查。DeserializationExceptionnullReplyingKafkaTemplate.checkDeserialization()replyErrorChecker