From 1718c652a1a6061913879ab40da7b1a72cd732ea Mon Sep 17 00:00:00 2001 From: ZhangKai Date: Tue, 8 Mar 2022 19:42:00 +0800 Subject: [PATCH] =?UTF-8?q?#22=20spring=20for=20apache=20kafka=20=20?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E6=A0=BC=E5=BC=8F=E5=AE=A1=E6=A0=B8=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/config.js | 25 +- docs/spring-for-apache-kafka/README.md | 1 + docs/spring-for-apache-kafka/spring-kafka.md | 7176 ++++++++++++++++++ 3 files changed, 7195 insertions(+), 7 deletions(-) create mode 100644 docs/spring-for-apache-kafka/README.md create mode 100644 docs/spring-for-apache-kafka/spring-kafka.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 790b7bc..ee5c7a4 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -469,7 +469,7 @@ module.exports = { ], '/spring-cloud-data-flow/': [ { - title: 'Spring Cloud Data Flow', + title: 'Spring Cloud Data Flow 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -501,7 +501,7 @@ module.exports = { '/spring-for-graphql/': [ { - title: 'Spring For Graphql', + title: 'Spring For Graphql 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -512,7 +512,7 @@ module.exports = { ], '/spring-hateoas/': [ { - title: 'Spring HATEOAS', + title: 'Spring HATEOAS 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -523,7 +523,7 @@ module.exports = { ], '/spring-rest-docs/': [ { - title: 'Spring HATEOAS', + title: 'Spring HATEOAS 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -534,7 +534,7 @@ module.exports = { ], '/spring-amqp/': [ { - title: 'Spring AMQP', + title: 'Spring AMQP 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -545,7 +545,7 @@ module.exports = { ], '/spring-credhub/': [ { - title: 'Spring CredHub', + title: 'Spring CredHub 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -556,7 +556,7 @@ module.exports = { ], '/spring-flo/': [ { - title: 'Spring Flo', + title: 'Spring Flo 文档', sidebarDepth: 2, collapsable: false, children: [ @@ -565,6 +565,17 @@ module.exports = { initialOpenGroupIndex: 0 // 可选的, 默认值是 0 } ], + '/spring-for-apache-kafka/': [ + { + title: 'Spring for Apache Kafka 文档', + sidebarDepth: 2, + collapsable: false, + children: [ + "/spring-for-apache-kafka/spring-kafka.md", + ], + initialOpenGroupIndex: 0 // 可选的, 默认值是 0 + } + ], // fallback '/': [{ diff --git a/docs/spring-for-apache-kafka/README.md b/docs/spring-for-apache-kafka/README.md new file mode 100644 index 0000000..8d71d05 --- /dev/null +++ b/docs/spring-for-apache-kafka/README.md @@ -0,0 +1 @@ +# Spring for Apache Kafka \ No newline at end of file diff --git a/docs/spring-for-apache-kafka/spring-kafka.md b/docs/spring-for-apache-kafka/spring-kafka.md new file mode 100644 index 0000000..801a9db --- /dev/null +++ b/docs/spring-for-apache-kafka/spring-kafka.md @@ -0,0 +1,7176 @@ +# Spring for Apache Kafka + +## 1.前言 + +Spring for Apache Kafka 项目将核心 Spring 概念应用于基于 Kafka 的消息传递解决方案的开发。我们提供了一个“模板”作为发送消息的高级抽象。我们还为消息驱动的 POJO 提供支持。 + +## 2.最新更新? + +### 2.1. 自从2.7之后2.8中的更新 + +本部分介绍了从 2.7 版本到 2.8 版本所做的更改。有关早期版本中的更改,请参见[[更新历史]](#history)。 + +#### 2.1.1.Kafka 客户端版本 + +此版本需要 3.0.0`kafka-clients` + +| |在使用事务时,`kafka-clients`3.0.0 及以后的版本不再支持`EOSMode.V2`(AKA`BETA`)(并且自动回退到`V1`-AKA`ALPHA`)与 2.5 之前的代理;因此你必须用`EOSMode`覆盖默认的`V2`(`V2`)如果你的经纪人年龄较大(或升级你的经纪人)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关更多信息,请参见[一次语义学](#exactly-once)和[KIP-447](https://cwiki.apache.org/confluence/display/KAFKA/KIP-447%3A+Producer+scalability+for+exactly+once+semantics)。 + +#### 2.1.2.软件包更改 + +与类型映射相关的类和接口已从`…​support.converter`移动到`…​support.mapping`。 + +* `AbstractJavaTypeMapper` + +* `ClassMapper` + +* `DefaultJackson2JavaTypeMapper` + +* `Jackson2JavaTypeMapper` + +#### 2.1.3.失效的手动提交 + +现在可以将侦听器容器配置为接受顺序错误的手动偏移提交(通常是异步的)。容器将推迟提交,直到确认丢失的偏移量。有关更多信息,请参见[手动提交偏移](#ooo-commits)。 + +#### 2.1.4.`@KafkaListener`变化 + +现在可以在方法本身上指定侦听器方法是否为批处理侦听器。这允许对记录和批处理侦听器使用相同的容器工厂。 + +有关更多信息,请参见[批处理侦听器](#batch-listeners)。 + +批处理侦听器现在可以处理转换异常。 + +有关更多信息,请参见[使用批处理错误处理程序的转换错误](#batch-listener-conv-errors)。 + +`RecordFilterStrategy`在与批处理侦听器一起使用时,现在可以在一个调用中过滤整个批处理。有关更多信息,请参见[批处理侦听器](#batch-listeners)末尾的注释。 + +#### 2.1.5.`KafkaTemplate`变化 + +给定主题、分区和偏移量,你现在可以接收一条记录。有关更多信息,请参见[使用`KafkaTemplate`接收]。 + +#### 2.1.6.`CommonErrorHandler`已添加 + +遗留的`GenericErrorHandler`及其用于记录批处理侦听器的子接口层次结构已被新的单一接口`CommonErrorHandler`所取代,其实现方式与`GenericErrorHandler`的大多数遗留实现方式相对应。有关更多信息,请参见[容器错误处理程序](#error-handlers)。 + +#### 2.1.7.监听器容器更改 + +默认情况下,`interceptBeforeTx`容器属性现在是`true`。 + +`authorizationExceptionRetryInterval`属性已重命名为`authExceptionRetryInterval`,并且现在除了以前的`AuthorizationException`s 之外,还应用于`AuthenticationException`s。这两个异常都被认为是致命的,除非设置了此属性,否则默认情况下容器将停止。 + +有关更多信息,请参见[使用`KafkaMessageListenerContainer`]和[侦听器容器属性](#container-props)。 + +#### 2.1.8.序列化器/反序列化器更改 + +现在提供了`DelegatingByTopicSerializer`和`DelegatingByTopicDeserializer`。有关更多信息,请参见[委派序列化器和反序列化器](#delegating-serialization)。 + +#### 2.1.9.`DeadLetterPublishingRecover`变化 + +默认情况下,属性`stripPreviousExceptionHeaders`现在是`true`。 + +有关更多信息,请参见[管理死信记录头](#dlpr-headers)。 + +#### 2.1.10.可重排的主题更改 + +现在,你可以对可重试和不可重试的主题使用相同的工厂。有关更多信息,请参见[指定 ListenerContainerFactory](#retry-topic-lcf)。 + +现在,全球范围内出现了一系列可控的致命异常,这些异常将使失败的记录直接流向 DLT。请参阅[异常分类器](#retry-topic-ex-classifier)以了解如何管理它。 + +使用可重排主题功能时引发的 KafkabackoffException 现在将在调试级别记录。如果需要更改日志级别以返回警告或将其设置为任何其他级别,请参见[[change-kboe-logging-level]]。 + +## 3.导言 + +参考文档的第一部分是对 Spring Apache Kafka 和底层概念以及一些代码片段的高级概述,这些代码片段可以帮助你尽快启动和运行。 + +### 3.1.快速游览 + +先决条件:你必须安装并运行 Apache Kafka。然后,你必须将 Apache Kafka(`spring-kafka`)的 Spring JAR 及其所有依赖项放在你的类路径上。最简单的方法是在构建工具中声明一个依赖项。 + +如果不使用 Spring boot,请在项目中将`spring-kafka`jar 声明为依赖项。 + +Maven + +``` + + org.springframework.kafka + spring-kafka + 2.8.3 + +``` + +Gradle + +``` +compile 'org.springframework.kafka:spring-kafka:2.8.3' +``` + +| |在使用 Spring 引导时(你还没有使用 Start. Spring.io 来创建你的项目),省略版本,启动将自动带来与你的启动版本兼容的正确版本:| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Maven + +``` + + org.springframework.kafka + spring-kafka + +``` + +Gradle + +``` +compile 'org.springframework.kafka:spring-kafka' +``` + +然而,最快的入门方法是使用[start.spring.io](https://start.spring.io)(或 Spring Tool Suits 和 IntelliJ Idea 中的向导)并创建一个项目,选择’ Spring for Apache Kafka’作为依赖项。 + +#### 3.1.1.相容性 + +此快速浏览适用于以下版本: + +* Apache Kafka Clients3.0.0 + +* Spring Framework5.3.x + +* 最低 Java 版本:8 + +#### 3.1.2.开始 + +最简单的入门方法是使用[start.spring.io](https://start.spring.io)(或 Spring Tool Suits 和 IntelliJ Idea 中的向导)并创建一个项目,选择’ Spring for Apache Kafka’作为依赖项。请参阅[Spring Boot documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-kafka)以获取有关其对基础设施 bean 的自以为是的自动配置的更多信息。 + +这是一个最小的消费者应用程序。 + +##### Spring 引导消费者应用程序 + +例 1.应用程序 + +Java + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public NewTopic topic() { + return TopicBuilder.name("topic1") + .partitions(10) + .replicas(1) + .build(); + } + + @KafkaListener(id = "myId", topics = "topic1") + public void listen(String in) { + System.out.println(in); + } + +} +``` + +Kotlin + +``` +@SpringBootApplication +class Application { + + @Bean + fun topic() = NewTopic("topic1", 10, 1) + + @KafkaListener(id = "myId", topics = ["topic1"]) + fun listen(value: String?) { + println(value) + } + +} + +fun main(args: Array) = runApplication(*args) +``` + +示例 2.application.properties + +``` +spring.kafka.consumer.auto-offset-reset=earliest +``` + +`NewTopic` Bean 导致在代理上创建主题;如果主题已经存在,则不需要该主题。 + +##### Spring Boot Producer app + +例 3.应用程序 + +Java + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public NewTopic topic() { + return TopicBuilder.name("topic1") + .partitions(10) + .replicas(1) + .build(); + } + + @Bean + public ApplicationRunner runner(KafkaTemplate template) { + return args -> { + template.send("topic1", "test"); + }; + } + +} +``` + +Kotlin + +``` +@SpringBootApplication +class Application { + + @Bean + fun topic() = NewTopic("topic1", 10, 1) + + @Bean + fun runner(template: KafkaTemplate) = + ApplicationRunner { template.send("topic1", "test") } + + companion object { + @JvmStatic + fun main(args: Array) = runApplication(*args) + } + +} +``` + +##### 带 Java 配置(no Spring boot) + +| |Spring 对于 Apache Kafka 是设计用于在 Spring 应用程序上下文中使用的。
例如,如果你自己在 Spring 上下文之外创建侦听器容器,则并非所有函数都将工作,除非你满足容器实现的所有`…​Aware`接口。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面是一个不使用 Spring 引导的应用程序的示例;它同时具有`Consumer`和`Producer`。 + +例 4.没有引导 + +Java + +``` +public class Sender { + + public static void main(String[] args) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + context.getBean(Sender.class).send("test", 42); + } + + private final KafkaTemplate template; + + public Sender(KafkaTemplate template) { + this.template = template; + } + + public void send(String toSend, int key) { + this.template.send("topic1", key, toSend); + } + +} + +public class Listener { + + @KafkaListener(id = "listen1", topics = "topic1") + public void listen1(String in) { + System.out.println(in); + } + +} + +@Configuration +@EnableKafka +public class Config { + + @Bean + ConcurrentKafkaListenerContainerFactory + kafkaListenerContainerFactory(ConsumerFactory consumerFactory) { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory); + return factory; + } + + @Bean + public ConsumerFactory consumerFactory() { + return new DefaultKafkaConsumerFactory<>(consumerProps()); + } + + private Map consumerProps() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "group"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + // ... + return props; + } + + @Bean + public Sender sender(KafkaTemplate template) { + return new Sender(template); + } + + @Bean + public Listener listener() { + return new Listener(); + } + + @Bean + public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(senderProps()); + } + + private Map senderProps() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ProducerConfig.LINGER_MS_CONFIG, 10); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + //... + return props; + } + + @Bean + public KafkaTemplate kafkaTemplate(ProducerFactory producerFactory) { + return new KafkaTemplate(producerFactory); + } + +} +``` + +Kotlin + +``` +class Sender(private val template: KafkaTemplate) { + + fun send(toSend: String, key: Int) { + template.send("topic1", key, toSend) + } + +} + +class Listener { + + @KafkaListener(id = "listen1", topics = ["topic1"]) + fun listen1(`in`: String) { + println(`in`) + } + +} + +@Configuration +@EnableKafka +class Config { + + @Bean + fun kafkaListenerContainerFactory(consumerFactory: ConsumerFactory) = + ConcurrentKafkaListenerContainerFactory().also { it.consumerFactory = consumerFactory } + + @Bean + fun consumerFactory() = DefaultKafkaConsumerFactory(consumerProps) + + val consumerProps = mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092", + ConsumerConfig.GROUP_ID_CONFIG to "group", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to IntegerDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest" + ) + + @Bean + fun sender(template: KafkaTemplate) = Sender(template) + + @Bean + fun listener() = Listener() + + @Bean + fun producerFactory() = DefaultKafkaProducerFactory(senderProps) + + val senderProps = mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092", + ProducerConfig.LINGER_MS_CONFIG to 10, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to IntegerSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java + ) + + @Bean + fun kafkaTemplate(producerFactory: ProducerFactory) = KafkaTemplate(producerFactory) + +} +``` + +正如你所看到的,在不使用 Spring boot 时,你必须定义几个基础设施 bean。 + +## 4.参考文献 + +参考文档的这一部分详细介绍了构成 Spring Apache Kafka 的各种组件。[主要章节](#kafka)涵盖了用 Spring 开发 Kafka 应用程序的核心类。 + +### 4.1.用 Spring 表示 Apache Kafka + +这一部分提供了对使用 Spring 表示 Apache Kafka 的各种关注的详细解释。欲了解一个简短但不太详细的介绍,请参见[Quick Tour](#quick-tour)。 + +#### 4.1.1.连接到 Kafka + +* `KafkaAdmin`-见[配置主题](#configuring-topics) + +* `ProducerFactory`-见[发送消息](#sending-messages) + +* `ConsumerFactory`-见[接收消息](#receiving-messages) + +从版本 2.5 开始,每个扩展`KafkaResourceFactory`。这允许在运行时通过将`Supplier`添加到它们的配置中来更改引导程序服务器:`setBootstrapServersSupplier(() → …​)`。将对所有新连接调用该命令,以获取服务器列表。消费者和生产者通常都是长寿的。要关闭现有的生产者,请在`DefaultKafkaProducerFactory`上调用`reset()`。要关闭现有的消费者,在`stop()`(然后`start()`)上调用`KafkaListenerEndpointRegistry`和/或`stop()`,并在任何其他侦听器容器 bean 上调用`start()`。 + +为了方便起见,该框架还提供了一个`ABSwitchCluster`,它支持两组引导程序服务器;其中一组在任何时候都是活动的。通过调用`setBootstrapServersSupplier()`,配置`ABSwitchCluster`并将其添加到生产者和消费者工厂,以及`KafkaAdmin`。当你想要切换时,在生产者工厂上调用`primary()`或`secondary()`并调用`reset()`以建立新的连接;对于消费者,`stop()`和`start()`所有侦听器容器。当使用`@KafkaListener`s,`stop()`和`start()`时,`KafkaListenerEndpointRegistry` Bean。 + +有关更多信息,请参见 Javadocs。 + +##### 工厂听众 + +从版本 2.5 开始,`DefaultKafkaProducerFactory`和`DefaultKafkaConsumerFactory`可以配置为`Listener`,以便在创建或关闭生产者或消费者时接收通知。 + +生产者工厂监听器 + +``` +interface Listener { + + default void producerAdded(String id, Producer producer) { + } + + default void producerRemoved(String id, Producer producer) { + } + +} +``` + +消费者工厂监听器 + +``` +interface Listener { + + default void consumerAdded(String id, Consumer consumer) { + } + + default void consumerRemoved(String id, Consumer consumer) { + } + +} +``` + +在每种情况下,`id`都是通过将`client-id`属性(创建后从`metrics()`获得)附加到工厂`beanName`属性中来创建的,并由`.`分隔。 + +例如,这些侦听器可用于在创建新客户机时创建和绑定 Micrometer`KafkaClientMetrics`实例(并在客户机关闭时关闭它)。 + +该框架提供了可以做到这一点的侦听器;参见[千分尺本机度量](#micrometer-native)。 + +#### 4.1.2.配置主题 + +如果你在应用程序上下文中定义了`KafkaAdmin` Bean,那么它可以自动向代理添加主题。为此,你可以将每个主题的`NewTopic``@Bean`添加到应用程序上下文中。版本 2.3 引入了一个新的类`TopicBuilder`,以使创建这样的 bean 更加方便。下面的示例展示了如何做到这一点: + +Java + +``` +@Bean +public KafkaAdmin admin() { + Map configs = new HashMap<>(); + configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + return new KafkaAdmin(configs); +} + +@Bean +public NewTopic topic1() { + return TopicBuilder.name("thing1") + .partitions(10) + .replicas(3) + .compact() + .build(); +} + +@Bean +public NewTopic topic2() { + return TopicBuilder.name("thing2") + .partitions(10) + .replicas(3) + .config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd") + .build(); +} + +@Bean +public NewTopic topic3() { + return TopicBuilder.name("thing3") + .assignReplicas(0, Arrays.asList(0, 1)) + .assignReplicas(1, Arrays.asList(1, 2)) + .assignReplicas(2, Arrays.asList(2, 0)) + .config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd") + .build(); +} +``` + +Kotlin + +``` +@Bean +fun admin() = KafkaAdmin(mapOf(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092")) + +@Bean +fun topic1() = + TopicBuilder.name("thing1") + .partitions(10) + .replicas(3) + .compact() + .build() + +@Bean +fun topic2() = + TopicBuilder.name("thing2") + .partitions(10) + .replicas(3) + .config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd") + .build() + +@Bean +fun topic3() = + TopicBuilder.name("thing3") + .assignReplicas(0, Arrays.asList(0, 1)) + .assignReplicas(1, Arrays.asList(1, 2)) + .assignReplicas(2, Arrays.asList(2, 0)) + .config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd") + .build() +``` + +从版本 2.6 开始,你可以省略`.partitions()`和/或`replicas()`,并且代理默认值将应用于这些属性。代理版本必须至少是 2.4.0 才能支持此功能-参见[KIP-464](https://cwiki.apache.org/confluence/display/KAFKA/KIP-464%3A+Defaults+for+AdminClient%23createTopic)。 + +Java + +``` +@Bean +public NewTopic topic4() { + return TopicBuilder.name("defaultBoth") + .build(); +} + +@Bean +public NewTopic topic5() { + return TopicBuilder.name("defaultPart") + .replicas(1) + .build(); +} + +@Bean +public NewTopic topic6() { + return TopicBuilder.name("defaultRepl") + .partitions(3) + .build(); +} +``` + +Kotlin + +``` +@Bean +fun topic4() = TopicBuilder.name("defaultBoth").build() + +@Bean +fun topic5() = TopicBuilder.name("defaultPart").replicas(1).build() + +@Bean +fun topic6() = TopicBuilder.name("defaultRepl").partitions(3).build() +``` + +从版本 2.7 开始,你可以在单个`KafkaAdmin.NewTopics` Bean 定义中声明多个`NewTopic`s: + +Java + +``` +@Bean +public KafkaAdmin.NewTopics topics456() { + return new NewTopics( + TopicBuilder.name("defaultBoth") + .build(), + TopicBuilder.name("defaultPart") + .replicas(1) + .build(), + TopicBuilder.name("defaultRepl") + .partitions(3) + .build()); +} +``` + +Kotlin + +``` +@Bean +fun topics456() = KafkaAdmin.NewTopics( + TopicBuilder.name("defaultBoth") + .build(), + TopicBuilder.name("defaultPart") + .replicas(1) + .build(), + TopicBuilder.name("defaultRepl") + .partitions(3) + .build() +) +``` + +| |当使用 Spring 引导时,`KafkaAdmin` Bean 是自动注册的,因此你只需要`NewTopic`(和/或`NewTopics`)`@Bean`s。| +|---|---------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下,如果代理不可用,将记录一条消息,但将继续加载上下文。你可以通过编程方式调用管理员的`initialize()`方法稍后再试。如果你希望此条件被认为是致命的,请将管理员的`fatalIfBrokerNotAvailable`属性设置为`true`。然后,上下文将无法初始化。 + +| |如果代理支持它(1.0.0 或更高),则如果发现现有主题的分区少于`NewTopic.numPartitions`,则管理员将增加分区的数量。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.7 开始,`KafkaAdmin`提供了在运行时创建和检查主题的方法。 + +* `createOrModifyTopics` + +* `describeTopics` + +对于更高级的功能,你可以直接使用`AdminClient`。下面的示例展示了如何做到这一点: + +``` +@Autowired +private KafkaAdmin admin; + +... + + AdminClient client = AdminClient.create(admin.getConfigurationProperties()); + ... + client.close(); +``` + +#### 4.1.3.发送消息 + +本节介绍如何发送消息。 + +##### 使用`KafkaTemplate` + +本节介绍如何使用`KafkaTemplate`发送消息。 + +###### 概述 + +`KafkaTemplate`封装了一个生成器,并提供了将数据发送到 Kafka 主题的方便方法。下面的清单显示了`KafkaTemplate`中的相关方法: + +``` +ListenableFuture> sendDefault(V data); + +ListenableFuture> sendDefault(K key, V data); + +ListenableFuture> sendDefault(Integer partition, K key, V data); + +ListenableFuture> sendDefault(Integer partition, Long timestamp, K key, V data); + +ListenableFuture> send(String topic, V data); + +ListenableFuture> send(String topic, K key, V data); + +ListenableFuture> send(String topic, Integer partition, K key, V data); + +ListenableFuture> send(String topic, Integer partition, Long timestamp, K key, V data); + +ListenableFuture> send(ProducerRecord record); + +ListenableFuture> send(Message message); + +Map metrics(); + +List partitionsFor(String topic); + + T execute(ProducerCallback callback); + +// Flush the producer. + +void flush(); + +interface ProducerCallback { + + T doInKafka(Producer producer); + +} +``` + +有关更多详细信息,请参见[Javadoc](https://docs.spring.io/spring-kafka/api/org/springframework/kafka/core/KafkaTemplate.html)。 + +`sendDefault`API 要求为模板提供了一个默认的主题。 + +API 将`timestamp`作为参数,并将此时间戳存储在记录中。如何存储用户提供的时间戳取决于在 Kafka 主题上配置的时间戳类型。如果主题被配置为使用`CREATE_TIME`,则记录用户指定的时间戳(如果未指定,则生成时间戳)。如果将主题配置为使用`LOG_APPEND_TIME`,则忽略用户指定的时间戳,而代理添加本地代理时间。 + +`metrics`和`partitionsFor`方法委托给底层[`Producer`](https://kafka. Apache.org/20/javadoc/org/ Apache/kafka/clients/producer/producer.html)上相同的方法。`execute`方法提供了对底层[`Producer`](https://kafka. Apache.org/20/javadoc/org/ Apache/kafka/clients/producer/producer.html)的直接访问。 + +要使用模板,你可以配置一个生产者工厂,并在模板的构造函数中提供它。下面的示例展示了如何做到这一点: + +``` +@Bean +public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigs()); +} + +@Bean +public Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.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 kafkaTemplate() { + return new KafkaTemplate(producerFactory()); +} +``` + +从版本 2.5 开始,你现在可以覆盖工厂的`ProducerConfig`属性,以创建具有来自同一工厂的不同生产者配置的模板。 + +``` +@Bean +public KafkaTemplate stringTemplate(ProducerFactory pf) { + return new KafkaTemplate<>(pf); +} + +@Bean +public KafkaTemplate bytesTemplate(ProducerFactory pf) { + return new KafkaTemplate<>(pf, + Collections.singletonMap(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class)); +} +``` + +注意,类型`ProducerFactory`的 Bean(例如由 Spring 引导自动配置的类型)可以用不同的窄泛型类型引用。 + +你还可以使用标准的``定义来配置模板。 + +然后,要使用模板,你可以调用它的一个方法。 + +当使用带有`Message`参数的方法时,主题、分区和键信息将在包含以下项的消息头中提供: + +* `KafkaHeaders.TOPIC` + +* `KafkaHeaders.PARTITION_ID` + +* `KafkaHeaders.MESSAGE_KEY` + +* `KafkaHeaders.TIMESTAMP` + +消息有效载荷就是数据。 + +可选地,你可以将`KafkaTemplate`配置为`ProducerListener`,以获得带有发送结果(成功或失败)的异步回调,而不是等待`Future`完成。下面的清单显示了`ProducerListener`接口的定义: + +``` +public interface ProducerListener { + + void onSuccess(ProducerRecord producerRecord, RecordMetadata recordMetadata); + + void onError(ProducerRecord producerRecord, RecordMetadata recordMetadata, + Exception exception); + +} +``` + +默认情况下,模板配置为`LoggingProducerListener`,它会记录错误,并且在发送成功时不会执行任何操作。 + +为了方便起见,在你只想实现其中一个方法的情况下,提供了默认的方法实现。 + +注意,send 方法返回`ListenableFuture`。你可以向侦听器注册回调,以异步地接收发送的结果。下面的示例展示了如何做到这一点: + +``` +ListenableFuture> future = template.send("myTopic", "something"); +future.addCallback(new ListenableFutureCallback>() { + + @Override + public void onSuccess(SendResult result) { + ... + } + + @Override + public void onFailure(Throwable ex) { + ... + } + +}); +``` + +`SendResult`有两个性质,a`ProducerRecord`和`RecordMetadata`。有关这些对象的信息,请参见 Kafka API 文档。 + +`Throwable`中的`onFailure`可以强制转换为`KafkaProducerException`;其`failedProducerRecord`属性包含失败的记录。 + +从版本 2.5 开始,你可以使用`KafkaSendCallback`而不是`ListenableFutureCallback`,从而更容易地提取失败的`ProducerRecord`,从而避免了强制转换`Throwable`的需要: + +``` +ListenableFuture> future = template.send("topic", 1, "thing"); +future.addCallback(new KafkaSendCallback() { + + @Override + public void onSuccess(SendResult result) { + ... + } + + @Override + public void onFailure(KafkaProducerException ex) { + ProducerRecord failed = ex.getFailedProducerRecord(); + ... + } + +}); +``` + +你也可以使用一对 lambdas: + +``` +ListenableFuture> future = template.send("topic", 1, "thing"); +future.addCallback(result -> { + ... + }, (KafkaFailureCallback) ex -> { + ProducerRecord failed = ex.getFailedProducerRecord(); + ... + }); +``` + +如果你希望阻止发送线程以等待结果,则可以调用 Future 的`get()`方法;建议使用带有超时的方法。你可能希望在等待之前调用`flush()`,或者,为了方便起见,模板具有一个带有`autoFlush`参数的构造函数,该构造函数将在每次发送时使模板`flush()`。只有当你设置了`linger.ms`producer 属性并希望立即发送部分批处理时,才需要刷新。 + +###### 示例 + +本节展示了向 Kafka 发送消息的示例: + +例 5.非阻塞(异步) + +``` +public void sendToKafka(final MyOutputData data) { + final ProducerRecord record = createRecord(data); + + ListenableFuture> future = template.send(record); + future.addCallback(new KafkaSendCallback() { + + @Override + public void onSuccess(SendResult result) { + handleSuccess(data); + } + + @Override + public void onFailure(KafkaProducerException ex) { + handleFailure(data, record, ex); + } + + }); +} +``` + +阻塞(同步) + +``` +public void sendToKafka(final MyOutputData data) { + final ProducerRecord 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`具有`failedProducerRecord`属性。 + +##### 使用`RoutingKafkaTemplate` + +从版本 2.5 开始,你可以使用`RoutingKafkaTemplate`在运行时基于目标`topic`名称选择生产者。 + +| |路由模板执行**不是**支持事务、`execute`、`flush`或`metrics`操作,因为这些操作的主题是未知的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------| + +该模板需要一个`java.util.regex.Pattern`到`ProducerFactory`实例的映射。这个映射应该是有序的(例如,a`LinkedHashMap`),因为它是按顺序遍历的;你应该在开始时添加更具体的模式。 + +Spring 以下简单的引导应用程序提供了一个示例,说明如何使用相同的模板发送到不同的主题,每个主题使用不同的值序列化器。 + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context, + ProducerFactory pf) { + + // Clone the PF with a different Serializer, register with Spring for shutdown + Map configs = new HashMap<>(pf.getConfigurationProperties()); + configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + DefaultKafkaProducerFactory bytesPF = new DefaultKafkaProducerFactory<>(configs); + context.registerBean(DefaultKafkaProducerFactory.class, "bytesPF", bytesPF); + + Map> 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`s 如[注释属性](#annotation-properties)所示。 + +对于另一种实现类似结果的技术,但具有向相同主题发送不同类型的附加功能,请参见[委派序列化器和反序列化器](#delegating-serialization)。 + +##### 使用`DefaultKafkaProducerFactory` + +如[使用`KafkaTemplate`](#kafka-template)中所示,使用`ProducerFactory`创建生产者。 + +当不使用[交易](#transactions)时,默认情况下,`DefaultKafkaProducerFactory`将创建一个由所有客户机使用的单例生成器,如`KafkaProducer`Javadocs 中所建议的那样。但是,如果在模板上调用`flush()`,这可能会导致使用相同生成器的其他线程的延迟。从版本 2.3 开始,`DefaultKafkaProducerFactory`有一个新的属性`producerPerThread`。当设置为`true`时,工厂将为每个线程创建(并缓存)一个单独的生产者,以避免此问题。 + +| |当`producerPerThread`是`true`时,用户代码**必须**在出厂时调用`closeThreadBoundProducer()`在出厂时不再需要生产者。
这将在物理上关闭生产者,并将其从`ThreadLocal`中删除。
调用`reset()`或`destroy()`不会清理这些生产者。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另请参见[`KafkaTemplate`事务性和非事务性发布]。 + +当创建`DefaultKafkaProducerFactory`时,可以通过调用只接收属性映射的构造函数(参见[using`KafkaTemplate`](#kafka-template)中的示例),从配置中获取键和/或值`Serializer`类,或者`Serializer`实例可以被传递到`DefaultKafkaProducerFactory`构造函数(在这种情况下,所有`Producer`的实例共享相同的实例)。或者,你可以提供`Supplier`s(从版本 2.3 开始),它将用于为每个`Producer`获取单独的`Serializer`实例: + +``` +@Bean +public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigs(), null, () -> new CustomValueSerializer()); +} + +@Bean +public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate(producerFactory()); +} +``` + +从版本 2.5.10 开始,你现在可以在工厂创建后更新生产者属性。这可能是有用的,例如,如果你必须在凭据更改后更新 SSL 密钥/信任存储位置。这些更改将不会影响现有的生产者实例;调用`reset()`来关闭任何现有的生产者,以便使用新的属性创建新的生产者。注意:不能将事务性生产工厂更改为非事务性生产工厂,反之亦然。 + +现在提供了两种新的方法: + +``` +void updateConfigs(Map updates); + +void removeConfig(String configKey); +``` + +从版本 2.8 开始,如果你将序列化器作为对象(在构造函数中或通过 setter)提供,则工厂将调用`configure()`方法来使用配置属性对它们进行配置。 + +##### 使用`ReplyingKafkaTemplate` + +版本 2.1.3 引入了`KafkaTemplate`的子类来提供请求/回复语义。该类名为`ReplyingKafkaTemplate`,并具有两个附加方法;以下显示了方法签名: + +``` +RequestReplyFuture sendAndReceive(ProducerRecord record); + +RequestReplyFuture sendAndReceive(ProducerRecord record, + Duration replyTimeout); +``` + +(另请参见[request/reply with`Message`s](#exchange-messages))。 + +结果是一个`ListenableFuture`,该结果是异步填充的(或者是一个异常,用于超时)。结果还具有`sendFuture`属性,这是调用`KafkaTemplate.send()`的结果。你可以使用这个 future 来确定发送操作的结果。 + +如果使用第一个方法,或者`replyTimeout`参数是`null`,则使用模板的`defaultReplyTimeout`属性(默认情况下为 5 秒)。 + +Spring 以下引导应用程序显示了如何使用该功能的示例: + +``` +@SpringBootApplication +public class KRequestingApplication { + + public static void main(String[] args) { + SpringApplication.run(KRequestingApplication.class, args).close(); + } + + @Bean + public ApplicationRunner runner(ReplyingKafkaTemplate template) { + return args -> { + ProducerRecord record = new ProducerRecord<>("kRequests", "foo"); + RequestReplyFuture replyFuture = template.sendAndReceive(record); + SendResult sendResult = replyFuture.getSendFuture().get(10, TimeUnit.SECONDS); + System.out.println("Sent ok: " + sendResult.getRecordMetadata()); + ConsumerRecord consumerRecord = replyFuture.get(10, TimeUnit.SECONDS); + System.out.println("Return value: " + consumerRecord.value()); + }; + } + + @Bean + public ReplyingKafkaTemplate replyingTemplate( + ProducerFactory pf, + ConcurrentMessageListenerContainer repliesContainer) { + + return new ReplyingKafkaTemplate<>(pf, repliesContainer); + } + + @Bean + public ConcurrentMessageListenerContainer repliesContainer( + ConcurrentKafkaListenerContainerFactory containerFactory) { + + ConcurrentMessageListenerContainer 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`](#error-handling-deSerializer)将其委托给你配置的反序列化器。当这样配置时,`RequestReplyFuture`将在特殊情况下完成,并且你可以捕获`ExecutionException`,而`DeserializationException`在其`cause`属性中。 + +从版本 2.6.7 开始,除了检测`DeserializationException`s 之外,如果提供的话,模板将调用`replyErrorChecker`函数。如果它返回一个异常,则将来将异常完成。 + +下面是一个例子: + +``` +template.setReplyErrorChecker(record -> { + Header error = record.headers().lastHeader("serverSentAnError"); + if (error != null) { + return new MyException(new String(error.value())); + } + else { + return null; + } +}); + +... + +RequestReplyFuture future = template.sendAndReceive(record); +try { + future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok + ConsumerRecord 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`,这将减少对调试的意外回复的日志级别,而不是默认错误。 + +下面是一个配置应答容器以使用相同的共享应答主题的示例: + +``` +@Bean +public ConcurrentMessageListenerContainer replyContainer( + ConcurrentKafkaListenerContainerFactory containerFactory) { + + ConcurrentMessageListenerContainer 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 位于 Classpath 上(对于`@KafkaListener`)。
如果它不可用,消息转换器没有头映射器,因此你必须配置一个`MessagingMessageConverter`和一个`SimpleKafkaHeaderMapper`,如前面所示。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下,使用 3 个标题: + +* `KafkaHeaders.CORRELATION_ID`-用于将回复与请求关联起来 + +* `KafkaHeaders.REPLY_TOPIC`-用于告诉服务器在哪里回复 + +* `KafkaHeaders.REPLY_PARTITION`-(可选)用于告诉服务器要回复哪个分区 + +`@KafkaListener`基础架构使用这些头名称来路由答复。 + +从版本 2.3 开始,你可以自定义标题名称-模板有 3 个属性`correlationHeaderName`、`replyTopicHeaderName`和`replyPartitionHeaderName`。如果你的服务器不是 Spring 应用程序(或者不使用`@KafkaListener`),这是有用的。 + +###### 请求/回复`Message`s + +版本 2.7 在`ReplyingKafkaTemplate`中添加了发送和接收`spring-messaging`的`Message`抽象的方法: + +``` +RequestReplyMessageFuture sendAndReceive(Message message); + +

RequestReplyTypedMessageFuture sendAndReceive(Message message, + ParameterizedTypeReference

returnType); +``` + +这些将使用模板的默认`replyTimeout`,也有重载版本可以在方法调用中占用超时时间。 + +如果使用者的`Deserializer`或模板的`MessageConverter`可以通过配置或在回复消息中键入元数据来转换有效负载,而不需要任何其他信息,请使用第一种方法。 + +如果需要为返回类型提供类型信息,请使用第二种方法来帮助消息转换器。这还允许相同的模板接收不同的类型,即使在答复中没有类型元数据,例如当服务器端不是 Spring 应用程序时也是如此。以下是后者的一个例子: + +例 6.模板 Bean + +Java + +``` +@Bean +ReplyingKafkaTemplate template( + ProducerFactory pf, + ConcurrentKafkaListenerContainerFactory factory) { + + ConcurrentMessageListenerContainer replyContainer = + factory.createContainer("replies"); + replyContainer.getContainerProperties().setGroupId("request.replies"); + ReplyingKafkaTemplate template = + new ReplyingKafkaTemplate<>(pf, replyContainer); + template.setMessageConverter(new ByteArrayJsonMessageConverter()); + template.setDefaultTopic("requests"); + return template; +} +``` + +Kotlin + +``` +@Bean +fun template( + pf: ProducerFactory?, + factory: ConcurrentKafkaListenerContainerFactory +): ReplyingKafkaTemplate { + val replyContainer = factory.createContainer("replies") + replyContainer.containerProperties.groupId = "request.replies" + val template = ReplyingKafkaTemplate(pf, replyContainer) + template.messageConverter = ByteArrayJsonMessageConverter() + template.defaultTopic = "requests" + return template +} +``` + +例 7.使用模板 + +Java + +``` +RequestReplyTypedMessageFuture future1 = + template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(), + new ParameterizedTypeReference() { }); +log.info(future1.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString()); +Thing thing = future1.get(10, TimeUnit.SECONDS).getPayload(); +log.info(thing.toString()); + +RequestReplyTypedMessageFuture> future2 = + template.sendAndReceive(MessageBuilder.withPayload("getThings").build(), + new ParameterizedTypeReference>() { }); +log.info(future2.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString()); +List things = future2.get(10, TimeUnit.SECONDS).getPayload(); +things.forEach(thing1 -> log.info(thing1.toString())); +``` + +Kotlin + +``` +val future1: RequestReplyTypedMessageFuture? = + template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(), + object : ParameterizedTypeReference() {}) +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?>? = + template.sendAndReceive(MessageBuilder.withPayload("getThings").build(), + object : ParameterizedTypeReference?>() {}) +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()) }) +``` + +##### 回复类型消息 \ + +当`@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.MESSAGE_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.MESSAGE_KEY, 42) + .build(); +} +``` + +##### 聚合多个回复 + +[使用`ReplyingKafkaTemplate`](#replying-template)中的模板严格用于单个请求/回复场景。对于单个消息的多个接收者返回答复的情况,可以使用`AggregatingReplyingKafkaTemplate`。这是[散-集 Enterprise 集成模式](https://www.enterpriseintegrationpatterns.com/patterns/messaging/BroadcastAggregate.html)客户端的一个实现。 + +与`ReplyingKafkaTemplate`类似,`AggregatingReplyingKafkaTemplate`构造函数需要一个生产者工厂和一个侦听器容器来接收回复;它有第三个参数`BiPredicate>, Boolean> releaseStrategy`,在每次接收到回复时都会查询这个参数;当谓词返回`true`时,`ConsumerRecord`s 的集合用于完成由`sendAndReceive`方法返回的`Future`。 + +还有一个额外的属性`returnPartialOnTimeout`(默认为 false)。当这被设置为`true`时,而不是用`KafkaReplyTimeoutException`来完成 future,部分结果通常会完成 future(只要至少收到了一条回复记录)。 + +从版本 2.3.5 开始,在超时之后也调用谓词(如果`returnPartialOnTimeout`是`true`)。第一个参数是当前的记录列表;第二个参数是`true`,如果这个调用是由于超时引起的。谓词可以修改记录列表。 + +``` +AggregatingReplyingKafkaTemplate template = + new AggregatingReplyingKafkaTemplate<>(producerFactory, container, + coll -> coll.size() == releaseSize); +... +RequestReplyFuture>> future = + template.sendAndReceive(record); +future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok +ConsumerRecord>> consumerRecord = + future.get(30, TimeUnit.SECONDS); +``` + +请注意,返回类型是`ConsumerRecord`,其值是`ConsumerRecord`s 的集合。该“外”`ConsumerRecord`不是一个“真实”的记录,它是由模板合成的,作为实际接收到的回复记录的持有者用于请求。当正常的发布发生时(Release Strategy 返回 true),主题设置为`aggregatedResults`;如果`returnPartialOnTimeout`为真,并且发生超时(并且至少收到了一条回复记录),主题设置为`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`](#error-handling-deserializer)与此聚合模板,框架将不会自动检测`DeserializationException`s.
相反,记录(带有`null`值)将原封不动地返回,使用头文件中的反序列化异常。
建议应用程序调用实用程序方法`ReplyingKafkaTemplate.checkDeserialization()`方法来确定如果发生反序列化异常。
有关更多信息,请参见其 Javadocs。
此聚合模板也不会调用`replyErrorChecker`;你应该对回复的每个元素执行检查。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.1.4.接收消息 + +可以通过配置`MessageListenerContainer`并提供消息侦听器或使用`@KafkaListener`注释来接收消息。 + +##### 消息侦听器 + +当使用[消息侦听器容器](#message-listener-container)时,必须提供一个侦听器来接收数据。目前,消息侦听器有八个受支持的接口。下面的清单展示了这些接口: + +``` +public interface MessageListener { (1) + + void onMessage(ConsumerRecord data); + +} + +public interface AcknowledgingMessageListener { (2) + + void onMessage(ConsumerRecord data, Acknowledgment acknowledgment); + +} + +public interface ConsumerAwareMessageListener extends MessageListener { (3) + + void onMessage(ConsumerRecord data, Consumer consumer); + +} + +public interface AcknowledgingConsumerAwareMessageListener extends MessageListener { (4) + + void onMessage(ConsumerRecord data, Acknowledgment acknowledgment, Consumer consumer); + +} + +public interface BatchMessageListener { (5) + + void onMessage(List> data); + +} + +public interface BatchAcknowledgingMessageListener { (6) + + void onMessage(List> data, Acknowledgment acknowledgment); + +} + +public interface BatchConsumerAwareMessageListener extends BatchMessageListener { (7) + + void onMessage(List> data, Consumer consumer); + +} + +public interface BatchAcknowledgingConsumerAwareMessageListener extends BatchMessageListener { (8) + + void onMessage(List> data, Acknowledgment acknowledgment, Consumer consumer); + +} +``` + +|**1**|当使用自动提交或容器管理的[提交方法](#committing-offsets)操作时,使用此接口处理从 Kafka 使用者`poll()`接收的单个`ConsumerRecord`实例。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|在使用[提交方法](#committing-offsets)中的一种手动操作时,使用此接口处理从 Kafka 使用者`poll()`接收到的单个`ConsumerRecord`实例。| +|**3**|当使用自动提交或容器管理的[提交方法](#committing-offsets)中的一个操作时,使用此接口处理从 Kafka 使用者`ConsumerRecord`接收的单个`ConsumerRecord`实例。
提供了对`Consumer`对象的访问。| +|**4**|使用此接口处理从 Kafka 使用者`ConsumerRecord`接收到的单个`poll()`实例时使用的手动[提交方法](#committing-offsets)中的一个操作。
提供了对`Consumer`对象的访问。| +|**5**|当使用自动提交或容器管理的[提交方法](#committing-offsets)操作时,使用此接口处理从 Kafka 使用者`poll()`接收到的所有`ConsumerRecord`实例。当你使用此接口时,不支持`AckMode.RECORD`,因为给了侦听器完整的批处理。| +|**6**|使用此接口处理从 Kafka 使用者`ConsumerRecord`接收到的所有`poll()`实例时,使用其中一个手动[提交方法](#committing-offsets)操作。| +|**7**|在使用自动提交或容器管理的[提交方法](#committing-offsets)操作时,使用此接口处理从 Kafka 使用者`ConsumerRecord`接收的所有`poll()`实例,当你使用此接口时,不支持`AckMode.RECORD`,因为给了侦听器完整的批处理。
提供了对`Consumer`对象的访问。| +|**8**|使用此接口处理从 Kafka 使用者`ConsumerRecord`接收到的所有`poll()`实例,当使用其中一个手动[提交方法](#committing-offsets)操作时。
提供了对`Consumer`对象的访问。| + +| |`Consumer`对象不是线程安全的。
你必须仅在调用侦听器的线程上调用它的方法。| +|---|---------------------------------------------------------------------------------------------------------------------| + +| |你不应该执行任何`Consumer`方法,这些方法会影响用户在监听器中的位置和或提交偏移;容器需要管理这些信息。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 消息侦听器容器 + +提供了两个`MessageListenerContainer`实现: + +* `KafkaMessageListenerContainer` + +* `ConcurrentMessageListenerContainer` + +`KafkaMessageListenerContainer`接收来自单个线程上所有主题或分区的所有消息。`ConcurrentMessageListenerContainer`将委托给一个或多个`KafkaMessageListenerContainer`实例,以提供多线程消耗。 + +从版本 2.2.7 开始,你可以将`RecordInterceptor`添加到侦听器容器;在调用侦听器允许检查或修改记录之前,将调用它。如果拦截器返回 null,则不调用侦听器。从版本 2.7 开始,它有额外的方法,在侦听器退出后调用这些方法(通常是通过抛出异常)。此外,从版本 2.7 开始,现在有一个`BatchInterceptor`,为[批处理侦听器](#batch-listeners)提供类似的功能。此外,`ConsumerAwareRecordInterceptor`(和`BatchInterceptor`)提供对`Consumer`的访问。例如,这可以用来访问拦截器中的消费者指标。 + +| |你不应该在这些拦截器中执行任何影响使用者位置或提交偏移的方法;容器需要管理这些信息。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`CompositeRecordInterceptor`和`CompositeBatchInterceptor`可用于调用多个拦截器。 + +默认情况下,从版本 2.8 开始,当使用事务时,拦截器在事务启动之前被调用。你可以将侦听器容器的`interceptBeforeTx`属性设置为`false`,以便在事务启动后调用拦截器。 + +从版本 2.3.8、2.4.6 开始,当并发性大于 1 时,`ConcurrentMessageListenerContainer`现在支持[静态成员](https://kafka.apache.org/documentation/#static_membership)。`group.instance.id`后缀为`-n`,后缀为`n`,起始于`1`。这与增加的`session.timeout.ms`一起,可以用来减少重新平衡事件,例如,当应用程序实例重新启动时。 + +###### 使用`KafkaMessageListenerContainer` + +以下构造函数可用: + +``` +public KafkaMessageListenerContainer(ConsumerFactory consumerFactory, + ContainerProperties containerProperties) +``` + +它在`ContainerProperties`对象中接收`ConsumerFactory`和有关主题和分区以及其他配置的信息。`ContainerProperties`具有以下构造函数: + +``` +public ContainerProperties(TopicPartitionOffset... topicPartitions) + +public ContainerProperties(String... topics) + +public ContainerProperties(Pattern topicPattern) +``` + +第一个构造函数接受一个由`TopicPartitionOffset`参数组成的数组,以显式地指示容器使用哪些分区(使用 Consumer`assign()`方法),并使用一个可选的初始偏移量。在默认情况下,正值是绝对的偏移量。在默认情况下,负值是相对于分区中当前的最后一个偏移量的。为`TopicPartitionOffset`提供了一个构造函数,它接受一个额外的`boolean`参数。如果这是`true`,则初始偏移(正或负)相对于此消费者的当前位置。当容器启动时,将应用这些偏移量。第二个是一个主题数组,Kafka 基于`group.id`属性(在整个组中分发分区)分配分区。第三种使用 regex`Pattern`来选择主题。 + +要将`MessageListener`分配给容器,可以在创建容器时使用`ContainerProps.setMessageListener`方法。下面的示例展示了如何做到这一点: + +``` +ContainerProperties containerProps = new ContainerProperties("topic1", "topic2"); +containerProps.setMessageListener(new MessageListener() { + ... +}); +DefaultKafkaConsumerFactory cf = + new DefaultKafkaConsumerFactory<>(consumerProps()); +KafkaMessageListenerContainer container = + new KafkaMessageListenerContainer<>(cf, containerProps); +return container; +``` + +请注意,当创建`DefaultKafkaConsumerFactory`时,使用只接收上述属性的构造函数意味着从配置中提取键和值`Deserializer`类。或者,`Deserializer`实例可以传递给`DefaultKafkaConsumerFactory`构造函数,用于键和/或值,在这种情况下,所有消费者共享相同的实例。另一种选择是提供`Supplier`s(从版本 2.3 开始),用于为每个`Consumer`获取单独的`Deserializer`实例: + +``` +DefaultKafkaConsumerFactory cf = + new DefaultKafkaConsumerFactory<>(consumerProps(), null, () -> new CustomValueDeserializer()); +KafkaMessageListenerContainer container = + new KafkaMessageListenerContainer<>(cf, containerProps); +return container; +``` + +有关可以设置的各种属性的更多信息,请参见[Javadoc](https://docs.spring.io/spring-kafka/api/org/springframework/kafka/listener/ContainerProperties.html)for`ContainerProperties`。 + +自版本 2.1.1 以来,一个名为`logContainerConfig`的新属性可用。当启用`true`和`INFO`日志记录时,每个侦听器容器写一个日志消息,总结其配置属性。 + +默认情况下,主题偏移提交的日志记录是在`DEBUG`日志级别执行的。从版本 2.1.2 开始,`ContainerProperties`中的一个名为`commitLogLevel`的属性允许你为这些消息指定日志级别。例如,要将日志级别更改为`INFO`,可以使用`containerProperties.setCommitLogLevel(LogIfLevelEnabled.Level.INFO);`。 + +从版本 2.2 开始,添加了一个名为`missingTopicsFatal`的新容器属性(默认值:`false`自 2.3.4 起)。如果代理上不存在任何已配置的主题,这将阻止容器启动。如果容器被配置为侦听主题模式(regex),则不会应用该选项。以前,容器线程在`consumer.poll()`方法中循环运行,等待在记录许多消息时出现主题。除了日志之外,没有迹象表明存在问题。 + +从版本 2.8 开始,引入了一个新的容器属性`authExceptionRetryInterval`。这将导致容器在从`KafkaConsumer`获取任何`AuthenticationException`或`AuthorizationException`后重试获取消息。例如,当被配置的用户被拒绝读取某个主题或凭据不正确时,就会发生这种情况。定义`authExceptionRetryInterval`允许容器在授予适当权限时恢复。 + +| |默认情况下,不会配置间隔——身份验证和授权错误被认为是致命的,这会导致容器停止。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.8 开始,在创建消费者工厂时,如果你将反序列化器作为对象(在构造函数中或通过 setter)提供,工厂将调用`configure()`方法来使用配置属性对它们进行配置。 + +###### 使用`ConcurrentMessageListenerContainer` + +单个构造函数类似于`KafkaListenerContainer`构造函数。下面的清单显示了构造函数的签名: + +``` +public ConcurrentMessageListenerContainer(ConsumerFactory consumerFactory, + ContainerProperties containerProperties) +``` + +它还具有`concurrency`属性。例如,`container.setConcurrency(3)`创建了三个`KafkaMessageListenerContainer`实例。 + +对于第一个构造函数,Kafka 使用其组管理功能在消费者之间分配分区。 + +| |当监听多个主题时,默认的分区分布可能不是你期望的那样,
例如,如果你有三个主题,每个主题有五个分区,并且希望使用`concurrency=15`,那么你只会看到五个活动的使用者,每个使用者从每个主题分配一个分区,
这是因为默认的 Kafka`PartitionAssignor`是`RangeAssignor`(参见其 Javadoc)。
对于这种情况,你可能想要考虑使用`RoundRobinAssignor`代替,它将分区分布在所有的消费者之间。,每个使用者被分配一个主题或分区。
要更改`PartitionAssignor`,可以将`partition.assignment.strategy`消费者属性(`ConsumerConfigs.PARTITION_ASSIGNMENT_STRATEGY_CONFIG`)中提供的属性设置为

在使用 Spring 引导时,可以将策略设置为:

r=“723”/>消费者属性。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当容器属性配置为`TopicPartitionOffset`s 时,`ConcurrentMessageListenerContainer`将`TopicPartitionOffset`实例分布在委托`KafkaMessageListenerContainer`实例中。 + +假设提供了六个`TopicPartitionOffset`实例,并且`concurrency`是`3`;每个容器都有两个分区。对于五个`TopicPartitionOffset`实例,两个容器获得两个分区,第三个容器获得一个分区。如果`concurrency`大于`TopicPartitions`的个数,则对`concurrency`进行向下调整,以便每个容器获得一个分区。 + +| |`client.id`属性(如果设置)以`-n`附加,其中`n`是对应于并发性的消费者实例。
这是在启用 JMX 时为 MBean 提供唯一名称所必需的。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 1.3 开始,`MessageListenerContainer`提供对底层`KafkaConsumer`的度量的访问。在`ConcurrentMessageListenerContainer`的情况下,`metrics()`方法返回所有目标`KafkaMessageListenerContainer`实例的度量。度量值由为底层`KafkaConsumer`提供的`client-id`分组为`Map`。 + +从版本 2.3 开始,`ContainerProperties`提供了一个`idleBetweenPolls`选项,让侦听器容器中的主循环在`KafkaConsumer.poll()`调用之间休眠。从所提供的选项和`max.poll.interval.ms`消费者配置和当前记录批处理时间之间的差值中选择一个实际的睡眠间隔作为最小值。 + +###### 提交偏移 + +为提交偏移提供了几个选项。如果`enable.auto.commit`消费者属性是`true`,Kafka 将根据其配置自动提交偏移。如果是`false`,则容器支持几个`AckMode`设置(在下一个列表中进行了描述)。默认的`AckMode`是`BATCH`。从版本 2.3 开始,该框架将`enable.auto.commit`设置为`false`,除非在配置中明确设置。以前,如果未设置属性,则使用 Kafka 默认值(`true`)。 + +消费者`poll()`方法返回一个或多个`ConsumerRecords`。为每个记录调用`MessageListener`。下面的列表描述了容器为每个`AckMode`(不使用事务时)所采取的操作: + +* `RECORD`:在侦听器在处理完记录后返回时提交偏移量。 + +* `BATCH`:在处理完`poll()`返回的所有记录后提交偏移量。 + +* `TIME`:在`poll()`返回的所有记录都已被处理的情况下提交偏移量,只要`ackTime`自上次提交以来的偏移量已被超过。 + +* `COUNT`:提交当`poll()`返回的所有记录都已被处理时的偏移量,只要`ackCount`记录自上次提交以来一直被接收。 + +* `COUNT_TIME`:类似于`TIME`和`COUNT`,但如果任一条件是`true`,则执行提交。 + +* `MANUAL`:消息侦听器负责`acknowledge()`的`Acknowledgment`。在此之后,将应用与`BATCH`相同的语义。 + +* `MANUAL_IMMEDIATE`:当侦听器调用`Acknowledgment.acknowledge()`方法时,立即提交偏移量。 + +当使用[交易](#transactions)时,偏移量被发送到事务,语义等价于`RECORD`或`BATCH`,这取决于侦听器类型(记录或批处理)。 + +| |`MANUAL`和`MANUAL_IMMEDIATE`要求侦听器是`AcknowledgingMessageListener`或`BatchAcknowledgingMessageListener`。
参见[消息侦听器](#message-listeners)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +根据`syncCommits`容器属性,将使用消费者上的`commitSync()`或`commitAsync()`方法。`syncCommits`默认情况下是`true`;还请参见`setSyncCommitTimeout`。参见`setCommitCallback`以获取异步提交的结果;默认的回调是`LoggingCommitCallback`,它记录错误(和调试级别的成功)。 + +因为侦听器容器有自己的提交偏移的机制,所以它更喜欢 kafka`ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG`为`false`。从版本 2.3 开始,它无条件地将其设置为 false,除非在消费者工厂或容器的消费者属性重写中专门设置了 false。 + +`Acknowledgment`具有以下方法: + +``` +public interface Acknowledgment { + + void acknowledge(); + +} +``` + +此方法使侦听器能够控制何时提交偏移。 + +从版本 2.3 开始,`Acknowledgment`接口有两个额外的方法`nack(long sleep)`和`nack(int index, long sleep)`。第一个用于记录侦听器,第二个用于批处理侦听器。为侦听器类型调用错误的方法将抛出`IllegalStateException`。 + +| |如果要使用`nack()`提交部分批处理,则在使用事务时,将`AckMode`设置为`MANUAL`;调用`nack()`将成功处理的记录的偏移量发送到事务。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`nack()`只能在调用侦听器的使用者线程上调用。| +|---|------------------------------------------------------------------------------| + +对于记录侦听器,当调用`nack()`时,将提交任何挂起的偏移量,丢弃上一次轮询的重置记录,并在它们的分区上执行查找,以便在下一次`poll()`上重新交付失败的记录和未处理的记录。通过设置`sleep`参数,可以在重新交付之前暂停使用者线程。这类似于当容器配置为`DefaultErrorHandler`时抛出异常的功能。 + +使用批处理侦听器时,可以在发生故障的批处理中指定索引。当调用`nack()`时,将对记录提交偏移,然后在分区上对失败和丢弃的记录执行索引和查找,以便在下一个`poll()`上重新交付它们。 + +有关更多信息,请参见[容器错误处理程序](#error-handlers)。 + +| |当通过组管理使用分区分配时,重要的是要确保`sleep`参数(加上处理来自上一次投票的记录所花费的时间)小于消费者`max.poll.interval.ms`属性。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 侦听器容器自动启动 + +侦听器容器实现`SmartLifecycle`,而`autoStartup`默认情况下是`true`。容器在后期启动(`Integer.MAX-VALUE - 100`)。实现`SmartLifecycle`以处理来自侦听器的数据的其他组件应该在较早的阶段启动。`- 100`为后面的阶段留出了空间,以使组件能够在容器之后自动启动。 + +##### 手动提交偏移 + +通常,当使用`AckMode.MANUAL`或`AckMode.MANUAL_IMMEDIATE`时,必须按顺序确认确认,因为 Kafka 不为每个记录维护状态,只为每个组/分区维护一个提交的偏移量。从版本 2.8 开始,你现在可以设置容器属性`asyncAcks`,它允许以任何顺序确认投票返回的记录的确认。侦听器容器将推迟顺序外的提交,直到收到缺少的确认。消费者将被暂停(没有新的记录交付),直到前一次投票的所有补偿都已提交。 + +| |虽然该特性允许应用程序异步处理记录,但应该理解的是,它增加了在发生故障后重复交付的可能性。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### `@KafkaListener`注释 + +`@KafkaListener`注释用于指定 Bean 方法作为侦听器容器的侦听器。 Bean 包装在`MessagingMessageListenerAdapter`中配置有各种特征,例如转换器来转换数据,如果需要,以匹配该方法的参数。 + +可以使用`#{…​}`或属性占位符(`${…​}`)使用 SPEL 配置注释上的大多数属性。有关更多信息,请参见[Javadoc](https://docs.spring.io/spring-kafka/api/org/springframework/kafka/annotation/KafkaListener.html)。 + +###### 记录收听者 + +`@KafkaListener`注释为简单的 POJO 侦听器提供了一种机制。下面的示例展示了如何使用它: + +``` +public class Listener { + + @KafkaListener(id = "foo", topics = "myTopic", clientIdPrefix = "myClientId") + public void listen(String data) { + ... + } + +} +``` + +这种机制需要在你的`@Configuration`类中的一个上进行`@EnableKafka`注释,并需要一个侦听器容器工厂,该工厂用于配置底层`ConcurrentMessageListenerContainer`。在缺省情况下,一个名称`kafkaListenerContainerFactory`的 Bean 是期望的。下面的示例展示了如何使用`ConcurrentMessageListenerContainer`: + +``` +@Configuration +@EnableKafka +public class KafkaConfig { + + @Bean + KafkaListenerContainerFactory> + kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + factory.setConcurrency(3); + factory.getContainerProperties().setPollTimeout(3000); + return factory; + } + + @Bean + public ConsumerFactory consumerFactory() { + return new DefaultKafkaConsumerFactory<>(consumerConfigs()); + } + + @Bean + public Map consumerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafka.getBrokersAsString()); + ... + return props; + } +} +``` + +注意,要设置容器属性,必须在工厂上使用`getContainerProperties()`方法。它被用作注入到容器中的实际属性的模板。 + +从版本 2.1.1 开始,你现在可以为由注释创建的消费者设置`client.id`属性。`clientIdPrefix`后缀为`-n`,其中`n`是表示使用并发性时容器号的整数。 + +从版本 2.2 开始,你现在可以通过在注释本身上使用属性来覆盖容器工厂的`concurrency`和`autoStartup`属性。这些属性可以是简单值、属性占位符或 SPEL 表达式。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(id = "myListener", topics = "myTopic", + autoStartup = "${listen.auto.start:true}", concurrency = "${listen.concurrency:3}") +public void listen(String data) { + ... +} +``` + +###### 显式分区分配 + +你还可以使用显式的主题和分区(以及它们的初始偏移量)来配置 POJO 侦听器。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(id = "thing2", topicPartitions = + { @TopicPartition(topic = "topic1", partitions = { "0", "1" }), + @TopicPartition(topic = "topic2", partitions = "0", + partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100")) + }) +public void listen(ConsumerRecord record) { + ... +} +``` + +你可以在`partitions`或`partitionOffsets`属性中指定每个分区,但不能同时指定这两个分区。 + +与大多数注释属性一样,你可以使用 SPEL 表达式;有关如何生成一个大的分区列表的示例,请参见[[tip-assign-all-parts]。 + +从版本 2.5.5 开始,你可以对所有分配的分区应用初始偏移量: + +``` +@KafkaListener(id = "thing3", topicPartitions = + { @TopicPartition(topic = "topic1", partitions = { "0", "1" }, + partitionOffsets = @PartitionOffset(partition = "*", initialOffset = "0")) + }) +public void listen(ConsumerRecord record) { + ... +} +``` + +`*`通配符表示`partitions`属性中的所有分区。每个`@TopicPartition`中必须只有一个带有通配符的`@PartitionOffset`。 + +此外,当侦听器实现`ConsumerSeekAware`时,现在调用`onPartitionsAssigned`,即使在使用手动分配时也是如此。例如,这允许在那个时候进行任意的查找操作。 + +从版本 2.6.4 开始,你可以指定一个以逗号分隔的分区列表,或分区范围: + +``` +@KafkaListener(id = "pp", autoStartup = "false", + topicPartitions = @TopicPartition(topic = "topic1", + partitions = "0-5, 7, 10-15")) +public void process(String in) { + ... +} +``` + +范围是包含的;上面的示例将分配分区`0, 1, 2, 3, 4, 5, 7, 10, 11, 12, 13, 14, 15`。 + +在指定初始偏移量时可以使用相同的技术: + +``` +@KafkaListener(id = "thing3", topicPartitions = + { @TopicPartition(topic = "topic1", + partitionOffsets = @PartitionOffset(partition = "0-5", initialOffset = "0")) + }) +public void listen(ConsumerRecord record) { + ... +} +``` + +初始偏移量将应用于所有 6 个分区。 + +###### 手动确认 + +当使用 Manual`AckMode`时,还可以向监听器提供`Acknowledgment`。下面的示例还展示了如何使用不同的容器工厂。 + +``` +@KafkaListener(id = "cat", topics = "myTopic", + containerFactory = "kafkaManualAckListenerContainerFactory") +public void listen(String data, Acknowledgment ack) { + ... + ack.acknowledge(); +} +``` + +###### 消费者记录元数据 + +最后,关于记录的元数据可以从消息头获得。你可以使用以下头名称来检索消息的头: + +* `KafkaHeaders.OFFSET` + +* `KafkaHeaders.RECEIVED_MESSAGE_KEY` + +* `KafkaHeaders.RECEIVED_TOPIC` + +* `KafkaHeaders.RECEIVED_PARTITION_ID` + +* `KafkaHeaders.RECEIVED_TIMESTAMP` + +* `KafkaHeaders.TIMESTAMP_TYPE` + +从版本 2.5 开始,如果传入的记录具有`null`键,则不存在`RECEIVED_MESSAGE_KEY`;以前,头被填充为`null`值。此更改是为了使框架与`spring-messaging`约定保持一致,其中不存在`null`值标头。 + +下面的示例展示了如何使用标题: + +``` +@KafkaListener(id = "qux", topicPattern = "myTopic1") +public void listen(@Payload String foo, + @Header(name = KafkaHeaders.RECEIVED_MESSAGE_KEY, required = false) Integer key, + @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition, + @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, + @Header(KafkaHeaders.RECEIVED_TIMESTAMP) long ts + ) { + ... +} +``` + +从版本 2.5 开始,你可以在`ConsumerRecordMetadata`参数中接收记录元数据,而不是使用离散的头。 + +``` +@KafkaListener(...) +public void listen(String str, ConsumerRecordMetadata meta) { + ... +} +``` + +这包含来自`ConsumerRecord`的所有数据,除了键和值。 + +###### 批处理侦听器 + +从版本 1.1 开始,你可以配置`@KafkaListener`方法来接收从消费者投票中接收到的整批消费者记录。要将侦听器容器工厂配置为创建批处理侦听器,你可以设置`batchListener`属性。下面的示例展示了如何做到这一点: + +``` +@Bean +public KafkaListenerContainerFactory batchFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + factory.setBatchListener(true); // <<<<<<<<<<<<<<<<<<<<<<<<< + return factory; +} +``` + +| |从版本 2.8 开始,你可以使用`@KafkaListener`注释上的`batch`属性重写工厂的`batchListener`Propery。
这一点以及对[容器错误处理程序](#error-handlers)的更改允许对记录和批处理侦听器使用相同的工厂。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何接收有效载荷列表: + +``` +@KafkaListener(id = "list", topics = "myTopic", containerFactory = "batchFactory") +public void listen(List list) { + ... +} +``` + +主题、分区、偏移量等在与有效负载并行的标题中可用。下面的示例展示了如何使用标题: + +``` +@KafkaListener(id = "list", topics = "myTopic", containerFactory = "batchFactory") +public void listen(List list, + @Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) List keys, + @Header(KafkaHeaders.RECEIVED_PARTITION_ID) List partitions, + @Header(KafkaHeaders.RECEIVED_TOPIC) List topics, + @Header(KafkaHeaders.OFFSET) List offsets) { + ... +} +``` + +或者,可以接收一个`List`的`Message`对象与每个偏移和每个消息中的其他详细信息,但是它必须是在方法上定义的唯一参数(除了可选的`Acknowledgment`,当使用手动提交时,和/或`Consumer`参数)。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(id = "listMsg", topics = "myTopic", containerFactory = "batchFactory") +public void listen14(List> list) { + ... +} + +@KafkaListener(id = "listMsgAck", topics = "myTopic", containerFactory = "batchFactory") +public void listen15(List> list, Acknowledgment ack) { + ... +} + +@KafkaListener(id = "listMsgAckConsumer", topics = "myTopic", containerFactory = "batchFactory") +public void listen16(List> list, Acknowledgment ack, Consumer consumer) { + ... +} +``` + +在这种情况下,不对有效负载执行任何转换。 + +如果`BatchMessagingMessageConverter`被配置为`RecordMessageConverter`,那么你还可以向`Message`参数添加一个泛型类型,然后对有效负载进行转换。有关更多信息,请参见[使用批处理侦听器的有效负载转换](#payload-conversion-with-batch)。 + +你还可以接收`ConsumerRecord`对象的列表,但它必须是方法上定义的唯一参数(除了可选的`Acknowledgment`,当使用手动提交和`Consumer`参数时)。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(id = "listCRs", topics = "myTopic", containerFactory = "batchFactory") +public void listen(List> list) { + ... +} + +@KafkaListener(id = "listCRsAck", topics = "myTopic", containerFactory = "batchFactory") +public void listen(List> list, Acknowledgment ack) { + ... +} +``` + +从版本 2.2 开始,侦听器可以接收由`poll()`方法返回的完整`ConsumerRecords`对象,让侦听器访问其他方法,例如`partitions()`(它返回列表中的`TopicPartition`实例)和`records(TopicPartition)`(它获得选择性记录)。同样,这必须是方法上唯一的参数(除了可选的`Acknowledgment`,当使用手动提交或`Consumer`参数时)。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(id = "pollResults", topics = "myTopic", containerFactory = "batchFactory") +public void pollResults(ConsumerRecords records) { + ... +} +``` + +| |如果容器工厂配置了`RecordFilterStrategy`,则对于`ConsumerRecords`侦听器将忽略它,并发出`WARN`日志消息。
如果使用`>`形式的侦听器,则只能使用批侦听器过滤记录。默认情况下,
,记录是一次过滤一次的;从版本 2.8 开始,你可以覆盖`filterBatch`以在一个调用中过滤整个批处理。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 注释属性 + +从版本 2.0 开始,`id`属性(如果存在)被用作 Kafka Consumer`group.id`属性,如果存在,则覆盖 Consumer 工厂中的配置属性。还可以显式地将`groupId`设置为`idIsGroup`,也可以将`idIsGroup`设置为 false,以恢复以前使用消费者工厂`group.id`的行为。 + +你可以在大多数注释属性中使用属性占位符或 SPEL 表达式,如下例所示: + +``` +@KafkaListener(topics = "${some.property}") + +@KafkaListener(topics = "#{someBean.someProperty}", + groupId = "#{someBean.someProperty}.group") +``` + +从版本 2.1.2 开始,SPEL 表达式支持一个特殊的令牌:`__listener`。它是一个伪 Bean 名称,表示存在此注释的当前 Bean 实例。 + +考虑以下示例: + +``` +@Bean +public Listener listener1() { + return new Listener("topic1"); +} + +@Bean +public Listener listener2() { + return new Listener("topic2"); +} +``` + +考虑到前面示例中的 bean,我们可以使用以下方法: + +``` +public class Listener { + + private final String topic; + + public Listener(String topic) { + this.topic = topic; + } + + @KafkaListener(topics = "#{__listener.topic}", + groupId = "#{__listener.topic}.group") + public void listen(...) { + ... + } + + public String getTopic() { + return this.topic; + } + +} +``` + +如果在不太可能的情况下,你有一个实际的 Bean 名为`__listener`,那么你可以使用`beanRef`属性来更改表达式标记。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(beanRef = "__x", topics = "#{__x.topic}", + groupId = "#{__x.topic}.group") +``` + +从版本 2.2.4 开始,你可以直接在注释中指定 Kafka 消费者属性,这些属性将覆盖在消费者工厂中配置的具有相同名称的任何属性。以这种方式指定**不能**和`client.id`属性;它们将被忽略;对这些属性使用`groupId`和`clientIdPrefix`注释属性。 + +这些属性被指定为具有普通 Java`Properties`文件格式的单个字符串:`foo:bar`,`foo=bar`,或`foo bar`。 + +``` +@KafkaListener(topics = "myTopic", groupId = "group", properties = { + "max.poll.interval.ms:60000", + ConsumerConfig.MAX_POLL_RECORDS_CONFIG + "=100" +}) +``` + +下面是[使用`RoutingKafkaTemplate`](#routing-template)示例中的相应侦听器的示例。 + +``` +@KafkaListener(id = "one", topics = "one") +public void listen1(String in) { + System.out.println("1: " + in); +} + +@KafkaListener(id = "two", topics = "two", + properties = "value.deserializer:org.apache.kafka.common.serialization.ByteArrayDeserializer") +public void listen2(byte[] in) { + System.out.println("2: " + new String(in)); +} +``` + +##### 获取消费者`group.id` + +当在多个容器中运行相同的侦听器代码时,能够确定记录来自哪个容器(由其`group.id`消费者属性标识)可能是有用的。 + +你可以在侦听器线程上调用`KafkaUtils.getConsumerGroupId()`来执行此操作。或者,你可以访问方法参数中的组 ID。 + +``` +@KafkaListener(id = "bar", topicPattern = "${topicTwo:annotated2}", exposeGroupId = "${always:true}") +public void listener(@Payload String foo, + @Header(KafkaHeaders.GROUP_ID) String groupId) { +... +} +``` + +| |这在接收`List`记录的记录侦听器和批处理侦听器中可用。**不是**在接收`ConsumerRecords`参数的批处理侦听器中可用。
在这种情况下使用`KafkaUtils`机制。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 容器线程命名 + +侦听器容器当前使用两个任务执行器,一个用于调用使用者,另一个用于在 Kafka 消费者属性`enable.auto.commit`为`false`时调用侦听器。你可以通过设置容器的`consumerExecutor`和`listenerExecutor`属性来提供自定义执行器。当使用池执行程序时,确保有足够多的线程可用来处理使用它们的所有容器之间的并发性。当使用`ConcurrentMessageListenerContainer`时,来自每个使用者的线程都用于每个使用者(`concurrency`)。 + +如果不提供消费者执行器,则使用`SimpleAsyncTaskExecutor`。此执行器创建名称与`-C-1`(使用者线程)类似的线程。对于`ConcurrentMessageListenerContainer`,线程名称的``部分变成`-m`,其中`m`表示消费者实例。`n`每次启动容器时都会增加。所以,具有 Bean 名称的`container`,此容器中的线程将被命名为`container-0-C-1`、`container-1-C-1`等,在容器被第一次启动之后;`container-0-C-2`、`container-1-C-2`等,在停止之后又被随后的启动。 + +##### `@KafkaListener`作为元注释 + +从版本 2.2 开始,你现在可以使用`@KafkaListener`作为元注释。下面的示例展示了如何做到这一点: + +``` +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@KafkaListener +public @interface MyThreeConsumersListener { + + @AliasFor(annotation = KafkaListener.class, attribute = "id") + String id(); + + @AliasFor(annotation = KafkaListener.class, attribute = "topics") + String[] topics(); + + @AliasFor(annotation = KafkaListener.class, attribute = "concurrency") + String concurrency() default "3"; + +} +``` + +你必须至少别名`topics`、`topicPattern`或`topicPartitions`中的一个(并且,通常是`id`或`groupId`,除非你在消费者工厂配置中指定了`group.id`)。下面的示例展示了如何做到这一点: + +``` +@MyThreeConsumersListener(id = "my.group", topics = "my.topic") +public void listen1(String in) { + ... +} +``` + +##### 在类上`@KafkaListener` + +在类级别上使用`@KafkaListener`时,必须在方法级别上指定`@KafkaHandler`。在发送消息时,将使用转换后的消息有效负载类型来确定调用哪个方法。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(id = "multi", topics = "myTopic") +static class MultiListenerBean { + + @KafkaHandler + public void listen(String foo) { + ... + } + + @KafkaHandler + public void listen(Integer bar) { + ... + } + + @KafkaHandler(isDefault = true) + public void listenDefault(Object object) { + ... + } + +} +``` + +从版本 2.1.3 开始,你可以将`@KafkaHandler`方法指定为默认方法,如果其他方法不匹配,则调用该方法。最多只能指定一种方法。当使用`@KafkaHandler`方法时,有效负载必须已经转换为域对象(因此可以执行匹配)。使用自定义的反序列化器,`JsonDeserializer`,或`JsonMessageConverter`,其`TypePrecedence`设置为`TYPE_ID`。有关更多信息,请参见[序列化、反序列化和消息转换](#serdes)。 + +| |由于 Spring 解析方法参数的方式的某些限制,默认的`@KafkaHandler`不能接收离散的头;它必须使用`ConsumerRecordMetadata`中讨论的[消费者记录元数据](#consumer-record-metadata)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例如: + +``` +@KafkaHandler(isDefault = true) +public void listenDefault(Object object, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) { + ... +} +``` + +如果对象是`String`,这将不起作用;`topic`参数还将获得对`object`的引用。 + +如果在默认方法中需要有关记录的元数据,请使用以下方法: + +``` +@KafkaHandler(isDefault = true) +void listen(Object in, @Header(KafkaHeaders.RECORD_METADATA) ConsumerRecordMetadata meta) { + String topic = meta.topic(); + ... +} +``` + +##### `topic`属性修改 + +从版本 2.7.2 开始,你现在可以在创建容器之前以编程方式修改注释属性。为此,将一个或多个`KafkaListenerAnnotationBeanPostProcessor.AnnotationEnhancer`添加到应用程序上下文。`AnnotationEnhancer`是一个`BiFunction, AnnotatedElement, Map`,并且必须返回属性映射。属性值可以包含 SPEL 和/或属性占位符;在执行任何解析之前都会调用增强器。如果存在多个增强器,并且它们实现`Ordered`,则将按顺序调用它们。 + +| |必须声明`AnnotationEnhancer` Bean 定义`static`,因为它们是应用程序上下文生命周期的早期要求。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------| + +以下是一个例子: + +``` +@Bean +public static AnnotationEnhancer groupIdEnhancer() { + return (attrs, element) -> { + attrs.put("groupId", attrs.get("id") + "." + (element instanceof Class + ? ((Class) element).getSimpleName() + : ((Method) element).getDeclaringClass().getSimpleName() + + "." + ((Method) element).getName())); + return attrs; + }; +} +``` + +##### `@KafkaListener`生命周期管理 + +为`@KafkaListener`注释创建的侦听器容器不是应用程序上下文中的 bean。相反,它们被注册在类型`KafkaListenerEndpointRegistry`的基础结构 Bean 中。 Bean 由框架自动声明并管理容器的生命周期;它将自动启动将`autoStartup`设置为`true`的任何容器。由所有容器工厂创建的所有容器必须在相同的`phase`中。有关更多信息,请参见[监听器容器自动启动](#container-auto-startup)。你可以通过使用注册表以编程方式管理生命周期。启动或停止注册表将启动或停止所有已注册的容器。或者,你可以通过使用其`id`属性获得对单个容器的引用。你可以在注释上设置`autoStartup`,这会覆盖配置到容器工厂中的默认设置。你可以从应用程序上下文中获得对 Bean 的引用,例如自动布线,以管理其注册的容器。下面的例子说明了如何做到这一点: + +``` +@KafkaListener(id = "myContainer", topics = "myTopic", autoStartup = "false") +public void listen(...) { ... } +``` + +``` +@Autowired +private KafkaListenerEndpointRegistry registry; + +... + + this.registry.getListenerContainer("myContainer").start(); + +... +``` + +注册中心仅维护其管理的容器的生命周期;声明为 bean 的容器不受注册中心的管理,可以从应用程序上下文中获得。可以通过调用注册表的`getListenerContainers()`方法获得托管容器的集合。版本 2.2.5 添加了一个方便的方法`getAllListenerContainers()`,该方法返回所有容器的集合,包括由注册中心管理的容器和声明为 bean 的容器。返回的集合将包括任何已初始化的原型 bean,但它不会初始化任何懒惰的 Bean 声明。 + +##### `@KafkaListener``@Payload`验证 + +从版本 2.2 开始,现在更容易添加`Validator`来验证`@KafkaListener``@Payload`参数。以前,你必须配置一个自定义`DefaultMessageHandlerMethodFactory`并将其添加到注册商。现在,你可以将验证器添加到注册器本身。下面的代码展示了如何做到这一点: + +``` +@Configuration +@EnableKafka +public class Config implements KafkaListenerConfigurer { + + ... + + @Override + public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) { + registrar.setValidator(new MyValidator()); + } + +} +``` + +| |当你使用 Spring 引导和验证启动器时,`LocalValidatorFactoryBean`是自动配置的,如下例所示:| +|---|---------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Configuration +@EnableKafka +public class Config implements KafkaListenerConfigurer { + + @Autowired + private LocalValidatorFactoryBean validator; + ... + + @Override + public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) { + registrar.setValidator(this.validator); + } +} +``` + +以下示例展示了如何验证: + +``` +public static class ValidatedClass { + + @Max(10) + private int bar; + + public int getBar() { + return this.bar; + } + + public void setBar(int bar) { + this.bar = bar; + } + +} +``` + +``` +@KafkaListener(id="validated", topics = "annotated35", errorHandler = "validationErrorHandler", + containerFactory = "kafkaJsonListenerContainerFactory") +public void validatedListener(@Payload @Valid ValidatedClass val) { + ... +} + +@Bean +public KafkaListenerErrorHandler validationErrorHandler() { + return (m, e) -> { + ... + }; +} +``` + +从版本 2.5.11 开始,验证现在可以在类级侦听器中的`KafkaMessageListenerContainer`方法的有效负载上进行。参见[`@KafkaListener`on a class](#class-level-kafkalistener)。 + +##### 重新平衡听众 + +`ContainerProperties`具有一个名为`consumerRebalanceListener`的属性,它接受了 Kafka 客户机的`ConsumerRebalanceListener`接口的一个实现。如果不提供此属性,则容器将配置一个日志侦听器,该侦听器将在`INFO`级别记录重新平衡事件。该框架还添加了一个子接口`@KafkaListener`。下面的清单显示了`ConsumerAwareRebalanceListener`接口定义: + +``` +public interface ConsumerAwareRebalanceListener extends ConsumerRebalanceListener { + + void onPartitionsRevokedBeforeCommit(Consumer consumer, Collection partitions); + + void onPartitionsRevokedAfterCommit(Consumer consumer, Collection partitions); + + void onPartitionsAssigned(Consumer consumer, Collection partitions); + + void onPartitionsLost(Consumer consumer, Collection partitions); + +} +``` + +注意,当分区被撤销时有两个回调。第一个是立即调用的。第二种方法是在任何未完成的补偿被提交后调用。如果你希望在某些外部存储库中维护偏移,这是非常有用的,如下例所示: + +``` +containerProperties.setConsumerRebalanceListener(new ConsumerAwareRebalanceListener() { + + @Override + public void onPartitionsRevokedBeforeCommit(Consumer consumer, Collection partitions) { + // acknowledge any pending Acknowledgments (if using manual acks) + } + + @Override + public void onPartitionsRevokedAfterCommit(Consumer consumer, Collection partitions) { + // ... + store(consumer.position(partition)); + // ... + } + + @Override + public void onPartitionsAssigned(Collection partitions) { + // ... + consumer.seek(partition, offsetTracker.getOffset() + 1); + // ... + } +}); +``` + +| |从版本 2.4 开始,已经添加了一个新的方法`onPartitionsLost()`(类似于`ConsumerRebalanceLister`中同名的方法)。
`ConsumerRebalanceLister`上的默认实现只调用`onPartionsRevoked`。
上的默认实现在`ConsumerAwareRebalanceListener`上什么也不做。,`org.springframework.messaging.Message`在向侦听器容器提供自定义侦听器(任一种类型)时,这很重要表示你的实现不调用`onPartitionsRevoked`from`onPartitionsLost`。
如果你实现`ConsumerRebalanceListener`,那么你应该覆盖默认的方法。
这是因为侦听器容器将从其实现的`onPartitionsRevoked`调用它自己的`onPartitionsLost`在调用你的实现中的方法之后。
如果你将实现委托给默认行为,则每次`onPartitionsRevoked`调用容器的侦听器上的方法时,都会调用两次`Consumer`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用`@SendTo`转发监听器结果 + +从版本 2.0 开始,如果你还使用`@KafkaListener`注释`@KafkaListener`,并且方法调用返回一个结果,则结果将被转发到[一次语义学](#exactly-once)指定的主题。 + +`@SendTo`值可以有几种形式: + +* `@SendTo("someTopic")`路由到字面主题 + +* `KafkaTemplate`路由到主题,该主题是在应用程序上下文初始化期间通过计算表达式一次来确定的。 + +* `@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 replyingBatchListener(List 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_MESSAGE_KEY) int key) { + ... + } + +} +``` + +| |为了支持`@SendTo`,侦听器容器工厂必须提供一个`onPartitionsRevoked`(在其`replyTemplate`属性中),这应该是一个`KafkaTemplate`,而不是一个`ReplyingKafkaTemplate`,它在客户端用于请求/回复处理。
当使用 Spring 引导时,引导会自动将模板配置到工厂;当配置自己的工厂时,它必须设置为如下示例所示。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.2 开始,你可以向监听器容器工厂添加`ReplyHeadersConfigurer`。查询此项以确定你想要在回复消息中设置哪些头。下面的示例展示了如何添加`ReplyHeadersConfigurer`: + +``` +@Bean +public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(cf()); + factory.setReplyTemplate(template()); + factory.setReplyHeadersConfigurer((k, v) -> k.equals("cat")); + return factory; +} +``` + +如果你愿意,还可以添加更多的标题。下面的示例展示了如何做到这一点: + +``` +@Bean +public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory 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 additionalHeaders() { + return Collections.singletonMap("qux", "fiz"); + } + + }); + return factory; +} +``` + +当使用`@SendTo`时,必须在其`replyTemplate`属性中配置`ReplyHeadersConfigurer`,以执行发送。 + +| |除非你使用[请求/回复语义](#replying-template)只使用简单的`send(topic, value)`方法,所以你可能希望创建一个子类来生成分区或键。
下面的示例展示了如何这样做:| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Bean +public KafkaTemplate myReplyingTemplate() { + return new KafkaTemplate(producerFactory()) { + + @Override + public ListenableFuture> send(String topic, String data) { + return super.send(topic, partitionForData(data), keyForData(data), data); + } + + ... + + }; +} +``` + +| |如果侦听器方法返回`Message`或`Collection>`,则侦听器方法负责为答复设置消息头。
例如,当处理来自`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.MESSAGE_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
};
}
```

参见[处理异常](#annotation-error-handling)以获取更多信息。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果侦听器方法返回`Iterable`,那么默认情况下,每个元素的值都会被发送,
从版本 2.3.5 开始,将`@KafkaListener`上的`splitIterables`属性设置为`false`,整个结果将作为单个`ProducerRecord`的值发送。
这需要在回复模板的生产者配置中有一个合适的序列化器,
但是,如果回复是`Iterable>`,则忽略该属性,并分别发送每条消息。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 过滤消息 + +在某些情况下,例如重新平衡,已经处理过的消息可能会被重新传递。框架不能知道这样的消息是否已被处理。这是一个应用程序级函数。这被称为[幂等接收机](https://www.enterpriseintegrationpatterns.com/patterns/messaging/IdempotentReceiver.html)模式,并且 Spring 集成提供了[幂等接收机](https://www.enterpriseintegrationpatterns.com/patterns/messaging/IdempotentReceiver.html)。 + +Spring for Apache Kafka 项目还通过`FilteringMessageListenerAdapter`类提供了一些帮助,它可以包装你的`MessageListener`。该类接受`RecordFilterStrategy`的实现,在该实现中,你实现`filter`方法,以表示消息是重复的,应该丢弃。这有一个名为`ackDiscarded`的附加属性,它指示适配器是否应该确认丢弃的记录。默认情况下是`false`。 + +当使用`@KafkaListener`时,在容器工厂上设置`RecordFilterStrategy`(以及可选的`ackDiscarded`),以便侦听器被包装在适当的过滤适配器中。 + +此外,当你使用批处理[消息监听器](#message-listeners)时,还提供了一个`FilteringBatchMessageListenerAdapter`。 + +| |如果你的`@KafkaListener`接收的是`ConsumerRecords`而不是`List>`,则忽略`FilteringBatchMessageListenerAdapter`,因为`ConsumerRecords`是不可变的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 重试送货 + +参见[处理异常](#annotation-error-handling)中的`DefaultErrorHandler`。 + +##### 按顺序开始`@KafkaListener`s + +一个常见的用例是,在另一个侦听器消耗了一个主题中的所有记录之后,启动一个侦听器。例如,在处理来自其他主题的记录之前,你可能希望将一个或多个压缩主题的内容加载到内存中。从版本 2.7.3 开始,引入了一个新的组件`ContainerGroupSequencer`。它使用`@KafkaListener``containerGroup`属性将容器分组,并在当前组中的所有容器都空闲时启动下一个组中的容器。 + +用一个例子最好地说明这一点。 + +``` +@KafkaListener(id = "listen1", topics = "topic1", containerGroup = "g1", concurrency = "2") +public void listen1(String in) { +} + +@KafkaListener(id = "listen2", topics = "topic2", containerGroup = "g1", concurrency = "2") +public void listen2(String in) { +} + +@KafkaListener(id = "listen3", topics = "topic3", containerGroup = "g2", concurrency = "2") +public void listen3(String in) { +} + +@KafkaListener(id = "listen4", topics = "topic4", containerGroup = "g2", concurrency = "2") +public void listen4(String in) { +} + +@Bean +ContainerGroupSequencer sequencer(KafkaListenerEndpointRegistry registry) { + return new ContainerGroupSequencer(registry, 5000, "g1", "g2"); +} +``` + +在这里,我们在两组中有 4 个听众,`g1`和`g2`。 + +在应用程序上下文初始化期间,Sequencer 将提供的组中所有容器的`autoStartup`属性设置为`false`。它还将任何容器(还没有设置)的`idleEventInterval`设置为提供的值(在本例中为 5000ms)。然后,当应用程序上下文启动序列器时,第一组中的容器将被启动。当`ListenerContainerIdleEvent`s 被接收时,每个容器中的每个单独的子容器都被停止。当`ConcurrentMessageListenerContainer`中的所有子容器被停止时,父容器被停止。当一个组中的所有容器都被停止时,下一个组中的容器将被启动。一个组中的组或容器的数量没有限制。 + +默认情况下,最终组(`g2`以上)中的容器在空闲时不会停止。要修改该行为,请将序列器上的`stopLastGroupWhenIdle`设置为`true`。 + +作为旁白;以前,每个组中的容器都被添加到类型`Collection`的 Bean 中,其 Bean 名称为`containerGroup`。现在不推荐这些集合,而支持类型`ContainerGroup`的 bean,其 Bean 名称是组名,后缀为`.group`;在上面的示例中,将有 2 个 bean`g1.group`和`g2.group`。`Collection`bean 将在未来的版本中被删除。 + +##### 使用`KafkaTemplate`接收 + +本节介绍如何使用`KafkaTemplate`接收消息。 + +从版本 2.8 开始,模板有四个`receive()`方法: + +``` +ConsumerRecord receive(String topic, int partition, long offset); + +ConsumerRecord receive(String topic, int partition, long offset, Duration pollTimeout); + +ConsumerRecords receive(Collection requested); + +ConsumerRecords receive(Collection requested, Duration pollTimeout); +``` + +如你所见,你需要知道需要检索的记录的分区和偏移量;为每个操作创建(并关闭)一个新的`Consumer`。 + +使用最后两个方法,可以单独检索每个记录,并将结果组装到`ConsumerRecords`对象中。在为请求创建`TopicPartitionOffset`s 时,只支持正的绝对偏移量。 + +#### 4.1.5.侦听器容器属性 + +| Property | Default |说明| +|---------------------------------------------------------------|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| | 1 |当`ackMode`为`COUNT`或`COUNT_TIME`时,提交挂起偏移之前的记录数量。| +| | `null` |一串`Advice`对象(例如`MethodInterceptor`关于建议)包装消息侦听器,按顺序调用。| +| 。| +| `]| +| | 5000 |当`ackMode`为`TIME`或`COUNT_TIME`时,提交挂起的偏移量的时间(以毫秒为单位)。| +| | LATEST\_ONLY \_NO\_TX |是否提交分配时的初始位置;默认情况下,只有当`ConsumerConfig.AUTO_OFFSET_RESET_CONFIG`是`latest`时,才会提交初始偏移,并且即使存在事务管理器,也不会在事务中运行。
有关可用选项的更多信息,请参见`ContainerProperties.AssignmentCommitOption`的 Javadocs。| +|| `null` |当不是 null 时,当 Kafka 客户端抛出一个`AuthenticationException`或`AuthorizationException`时,一个`ContainerProperties.AssignmentCommitOption`在轮询之间休眠。`ContainerProperties.AssignmentCommitOption`当为 null 时,此类异常被认为是致命的,容器将停止。| +| |`client.id`消费者属性的前缀。
覆盖了消费者工厂`client.id`属性;在并发容器中,`ContainerProperties.AssignmentCommitOption`被添加为每个消费者实例的后缀。| +| | false |设置为`true`,以便在接收到`null``key`报头时始终检查`DeserializationException`报头。
在消费者代码无法确定已配置`ErrorHandlingDeserializer`时有用,例如在使用委托反序列化器时。| +| | false |设置为`true`,以便在接收到`DeserializationException``value`报头时始终检查`DeserializationException`报头。
在消费者代码无法确定已配置`ErrorHandlingDeserializer`时有用,例如在使用委托反序列化器时。| +| | `null` |当 present 和`syncCommits`是`false`时,在提交完成后调用的回调。| +| | DEBUG |用于提交偏移的日志的日志记录级别。| +| 。| +| | 30s |在记录错误之前等待使用者启动的时间;如果使用线程不足的任务执行器,可能会发生这种情况。| +| |`SimpleAsyncTaskExecutor`|用于运行使用者线程的任务执行器。
默认执行器创建名为`-C-n`的线程;使用`KafkaMessageListenerContainer`,名称为 Bean 名称;使用`ConcurrentMessageListenerContainer`,名称为 Bean 名称,后缀为`-n`,其中 n 为每个子容器递增。| +| 。| +| | `V2` |精确一次语义模式;参见`syncCommits`。| +| 。| +| | `null` |覆盖消费者`group.id`属性;由`isolation.level=read_committed``id`或`groupId`属性自动设置。| +| | 5.0 |在接收到任何记录之前应用的
乘法器。
在接收到记录之后,不再应用乘法器。
自版本 2.8 起可用。| +| | 0 |用于通过在轮询之间休眠线程来减慢交付速度。
处理一批记录的时间加上该值必须小于`max.poll.interval.ms`消费者属性。| +| 。
也参见`idleBeforeDataMultiplier`。| +|。| +| | None |用于覆盖在消费者工厂上配置的任意消费者属性。| +| | `false` |设置为 true 以在信息级别记录所有容器属性.| +| | `null` |消息监听器。| +| | `true` |是否为用户线程维护千分尺计时器。| +| | `false` |如果代理上不存在配置的主题,则当 TRUE 阻止容器启动时。| +| 和`pollTimeout`。| +| | 3.0 |乘以`pollTimeOut`,以确定是否发布`NonResponsiveConsumerEvent`。
见`monitorInterval`。| +| `。| +| `。| +| |`ThreadPoolTaskScheduler`|在其上运行消费者监视器任务的计划程序。| +| `方法的最长时间,直到所有消费者停止并且在发布容器停止事件之前。| +| 。| +| | `false` |当容器被停止时,在当前记录之后停止处理,而不是在处理来自上一个轮询的所有记录之后。| +| 。| +| | `null` |当`syncCommits`时要使用的超时是`true`。
未设置时,容器将尝试确定`default.api.timeout.ms`消费者属性并使用它;否则将使用 60 秒。| +| | `true` |是否使用同步或异步提交进行偏移;请参见`commitCallback`。| +| | n/a |已配置的主题、主题模式或显式分配的主题/分区。
互斥;至少必须提供一个;由`ContainerProperties`构造函数强制执行。| +| 。| + +| Property | Default |说明| +|-------------------------------------------------------------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| |`DefaultAfterRollbackProcessor`|回滚事务后调用的`AfterRollbackProcessor`。| +|| application context |事件发布者。| +| | See desc. |弃用-见`commonErrorHandler`。| +| | `null` |设置`BatchInterceptor`在调用批处理侦听器之前调用;不适用于记录侦听器。
另请参见`interceptBeforeTx`。| +| | bean name |容器的 Bean 名称;后缀为子容器的`-n`。| +| 。| +| | `ContainerProperties` |容器属性实例。| +| | See desc. |弃用-见`commonErrorHandler`。| +| | See desc. |弃用-见`commonErrorHandler`。| +| | See desc. |`default.api.timeout.ms`,如果存在,否则来自消费工厂的`group.id`属性。| +| | `true` |确定是在事务开始之前还是之后调用`recordInterceptor`。| +| | See desc. |Bean 用户配置容器的名称或`@KafkaListener`s 的`id`属性。| +| |如果请求了消费者暂停,则为真。| +| | `null` |设置`RecordInterceptor`在调用记录侦听器之前调用;不适用于批处理侦听器。
另请参见`interceptBeforeTx`。| +| | 30s |当`missingTopicsFatal`容器属性是`true`时,要等待多长时间(以秒为单位)才能完成`describeTopics`操作。| + +| Property | Default |说明| +|-------------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------| +| |当前分配给这个容器的分区(显式或非显式)。| +||当前分配给这个容器的分区(显式或非显式)。| +| | `null` |并发容器用于为每个子容器的使用者提供唯一的`client.id`。| +| | n/a |如果请求暂停,而消费者实际上已经暂停,则为真。| + +| Property | Default |说明| +|-------------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| | `true` |设置为 FALSE 以禁止在`concurrency`消费者属性中添加后缀,此时`concurrency`仅为 1.| +| |当前分配给这个容器的子`KafkaMessageListenerContainer`s 的分区的集合(显式或非显式)。| +||当前分配给这个容器的子容器`KafkaMessageListenerContainer`s(显式或非显式)的分区,由子容器的使用者的`client.id`属性进行键控。| +| | 1 |要管理的子`KafkaMessageListenerContainer`s 的数量。| +| | n/a |如果请求了暂停,并且所有子容器的使用者实际上已经暂停,则为真。| +| | n/a |对所有子`KafkaMessageListenerContainer`s 的引用。| + +#### 4.1.6.应用程序事件 + +以下 Spring 应用程序事件由侦听器容器及其使用者发布: + +* `ConsumerStartingEvent`-在使用者线程第一次启动时发布,然后开始轮询。 + +* `ConsumerStartedEvent`-在使用者即将开始轮询时发布。 + +* `ConsumerFailedToStartEvent`-如果在`consumerStartTimeout`容器属性内没有`ConsumerStartingEvent`发布,则发布。此事件可能表示配置的任务执行器没有足够的线程来支持它所使用的容器及其并发性。当出现此情况时,还会记录错误消息。 + +* `ListenerContainerIdleEvent`:在`idleInterval`(如果配置)中没有收到消息时发布。 + +* `ListenerContainerNoLongerIdleEvent`:在先前发布`ListenerContainerIdleEvent`后,当记录被消费时发布。 + +* `ListenerContainerPartitionIdleEvent`:在`idlePartitionEventInterval`中没有从该分区接收到消息时发布(如果已配置)。 + +* `ListenerContainerPartitionNoLongerIdleEvent`:当从以前发布过`ListenerContainerPartitionIdleEvent`的分区中消费一条记录时发布。 + +* `NonResponsiveConsumerEvent`:当消费者似乎在`poll`方法中被阻止时发布。 + +* `ConsumerPartitionPausedEvent`:当一个分区暂停时,由每个使用者发布。 + +* `ConsumerPartitionResumedEvent`:当一个分区被恢复时,由每个使用者发布。 + +* `ConsumerPausedEvent`:当容器暂停时,由每个使用者发布。 + +* `ConsumerResumedEvent`:当容器恢复时,由每个使用者发布。 + +* `max.poll.interval.ms`:在停止之前由每个消费者发布。 + +* `ConsumerStoppedEvent`:在消费者关闭后发布。见[螺纹安全](#thread-safety)。 + +* `ContainerStoppedEvent`:当所有消费者都停止使用时发布。 + +| |默认情况下,应用程序上下文的事件多播报器调用调用调用线程上的事件侦听器。
如果将多播报器更改为使用异步执行器,则当事件包含对使用者的引用时,不得调用任何`Consumer`方法。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`ListenerContainerIdleEvent`具有以下属性: + +* `source`:发布事件的侦听器容器实例。 + +* `container`:侦听器容器或父侦听器容器,如果源容器是一个子容器。 + +* `id`:侦听器 ID(或容器 Bean 名称)。 + +* `idleTime`:事件发布时容器处于空闲状态的时间。 + +* `topicPartitions`:在事件生成时容器被分配的主题和分区。 + +* `consumer`:对 Kafka`Consumer`对象的引用。例如,如果先前调用了消费者的`pause()`方法,那么当接收到事件时,它可以`resume()`。 + +* `paused`:容器当前是否暂停。有关更多信息,请参见[暂停和恢复监听器容器](#pause-resume)。 + +`ListenerContainerNoLongerIdleEvent`具有相同的属性,但`idleTime`和`paused`除外。 + +`ListenerContainerPartitionIdleEvent`具有以下属性: + +* `source`:发布事件的侦听器容器实例。 + +* `container`:侦听器容器或父侦听器容器,如果源容器是一个子容器。 + +* `id`:侦听器 ID(或容器 Bean 名称)。 + +* `idleTime`:事件发布时,分区消耗的时间是空闲的。 + +* `topicPartition`:触发事件的主题和分区。 + +* `consumer`:对 Kafka`Consumer`对象的引用。例如,如果先前调用了消费者的`pause()`方法,那么当接收到事件时,它可以`resume()`。 + +* `paused`:是否为该消费者暂停了该分区的消费。有关更多信息,请参见[暂停和恢复监听器容器](#pause-resume)。 + +`ListenerContainerPartitionNoLongerIdleEvent`具有相同的属性,但`idleTime`和`paused`除外。 + +`NonResponsiveConsumerEvent`具有以下属性: + +* `source`:发布事件的侦听器容器实例。 + +* `container`:侦听器容器或父侦听器容器,如果源容器是一个子容器。 + +* `id`:侦听器 ID(或容器 Bean 名称)。 + +* `timeSinceLastPoll`:容器上次调用`poll()`之前的时间。 + +* `topicPartitions`:在事件生成时容器被分配的主题和分区。 + +* `consumer`:对 Kafka`Consumer`对象的引用。例如,如果先前调用了消费者的`pause()`方法,则在接收到事件时可以`resume()`。 + +* `paused`:容器当前是否暂停。有关更多信息,请参见[暂停和恢复监听器容器](#pause-resume)。 + +`ConsumerPausedEvent`、`ConsumerResumedEvent`和`ConsumerStopping`事件具有以下属性: + +* `source`:发布事件的侦听器容器实例。 + +* `container`:侦听器容器或父侦听器容器,如果源容器是一个子容器。 + +* `partitions`:涉及`TopicPartition`实例。 + +`ConsumerPartitionPausedEvent`,`ConsumerPartitionResumedEvent`事件具有以下属性: + +* `source`:发布事件的侦听器容器实例。 + +* `container`:侦听器容器或父侦听器容器,如果源容器是一个子容器。 + +* `partition`:涉及`TopicPartition`实例。 + +`ConsumerStartingEvent`,`ConsumerStartingEvent`,`ConsumerFailedToStartEvent`,`ConsumerStoppedEvent`和`ContainerStoppedEvent`事件具有以下属性: + +* `source`:发布事件的侦听器容器实例。 + +* `container`:侦听器容器或父侦听器容器,如果源容器是一个子容器。 + +所有容器(无论是子容器还是父容器)发布`ContainerStoppedEvent`。对于父容器,源属性和容器属性是相同的。 + +此外,`ConsumerStoppedEvent`还具有以下附加属性: + +* `reason` + + * `NORMAL`-消费者正常停止(容器已停止)。 + + * `ERROR`-a`java.lang.Error`被抛出。 + + * `FENCED`-对事务生成器进行了保护,并且`stopContainerWhenFenced`容器属性是`true`。 + + * `AUTH`-一个`AuthenticationException`或`AuthorizationException`被抛出,并且`authExceptionRetryInterval`未配置。 + + * `NO_OFFSET`-对于一个分区没有偏移量,并且`auto.offset.reset`策略是`none`。 + +你可以使用此事件在出现以下情况后重新启动容器: + +``` +if (event.getReason.equals(Reason.FENCED)) { + event.getSource(MessageListenerContainer.class).start(); +} +``` + +##### 检测空闲和无响应的消费者 + +尽管效率很高,但异步用户的一个问题是检测它们何时空闲。如果一段时间内没有消息到达,你可能需要采取一些措施。 + +你可以将侦听器容器配置为在一段时间后没有消息传递的情况下发布`ListenerContainerIdleEvent`。当容器处于空闲状态时,每`idleEventInterval`毫秒就会发布一个事件。 + +要配置此功能,请在容器上设置`idleEventInterval`。下面的示例展示了如何做到这一点: + +``` +@Bean +public KafkaMessageListenerContainer(ConsumerFactory consumerFactory) { + ContainerProperties containerProps = new ContainerProperties("topic1", "topic2"); + ... + containerProps.setIdleEventInterval(60000L); + ... + KafkaMessageListenerContainer container = new KafKaMessageListenerContainer<>(...); + return container; +} +``` + +下面的示例展示了如何为`@KafkaListener`设置`idleEventInterval`: + +``` +@Bean +public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + ... + factory.getContainerProperties().setIdleEventInterval(60000L); + ... + return factory; +} +``` + +在每种情况下,当容器处于空闲状态时,每分钟都会发布一次事件。 + +如果由于某种原因,使用者`poll()`方法没有退出,则不会接收到任何消息,也不能生成空闲事件(这是`kafka-clients`的早期版本在无法访问代理时的一个问题)。在这种情况下,如果轮询不在`3x`内返回`pollTimeout`属性,则容器将发布`NonResponsiveConsumerEvent`。默认情况下,该检查在每个容器中每 30 秒执行一次。在配置侦听器容器时,可以通过在`ContainerProperties`中设置`monitorInterval`(默认 30 秒)和`noPollThreshold`(默认 3.0)属性来修改此行为。`noPollThreshold`应该大于`1.0`,以避免由于比赛条件而导致虚假事件。接收这样的事件可以让你停止容器,从而唤醒消费者,使其可以停止。 + +从版本 2.6.2 开始,如果容器已经发布了`ListenerContainerIdleEvent`,那么当随后接收到一条记录时,它将发布`ListenerContainerNoLongerIdleEvent`。 + +##### 事件消费 + +你可以通过实现`ApplicationListener`来捕获这些事件——或者是一个普通的侦听器,或者是一个缩小到只接收这个特定事件的侦听器。还可以使用 Spring Framework4.2 中介绍的`@EventListener`。 + +下一个示例将`@KafkaListener`和`@EventListener`合并为一个类。你应该理解,应用程序侦听器获取所有容器的事件,因此,如果你想根据哪个容器空闲来采取特定的操作,可能需要检查侦听器 ID。你也可以为此目的使用`@EventListener``condition`。 + +有关事件属性的信息,请参见[应用程序事件](#events)。 + +该事件通常发布在使用者线程上,因此与`Consumer`对象交互是安全的。 + +下面的示例同时使用`@KafkaListener`和`@EventListener`: + +``` +public class Listener { + + @KafkaListener(id = "qux", topics = "annotated") + public void listen4(@Payload String foo, Acknowledgment ack) { + ... + } + + @EventListener(condition = "event.listenerId.startsWith('qux-')") + public void eventHandler(ListenerContainerIdleEvent event) { + ... + } + +} +``` + +| |事件侦听器看到所有容器的事件。
因此,在前面的示例中,我们根据侦听器 ID 缩小了接收到的事件的范围,
因为为`@KafkaListener`创建的容器支持并发性,实际的容器名为`id-n`,其中`n`是每个实例的唯一值,以支持并发性。
这就是为什么我们在条件中使用`startsWith`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果希望使用空闲事件停止 Lister 容器,则不应在调用侦听器的线程上调用`container.stop()`。
这样做会导致延迟和不必要的日志消息。相反,
,你应该将事件传递给另一个线程,该线程可以停止容器。
此外,如果容器实例是一个子容器,则不应该`stop()`容器实例。
你应该停止并发容器。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 空闲时的当前位置 + +请注意,你可以通过在侦听器中实现`ConsumerSeekAware`来获得检测到空闲时的当前位置。见`onIdleContainer()`in[寻求一种特定的抵消](#seek)。 + +#### 4.1.7.主题/分区初始偏移 + +有几种方法可以设置分区的初始偏移量。 + +当手动分配分区时,可以在配置的`TopicPartitionOffset`参数中设置初始偏移量(如果需要)(参见[消息监听器容器](#message-listener-container))。你还可以在任何时候寻求特定的偏移。 + +在使用分组管理时,代理将分配分区: + +* 对于新的`group.id`,初始偏移量由`auto.offset.reset`消费者属性(`earliest`或`latest`)确定。 + +* 对于现有的组 ID,初始偏移量是该组 ID 的当前偏移量。但是,你可以在初始化期间(或之后的任何时间)寻求特定的偏移量。 + +#### 4.1.8.寻求一种特定的抵消 + +为了进行查找,侦听器必须实现`ConsumerSeekAware`,它具有以下方法: + +``` +void registerSeekCallback(ConsumerSeekCallback callback); + +void onPartitionsAssigned(Map assignments, ConsumerSeekCallback callback); + +void onPartitionsRevoked(Collection partitions) + +void onIdleContainer(Map assignments, ConsumerSeekCallback callback); +``` + +在启动容器时和分配分区时调用`registerSeekCallback`。在初始化后的某个任意时间进行查找时,应该使用此回调。你应该保存对回调的引用。如果在多个容器(或`ConcurrentMessageListenerContainer`)中使用相同的侦听器,则应将回调存储在`ThreadLocal`或由侦听器`Thread`键控的其他结构中。 + +当使用组管理时,分配分区时调用`onPartitionsAssigned`。例如,你可以使用这个方法,通过调用回调来设置分区的初始偏移量。你还可以使用此方法将此线程的回调与分配的分区关联起来(请参见下面的示例)。你必须使用回调参数,而不是传递到`registerSeekCallback`的参数。从版本 2.5.5 开始,即使使用[手动分区分配](#manual-assignment),也会调用此方法。 + +`onPartitionsRevoked`在停止容器或 Kafka 撤销分配时调用。你应该放弃这个线程的回调,并删除与已撤销分区的任何关联。 + +回调有以下方法: + +``` +void seek(String topic, int partition, long offset); + +void seekToBeginning(String topic, int partition); + +void seekToBeginning(Collection= partitions); + +void seekToEnd(String topic, int partition); + +void seekToEnd(Collection= partitions); + +void seekRelative(String topic, int partition, long offset, boolean toCurrent); + +void seekToTimestamp(String topic, int partition, long timestamp); + +void seekToTimestamp(Collection topicPartitions, long timestamp); +``` + +`seekRelative`在版本 2.3 中被添加,以执行相对查找。 + +* `offset`负且`toCurrent``false`-相对于分区的末尾进行查找。 + +* `offset`正和`toCurrent``false`-相对于分区的开始进行查找。 + +* `offset`负数和`toCurrent``true`-相对于当前位置进行查找(倒带)。 + +* `offset`正和`toCurrent``true`-相对于当前位置进行搜索(快进)。 + +在版本 2.3 中还添加了`seekToTimestamp`方法。 + +| |当在`onIdleContainer`或`onPartitionsAssigned`方法中为多个分区寻求相同的时间戳时,第二种方法是首选的,因为在对消费者的`offsetsForTimes`方法的一次调用中,为时间戳查找偏移量更有效。当从其他位置调用
时,容器将收集所有的时间戳查找请求,并对`offsetsForTimes`进行一次调用。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当检测到空闲容器时,还可以从`onIdleContainer()`执行查找操作。有关如何启用空闲容器检测,请参见[检测空闲和无响应的消费者](#idle-containers)。 + +| |接受集合的`seekToBeginning`方法很有用,例如,在处理压缩主题时,并且在每次启动应用程序时都希望查找到开头:| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +public class MyListener implements ConsumerSeekAware { + +... + + @Override + public void onPartitionsAssigned(Map assignments, ConsumerSeekCallback callback) { + callback.seekToBeginning(assignments.keySet()); + } + +} +``` + +要在运行时任意查找,请使用来自`registerSeekCallback`的回调引用来查找合适的线程。 + +下面是一个简单的 Spring 启动应用程序,它演示了如何使用回调;它向主题发送 10 条记录;在控制台中点击``,将导致所有分区从头开始查找。 + +``` +@SpringBootApplication +public class SeekExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SeekExampleApplication.class, args); + } + + @Bean + public ApplicationRunner runner(Listener listener, KafkaTemplate template) { + return args -> { + IntStream.range(0, 10).forEach(i -> template.send( + new ProducerRecord<>("seekExample", i % 3, "foo", "bar"))); + while (true) { + System.in.read(); + listener.seekToStart(); + } + }; + } + + @Bean + public NewTopic topic() { + return new NewTopic("seekExample", 3, (short) 1); + } + +} + +@Component +class Listener implements ConsumerSeekAware { + + private static final Logger logger = LoggerFactory.getLogger(Listener.class); + + private final ThreadLocal callbackForThread = new ThreadLocal<>(); + + private final Map callbacks = new ConcurrentHashMap<>(); + + @Override + public void registerSeekCallback(ConsumerSeekCallback callback) { + this.callbackForThread.set(callback); + } + + @Override + public void onPartitionsAssigned(Map assignments, ConsumerSeekCallback callback) { + assignments.keySet().forEach(tp -> this.callbacks.put(tp, this.callbackForThread.get())); + } + + @Override + public void onPartitionsRevoked(Collection partitions) { + partitions.forEach(tp -> this.callbacks.remove(tp)); + this.callbackForThread.remove(); + } + + @Override + public void onIdleContainer(Map assignments, ConsumerSeekCallback callback) { + } + + @KafkaListener(id = "seekExample", topics = "seekExample", concurrency = "3") + public void listen(ConsumerRecord in) { + logger.info(in.toString()); + } + + public void seekToStart() { + this.callbacks.forEach((tp, callback) -> callback.seekToBeginning(tp.topic(), tp.partition())); + } + +} +``` + +为了使事情变得更简单,版本 2.3 添加了`AbstractConsumerSeekAware`类,该类跟踪主题/分区将使用哪个回调。下面的示例展示了每次容器空闲时,如何查找每个分区中处理的最后一条记录。它还有一些方法,允许任意外部调用通过一条记录来倒带分区。 + +``` +public class SeekToLastOnIdleListener extends AbstractConsumerSeekAware { + + @KafkaListener(id = "seekOnIdle", topics = "seekOnIdle") + public void listen(String in) { + ... + } + + @Override + public void onIdleContainer(Map assignments, + ConsumerSeekCallback callback) { + + assignments.keySet().forEach(tp -> callback.seekRelative(tp.topic(), tp.partition(), -1, true)); + } + + /** + * Rewind all partitions one record. + */ + public void rewindAllOneRecord() { + getSeekCallbacks() + .forEach((tp, callback) -> + callback.seekRelative(tp.topic(), tp.partition(), -1, true)); + } + + /** + * Rewind one partition one record. + */ + public void rewindOnePartitionOneRecord(String topic, int partition) { + getSeekCallbackFor(new org.apache.kafka.common.TopicPartition(topic, partition)) + .seekRelative(topic, partition, -1, true); + } + +} +``` + +版本 2.6 为抽象类添加了方便的方法: + +* `seekToBeginning()`-将所有分配的分区查找到开始位置 + +* `seekToEnd()`-将所有分配的分区查找到末尾 + +* `seekToTimestamp(long time)`-将所有分配的分区查找到由该时间戳表示的偏移量。 + +示例: + +``` +public class MyListener extends AbstractConsumerSeekAware { + + @KafkaListener(...) + void listn(...) { + ... + } +} + +public class SomeOtherBean { + + MyListener listener; + + ... + + void someMethod() { + this.listener.seekToTimestamp(System.currentTimeMillis - 60_000); + } + +} +``` + +#### 4.1.9.集装箱工厂 + +正如[`@KafkaListener`注释](#kafka-listener-annotation)中所讨论的,`ConcurrentKafkaListenerContainerFactory`用于为带注释的方法创建容器。 + +从版本 2.2 开始,你可以使用相同的工厂来创建任何`ConcurrentMessageListenerContainer`。如果你希望创建几个具有类似属性的容器,或者希望使用一些外部配置的工厂,例如 Spring Boot Auto-Configuration 提供的容器,那么这可能会很有用。创建容器后,你可以进一步修改其属性,其中许多属性是通过使用`container.getContainerProperties()`设置的。以下示例配置`ConcurrentMessageListenerContainer`: + +``` +@Bean +public ConcurrentMessageListenerContainer( + ConcurrentKafkaListenerContainerFactory factory) { + + ConcurrentMessageListenerContainer container = + factory.createContainer("topic1", "topic2"); + container.setMessageListener(m -> { ... } ); + return container; +} +``` + +| |以这种方式创建的容器不会被添加到端点注册中心。
它们应该被创建为`@Bean`定义,以便它们在应用程序上下文中注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.3.4 开始,你可以向工厂添加`ContainerCustomizer`,以便在创建和配置每个容器之后进一步配置它。 + +``` +@Bean +public KafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + ... + factory.setContainerCustomizer(container -> { /* customize the container */ }); + return factory; +} +``` + +#### 4.1.10.螺纹安全 + +当使用并发消息侦听器容器时,将在所有使用者线程上调用单个侦听器实例。因此,侦听器需要是线程安全的,最好是使用无状态侦听器。如果不可能使你的侦听器线程安全,或者添加同步将大大降低添加并发性的好处,那么你可以使用以下几种技术中的一种: + +* 使用`n`容器和`concurrency=1`原型作用域`MessageListener` Bean,以便每个容器获得自己的实例(当使用`@KafkaListener`时,这是不可能的)。 + +* 将状态保持在`ThreadLocal`实例中。 + +* 将 singleton 侦听器委托给在`SimpleThreadScope`(或类似的作用域)中声明的 Bean。 + +为了便于清理线程状态(对于前面列表中的第二个和第三个项目),从版本 2.2 开始,侦听器容器在每个线程退出时发布`ConsumerStoppedEvent`。你可以使用`ApplicationListener`或`@EventListener`方法来使用这些事件,以从作用域中删除`ThreadLocal`实例或`remove()`线程作用域 bean。请注意,`SimpleThreadScope`不会销毁具有销毁接口的 bean(例如`DisposableBean`),因此你应该`destroy()`自己的实例。 + +| |默认情况下,应用程序上下文的事件多播器调用调用调用线程上的事件侦听器。
如果你将多播器更改为使用异步执行器,则线程清理将无效。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.1.11.监测 + +##### 监视侦听器性能 + +从版本 2.3 开始,如果在类路径上检测到`Micrometer`,并且在应用程序上下文中存在一个`MeterRegistry`,则侦听器容器将自动为侦听器创建和更新微米计`Timer`s。可以通过将`ContainerProperty``micrometerEnabled`设置为`false`来禁用计时器。 + +两个计时器被维护-一个用于对听者的成功调用,另一个用于失败调用。 + +计时器名为`spring.kafka.listener`,并具有以下标记: + +* `name`:(容器 Bean 名称) + +* `result`:`success`或`failure` + +* `exception`:`none`或`ListenerExecutionFailedException` + +你可以使用`ContainerProperties``micrometerTags`属性添加其他标记。 + +| |使用并发容器,为每个线程创建计时器,`name`标记后缀为`-n`,其中 n 为`0`到`concurrency-1`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------| + +##### 监控 Kafkatemplate 性能 + +从版本 2.5 开始,如果在类路径上检测到`Micrometer`,并且在应用程序上下文中存在一个`MeterRegistry`,则模板将自动为发送操作创建和更新 Micrometer`Timer`s。可以通过将模板的`micrometerEnabled`属性设置为`false`来禁用计时器。 + +两个计时器被维护-一个用于对听者的成功调用,另一个用于失败调用。 + +计时器名为`spring.kafka.template`,并具有以下标记: + +* `name`:(模板 Bean 名称) + +* `result`:`success`或`failure` + +* `exception`:`none`或失败的异常类名 + +你可以使用模板的`micrometerTags`属性添加其他标记。 + +##### 千分尺本机度量 + +从版本 2.5 开始,该框架提供[工厂监听器](#factory-listeners)来管理微米计`KafkaClientMetrics`实例,无论何时创建和关闭生产者和消费者。 + +要启用此功能,只需将侦听器添加到你的生产者和消费者工厂: + +``` +@Bean +public ConsumerFactory myConsumerFactory() { + Map configs = consumerConfigs(); + ... + DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(configs); + ... + cf.addListener(new MicrometerConsumerListener(meterRegistry(), + Collections.singletonList(new ImmutableTag("customTag", "customTagValue")))); + ... + return cf; +} + +@Bean +public ProducerFactory myProducerFactory() { + Map configs = producerConfigs(); + configs.put(ProducerConfig.CLIENT_ID_CONFIG, "myClientId"); + ... + DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(configs); + ... + pf.addListener(new MicrometerProducerListener(meterRegistry(), + Collections.singletonList(new ImmutableTag("customTag", "customTagValue")))); + ... + return pf; +} +``` + +传递给侦听器的消费者/生产者`id`被添加到计价器的标记中,标记名`spring.id`。 + +获取一个 Kafka 度量的示例 + +``` +double count = this.meterRegistry.get("kafka.producer.node.incoming.byte.total") + .tag("customTag", "customTagValue") + .tag("spring.id", "myProducerFactory.myClientId-1") + .functionCounter() + .count() +``` + +为`StreamsBuilderFactoryBean`提供了类似的侦听器-参见[Kafkastreams 测微仪支持](#streams-micrometer)。 + +#### 4.1.12.交易 + +本节描述了 Spring for Apache Kafka 如何支持事务。 + +##### 概述 + +0.11.0.0 客户端库增加了对事务的支持。 Spring For Apache Kafka 通过以下方式增加了支持: + +* `KafkaTransactionManager`:用于正常的 Spring 事务支持(`@Transactional`,`TransactionTemplate`等)。 + +* 事务性`KafkaMessageListenerContainer` + +* 具有`KafkaTemplate`的本地事务 + +* 与其他事务管理器的事务同步 + +通过提供`DefaultKafkaProducerFactory`和`transactionIdPrefix`来启用事务。在这种情况下,工厂维护事务生产者的缓存,而不是管理单个共享的`Producer`。当用户在生成器上调用`close()`时,它将返回到缓存中进行重用,而不是实际关闭。每个生成器的`transactional.id`属性是`transactionIdPrefix`+`n`,其中`n`以`n`开头,并为每个新生成器递增,除非事务是由具有基于记录的侦听器的侦听器容器启动的。在这种情况下,`transactional.id`是`...`。这是正确支持击剑僵尸,[如此处所述](https://www.confluent.io/blog/transactions-apache-kafka/)。在 1.3.7、2.0.6、2.1.10 和 2.2.0 版本中添加了这种新行为。如果希望恢复到以前的行为,可以将`DefaultKafkaProducerFactory`上的`producerPerConsumerPartition`属性设置为`false`。 + +| |虽然批处理侦听器支持事务,但默认情况下,不支持僵尸围栏,因为一个批处理可能包含来自多个主题或分区的记录。
但是,从版本 2.3.2 开始,如果你将容器属性`subBatchPerPartition`设置为真,则支持僵尸围栏。,在这种情况下,
,从上次投票中收到的每个分区都会调用批处理侦听器一次,就好像每个轮询只返回单个分区的记录一样。
当`EOSMode.ALPHA`启用事务时,这是`true`自版本 2.5 以来默认的`true`;如果你正在使用事务但不担心僵尸围栏,则将其设置为`false`。
还请参见[一次语义学](#exactly-once)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另见[`transactionIdPrefix`](#transaction-id-prefix)。 + +有了 Spring boot,只需要设置`spring.kafka.producer.transaction-id-prefix`属性-boot 将自动配置一个`KafkaTransactionManager` Bean 并将其连接到侦听器容器中。 + +| |从版本 2.5.8 开始,你现在可以在生产者工厂上配置`maxAge`属性,
这在使用事务生产者时很有用,这些生产者可能为代理的`transactional.id.expiration.ms`闲置。,
使用当前的`kafka-clients`,这可能会导致`ProducerFencedException`而不进行再平衡。
通过将`maxAge`设置为`transactional.id.expiration.ms`小于`transactional.id.expiration.ms`,工厂将刷新生产者,如果它已经超过了最大年龄。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用`KafkaTransactionManager` + +`KafkaTransactionManager`是 Spring 框架`PlatformTransactionManager`的一个实现。为生产厂在其构造中的应用提供了参考.如果你提供了一个自定义的生产者工厂,那么它必须支持事务。见`ProducerFactory.transactionCapable()`。 + +你可以使用具有正常 Spring 事务支持的`KafkaTransactionManager`(`@Transactional`、`TransactionTemplate`等)。如果事务是活动的,则在事务范围内执行的任何`KafkaTemplate`操作都使用事务的`Producer`。Manager 根据成功或失败提交或回滚事务。你必须配置`KafkaTemplate`以使用与事务管理器相同的`ProducerFactory`。 + +##### 事务同步 + +本节引用仅生产者事务(不是由侦听器容器启动的事务);有关在容器启动事务时链接事务的信息,请参见[使用消费者发起的交易](#container-transaction-manager)。 + +如果希望将记录发送到 Kafka 并执行某些数据库更新,则可以使用普通的 Spring 事务管理,例如,使用`DataSourceTransactionManager`。 + +``` +@Transactional +public void process(List things) { + things.forEach(thing -> this.kafkaTemplate.send("topic", thing)); + updateDb(things); +} +``` + +`@Transactional`注释的拦截器将启动事务,`KafkaTemplate`将与该事务管理器同步一个事务;每个发送都将参与该事务。当该方法退出时,数据库事务将提交,然后是 Kafka 事务。如果希望以相反的顺序执行提交(首先是 Kafka),请使用嵌套的`@Transactional`方法,外部方法配置为使用`DataSourceTransactionManager`,内部方法配置为使用`KafkaTransactionManager`。 + +有关在 Kafka-first 或 DB-first 配置中同步 JDBC 和 Kafka 事务的应用程序示例,请参见[[ex-jdbc-sync]]。 + +| |从版本 2.5.17、2.6.12、2.7.9 和 2.8.0 开始,如果在同步事务上提交失败(在主事务提交之后),异常将被抛给调用者,
以前,这一点被静默忽略(在调试时记录),
应用程序应该采取补救措施,如果有必要,对已提交的主要事务进行补偿。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用消费者发起的事务 + +从版本 2.7 开始,`ChainedKafkaTransactionManager`现在已被弃用;有关更多信息,请参见 Javadocs 的超类`ChainedTransactionManager`。相反,在容器中使用`KafkaTransactionManager`来启动 Kafka 事务,并用`@Transactional`注释侦听器方法来启动另一个事务。 + +有关链接 JDBC 和 Kafka 事务的示例应用程序,请参见[[ex-jdbc-sync]]。 + +##### `KafkaTemplate`本地事务 + +你可以使用`KafkaTemplate`在本地事务中执行一系列操作。下面的示例展示了如何做到这一点: + +``` +boolean result = template.executeInTransaction(t -> { + t.sendDefault("thing1", "thing2"); + t.sendDefault("cat", "hat"); + return true; +}); +``` + +回调中的参数是模板本身(`this`)。如果回调正常退出,则提交事务。如果抛出异常,事务将被回滚。 + +| |如果进程中有`KafkaTransactionManager`(或同步)事务,则不使用它。
而是使用新的“嵌套”事务。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------| + +##### `transactionIdPrefix` + +正如[概述](#transactions)中提到的,生产者工厂配置了此属性,以构建生产者`transactional.id`属性。在使用`EOSMode.ALPHA`运行应用程序的多个实例时,当在监听器容器线程上生成记录时,在所有实例上都必须相同,以满足 fencing zombies(在概述中也提到了)的要求。但是,当使用侦听器容器启动的**不是**事务生成记录时,每个实例的前缀必须不同。版本 2.3 使此配置更简单,尤其是在 Spring 启动应用程序中。在以前的版本中,你必须创建两个生产者工厂和`KafkaTemplate`S-一个用于在侦听器容器线程上生成记录,另一个用于由`kafkaTemplate.executeInTransaction()`或由`@Transactional`方法上的事务拦截器启动的独立事务。 + +现在,你可以在`KafkaTemplate`和`KafkaTransactionManager`上覆盖工厂的`transactionalIdPrefix`。 + +当为侦听器容器使用事务管理器和模板时,通常将其默认设置为生产者工厂的属性。当使用`EOSMode.ALPHA`时,对于所有应用程序实例,该值应该是相同的。对于`EOSMode.BETA`,不再需要使用相同的`transactional.id`,即使对于消费者发起的事务也是如此;实际上,它必须在每个实例上都是唯一的,就像生产者发起的事务一样。对于由模板(或`@Transaction`的事务管理器)启动的事务,应该分别在模板和事务管理器上设置属性。此属性在每个应用程序实例上必须具有不同的值。 + +当使用`EOSMode.BETA`(代理版本 \>=2.5)时,此问题(`transactional.id`的不同规则)已被消除;请参见[一次语义学](#exactly-once)。 + +##### `KafkaTemplate`事务性和非事务性发布 + +通常,当`KafkaTemplate`是事务性的(配置了能够处理事务的生产者工厂)时,事务是必需的。事务可以通过`TransactionTemplate`、`@Transactional`方法启动,调用`executeInTransaction`,或者在配置`KafkaTransactionManager`时通过侦听器容器启动。在事务范围之外使用模板的任何尝试都会导致模板抛出`IllegalStateException`。从版本 2.4.3 开始,你可以将模板的`allowNonTransactional`属性设置为`true`。在这种情况下,通过调用`ProducerFactory`的`createNonTransactionalProducer()`方法,模板将允许操作在没有事务的情况下运行;生产者将被缓存或线程绑定,以进行正常的重用。参见[使用`DefaultKafkaProducerFactory`](#producer-factory)。 + +##### 具有批处理侦听器的事务 + +当侦听器在使用事务时失败时,将调用`AfterRollbackProcessor`在回滚发生后采取一些操作。当在记录侦听器中使用默认的`AfterRollbackProcessor`时,将执行查找,以便重新交付失败的记录。但是,对于批处理侦听器,整个批处理将被重新交付,因为框架不知道批处理中的哪个记录失败了。有关更多信息,请参见[后回滚处理器](#after-rollback)。 + +在使用批处理侦听器时,版本 2.4.2 引入了一种替代机制来处理批处理过程中的故障;`BatchToRecordAdapter`。当将`batchListener`设置为 true 的容器工厂配置为`BatchToRecordAdapter`时,侦听器一次用一条记录调用。这允许在批处理中进行错误处理,同时仍然可以根据异常类型停止处理整个批处理。提供了一个默认的`BatchToRecordAdapter`,可以使用标准的`ConsumerRecordRecoverer`进行配置,例如`DeadLetterPublishingRecoverer`。下面的测试用例配置片段演示了如何使用此功能: + +``` +public static class TestListener { + + final List values = new ArrayList<>(); + + @KafkaListener(id = "batchRecordAdapter", topics = "test") + public void listen(String data) { + values.add(data); + if ("bar".equals(data)) { + throw new RuntimeException("reject partial"); + } + } + +} + +@Configuration +@EnableKafka +public static class Config { + + ConsumerRecord failed; + + @Bean + public TestListener test() { + return new TestListener(); + } + + @Bean + public ConsumerFactory consumerFactory() { + return mock(ConsumerFactory.class); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); + factory.setConsumerFactory(consumerFactory()); + factory.setBatchListener(true); + factory.setBatchToRecordAdapter(new DefaultBatchToRecordAdapter<>((record, ex) -> { + this.failed = record; + })); + return factory; + } + +} +``` + +#### 4.1.13.一次语义学 + +你可以为侦听器容器提供一个`KafkaAwareTransactionManager`实例。当这样配置时,容器在调用侦听器之前启动一个事务。侦听器执行的任何`KafkaTemplate`操作都参与事务。如果侦听器在使用`BatchMessageListener`时成功地处理该记录(或多个记录),则容器在事务管理器提交事务之前通过使用`producer.sendOffsetsToTransaction()`向事务发送偏移量。如果侦听器抛出异常,事务将被回滚,使用者将被重新定位,以便在下一次投票时可以检索回滚记录。有关更多信息和处理多次失败的记录,请参见[后回滚处理器](#after-rollback)。 + +使用事务可以实现精确的一次语义(EOS)。 + +这意味着,对于`read→process-write`序列,可以保证**序列**恰好完成一次。(read 和 process 至少有一次语义)。 + +Spring 对于 Apache Kafka 版本 2.5 及更高版本,支持两种 EOS 模式: + +* `ALPHA`-`V1`的别名(不推荐) + +* `BETA`-`V2`的别名(不推荐) + +* `V1`-AKA`transactional.id`击剑(自版本 0.11.0.0 起) + +* `V2`-AKAfetch-offset-request fencing(自版本 2.5 起) + +在模式`V1`下,如果启动了另一个具有相同`transactional.id`的实例,那么生产者将被“隔离”。 Spring 通过对每个`group.id/topic/partition`使用`Producer`来管理这一点;当重新平衡发生时,新实例将使用相同的`transactional.id`,并且旧的生产者将被隔离。 + +对于模式`V2`,不需要为每个`group.id/topic/partition`都有一个生产者,因为消费者元数据与偏移量一起发送到事务,并且代理可以确定生产者是否使用该信息来保护生产者。 + +从版本 2.6 开始,默认的`EOSMode`是`V2`。 + +要将容器配置为使用模式`ALPHA`,请将容器属性`EOSMode`设置为`ALPHA`,以恢复到以前的行为。 + +| |使用`V2`(默认),你的代理必须是版本 2.5 或更高版本;`kafka-clients`版本 3.0,生产者将不再返回`V1`;如果代理不支持`V2`,则抛出一个异常。
如果你的代理早于 2.5,必须将`EOSMode`设置为`V1`,将`DefaultKafkaProducerFactory``producerPerConsumerPartition`设置为`true`,如果使用批处理侦听器,则应将`subBatchPerPartition`设置为`true`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当你的代理程序升级到 2.5 或更高时,你应该将模式切换到`V2`,但是生产者的数量将保持不变。然后可以对应用程序进行滚动升级,将`producerPerConsumerPartition`设置为`false`,以减少生成器的数量;还应该不再设置`subBatchPerPartition`容器属性。 + +如果你的代理已经是 2.5 或更新版本,则应该将`DefaultKafkaProducerFactory``producerPerConsumerPartition`属性设置为`false`,以减少所需的生产者数量。 + +| |当使用`EOSMode.V2`和`producerPerConsumerPartition=false`时,`transactional.id`在所有应用程序实例中都必须是唯一的。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------| + +当使用`V2`模式时,不再需要将`subBatchPerPartition`设置为`true`;当`EOSMode`为`V2`时,将默认为`false`。 + +有关更多信息,请参见[KIP-447](https://cwiki.apache.org/confluence/display/KAFKA/KIP-447%3A+Producer+scalability+for+exactly+once+semantics)。 + +`V1`和`V2`以前是`ALPHA`和`BETA`;它们已被更改以使框架与[KIP-732](https://cwiki.apache.org/confluence/display/KAFKA/KIP-732%3A+Deprecate+eos-alpha+and+replace+eos-beta+with+eos-v2)对齐。 + +#### 4.1.14.将 Spring bean 连接到生产者/消费者拦截器 + +Apache Kafka 提供了一种向生产者和消费者添加拦截器的机制。这些对象是由 Kafka 管理的,而不是 Spring,因此正常的 Spring 依赖注入不适用于在依赖的 Spring bean 中连接。但是,你可以使用拦截器`config()`方法手动连接这些依赖项。下面的 Spring 引导应用程序展示了如何通过覆盖 Boot 的默认工厂将一些依赖的 Bean 添加到配置属性中来实现这一点。 + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public ConsumerFactory kafkaConsumerFactory(SomeBean someBean) { + Map consumerProperties = new HashMap<>(); + // consumerProperties.put(..., ...) + // ... + consumerProperties.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, MyConsumerInterceptor.class.getName()); + consumerProperties.put("some.bean", someBean); + return new DefaultKafkaConsumerFactory<>(consumerProperties); + } + + @Bean + public ProducerFactory kafkaProducerFactory(SomeBean someBean) { + Map producerProperties = new HashMap<>(); + // producerProperties.put(..., ...) + // ... + Map producerProperties = properties.buildProducerProperties(); + producerProperties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, MyProducerInterceptor.class.getName()); + producerProperties.put("some.bean", someBean); + DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>(producerProperties); + return factory; + } + + @Bean + public SomeBean someBean() { + return new SomeBean(); + } + + @KafkaListener(id = "kgk897", topics = "kgh897") + public void listen(String in) { + System.out.println("Received " + in); + } + + @Bean + public ApplicationRunner runner(KafkaTemplate template) { + return args -> template.send("kgh897", "test"); + } + + @Bean + public NewTopic kRequests() { + return TopicBuilder.name("kgh897") + .partitions(1) + .replicas(1) + .build(); + } + +} +``` + +``` +public class SomeBean { + + public void someMethod(String what) { + System.out.println(what + " in my foo bean"); + } + +} +``` + +``` +public class MyProducerInterceptor implements ProducerInterceptor { + + private SomeBean bean; + + @Override + public void configure(Map configs) { + this.bean = (SomeBean) configs.get("some.bean"); + } + + @Override + public ProducerRecord onSend(ProducerRecord record) { + this.bean.someMethod("producer interceptor"); + return record; + } + + @Override + public void onAcknowledgement(RecordMetadata metadata, Exception exception) { + } + + @Override + public void close() { + } + +} +``` + +``` +public class MyConsumerInterceptor implements ConsumerInterceptor { + + private SomeBean bean; + + @Override + public void configure(Map configs) { + this.bean = (SomeBean) configs.get("some.bean"); + } + + @Override + public ConsumerRecords onConsume(ConsumerRecords records) { + this.bean.someMethod("consumer interceptor"); + return records; + } + + @Override + public void onCommit(Map offsets) { + } + + @Override + public void close() { + } + +} +``` + +结果: + +``` +producer interceptor in my foo bean +consumer interceptor in my foo bean +Received test +``` + +#### 4.1.15.暂停和恢复监听器容器 + +版本 2.1.3 为侦听器容器添加了`pause()`和`resume()`方法。以前,你可以在`ConsumerAwareMessageListener`中暂停一个消费者,并通过监听`ListenerContainerIdleEvent`来恢复它,该监听提供了对`Consumer`对象的访问。虽然可以通过使用事件侦听器在空闲容器中暂停使用者,但在某些情况下,这不是线程安全的,因为不能保证在使用者线程上调用事件侦听器。为了安全地暂停和恢复消费者,你应该在侦听器容器上使用`pause`和`resume`方法。a`pause()`在下一个`poll()`之前生效;a`resume()`在当前`poll()`返回之后生效。当容器暂停时,它将继续`poll()`使用者,从而避免在使用组管理时进行重新平衡,但它不会检索任何记录。有关更多信息,请参见 Kafka 文档。 + +从版本 2.1.5 开始,你可以调用`isPauseRequested()`来查看是否调用了`pause()`。但是,消费者可能还没有真正暂停。`isConsumerPaused()`如果所有`Consumer`实例都实际暂停,则返回 true。 + +此外(也是从 2.1.5 开始),`ConsumerPausedEvent`和`ConsumerResumedEvent`实例与容器一起作为`source`属性和`TopicPartition`属性所涉及的实例一起发布。 + +以下简单的 Spring 引导应用程序演示了如何使用容器注册中心获得对`@KafkaListener`方法的容器的引用,并暂停或恢复其使用者以及接收相应的事件: + +``` +@SpringBootApplication +public class Application implements ApplicationListener { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args).close(); + } + + @Override + public void onApplicationEvent(KafkaEvent event) { + System.out.println(event); + } + + @Bean + public ApplicationRunner runner(KafkaListenerEndpointRegistry registry, + KafkaTemplate template) { + return args -> { + template.send("pause.resume.topic", "thing1"); + Thread.sleep(10_000); + System.out.println("pausing"); + registry.getListenerContainer("pause.resume").pause(); + Thread.sleep(10_000); + template.send("pause.resume.topic", "thing2"); + Thread.sleep(10_000); + System.out.println("resuming"); + registry.getListenerContainer("pause.resume").resume(); + Thread.sleep(10_000); + }; + } + + @KafkaListener(id = "pause.resume", topics = "pause.resume.topic") + public void listen(String in) { + System.out.println(in); + } + + @Bean + public NewTopic topic() { + return TopicBuilder.name("pause.resume.topic") + .partitions(2) + .replicas(1) + .build(); + } + +} +``` + +下面的清单显示了前面示例的结果: + +``` +partitions assigned: [pause.resume.topic-1, pause.resume.topic-0] +thing1 +pausing +ConsumerPausedEvent [partitions=[pause.resume.topic-1, pause.resume.topic-0]] +resuming +ConsumerResumedEvent [partitions=[pause.resume.topic-1, pause.resume.topic-0]] +thing2 +``` + +#### 4.1.16.在侦听器容器上暂停和恢复分区 + +从版本 2.7 开始,你可以通过使用侦听器容器中的`pausePartition(TopicPartition topicPartition)`和`resumePartition(TopicPartition topicPartition)`方法暂停并恢复分配给该使用者的特定分区的使用。暂停和恢复分别发生在`poll()`之前和之后,类似于`pause()`和`resume()`方法。如果请求了该分区的暂停,`isPartitionPauseRequested()`方法将返回 true。如果该分区已有效地暂停,`isPartitionPaused()`方法将返回 true。 + +另外,由于版本 2.7`ConsumerPartitionPausedEvent`和`ConsumerPartitionResumedEvent`实例与容器一起作为`source`属性和`TopicPartition`实例发布。 + +#### 4.1.17.序列化、反序列化和消息转换 + +##### 概述 + +Apache Kafka 提供了用于序列化和反序列化记录值及其键的高级 API。它存在于带有一些内置实现的`org.apache.kafka.common.serialization.Serializer`和`org.apache.kafka.common.serialization.Deserializer`抽象中。同时,我们可以通过使用`Producer`或`Consumer`配置属性来指定序列化器和反序列化器类。下面的示例展示了如何做到这一点: + +``` +props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class); +props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); +... +props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); +props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); +``` + +对于更复杂或更特殊的情况,`KafkaConsumer`(因此,`KafkaProducer`)提供重载的构造函数来分别接受`Serializer`和`Deserializer`的实例。 + +当你使用这个 API 时,`DefaultKafkaProducerFactory`和`DefaultKafkaConsumerFactory`还提供属性(通过构造函数或 setter 方法)来将自定义`Serializer`和`Deserializer`实例注入到目标`Producer`或`Consumer`中。同样,你可以通过构造函数传入`Supplier`或`Supplier`实例-这些`Supplier`s 在创建每个`Producer`或`Consumer`时被调用。 + +##### 字符串序列化 + +自版本 2.5 以来, Spring for Apache Kafka 提供了`ToStringSerializer`和`ParseStringDeserializer`使用实体的字符串表示的类。它们依赖于方法`toString`和一些`Function`或`BiFunction`来解析字符串并填充实例的属性。通常,这会调用类上的一些静态方法,例如`parse`: + +``` +ToStringSerializer thingSerializer = new ToStringSerializer<>(); +//... +ParseStringDeserializer deserializer = new ParseStringDeserializer<>(Thing::parse); +``` + +默认情况下,`ToStringSerializer`被配置为传递关于记录`Headers`中的序列化实体的类型信息。你可以通过将`addTypeInfo`属性设置为 false 来禁用它。此信息可由接收端的`ParseStringDeserializer`使用。 + +* `ToStringSerializer.ADD_TYPE_INFO_HEADERS`(默认`true`):你可以将其设置为`false`,以在`ToStringSerializer`上禁用此功能(设置`addTypeInfo`属性)。 + +``` +ParseStringDeserializer deserializer = new ParseStringDeserializer<>((str, headers) -> { + byte[] header = headers.lastHeader(ToStringSerializer.VALUE_TYPE).value(); + String entityType = new String(header); + + if (entityType.contains("Thing")) { + return Thing.parse(str); + } + else { + // ...parsing logic + } +}); +``` + +可以配置用于将`String`转换为/from`byte[]`的`Charset`,缺省值为`UTF-8`。 + +可以使用`ConsumerConfig`属性以解析器方法的名称配置反序列化器: + +* `ParseStringDeserializer.KEY_PARSER` + +* `ParseStringDeserializer.VALUE_PARSER` + +属性必须包含类的完全限定名,后面跟着方法名,中间用一个句号`.`隔开。该方法必须是静态的,并且具有`(String, Headers)`或`(String)`的签名。 + +还提供了用于 Kafka 流的`ToFromStringSerde`。 + +##### JSON + +Spring 对于 Apache Kafka 还提供了基于 JacksonJSON 对象映射器的和实现。`JsonSerializer`允许将任何 Java 对象写为 JSON`byte[]`。`JsonDeserializer`需要一个额外的`Class targetType`参数,以允许将已使用的`byte[]`反序列化到正确的目标对象。下面的示例展示了如何创建`JsonDeserializer`: + +``` +JsonDeserializer thingDeserializer = new JsonDeserializer<>(Thing.class); +``` + +你可以使用`ObjectMapper`自定义`JsonSerializer`和`JsonDeserializer`。你还可以扩展它们,以在`configure(Map configs, boolean isKey)`方法中实现某些特定的配置逻辑。 + +从版本 2.3 开始,所有可感知 JSON 的组件都默认配置了`JacksonUtils.enhancedObjectMapper()`实例,该实例带有`MapperFeature.DEFAULT_VIEW_INCLUSION`和`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`禁用的功能。还为这样的实例提供了用于自定义数据类型的众所周知的模块,这样的 Java Time 和 Kotlin 支持。有关更多信息,请参见`JacksonUtils.enhancedObjectMapper()`Javadocs。该方法还将`org.springframework.kafka.support.JacksonMimeTypeModule`对象序列化的`org.springframework.kafka.support.JacksonMimeTypeModule`注册到普通字符串中,以实现网络上的平台间兼容性。一个`JacksonMimeTypeModule`可以在应用程序上下文中注册为一个 Bean 并且它将被自动配置为[ Spring boot`ObjectMapper`实例](https://DOCS. Spring.io/ Spring-boot/DOCS/current/reference/html/howto- Spring-mvc.html#howto-customize-the-Jackson-objectmapper)。 + +同样从版本 2.3 开始,`JsonDeserializer`提供了基于`TypeReference`的构造函数,以更好地处理目标泛型容器类型。 + +从版本 2.1 开始,你可以在记录`Headers`中传递类型信息,从而允许处理多个类型。此外,你可以通过使用以下 Kafka 属性来配置序列化器和反序列化器。如果分别为`KafkaConsumer`和`KafkaProducer`提供了`Deserializer`实例,则它们没有任何作用。 + +###### 配置属性 + +* `JsonSerializer.ADD_TYPE_INFO_HEADERS`(默认`true`):你可以将其设置为`false`,以在`JsonSerializer`上禁用此功能(设置`addTypeInfo`属性)。 + +* `JsonSerializer.TYPE_MAPPINGS`(默认`empty`):见[映射类型](#serdes-mapping-types)。 + +* `JsonDeserializer.USE_TYPE_INFO_HEADERS`(默认`true`):可以将其设置为`false`,以忽略序列化器设置的头。 + +* `JsonDeserializer.REMOVE_TYPE_INFO_HEADERS`(默认`true`):可以将其设置为`false`,以保留序列化器设置的标题。 + +* `JsonDeserializer.KEY_DEFAULT_TYPE`:如果不存在头信息,则用于对键进行反序列化的回退类型。 + +* `JsonDeserializer.VALUE_DEFAULT_TYPE`:如果不存在头信息,则用于反序列化值的回退类型。 + +* `JsonDeserializer.TRUSTED_PACKAGES`(默认`java.util`,`java.lang`):允许反序列化的以逗号分隔的包模式列表。`*`表示全部反序列化。 + +* `JsonDeserializer.TYPE_MAPPINGS`(默认`empty`):见[映射类型](#serdes-mapping-types)。 + +* `JsonDeserializer.KEY_TYPE_METHOD`(默认`empty`):见[使用方法确定类型](#serdes-type-methods)。 + +* `JsonDeserializer.VALUE_TYPE_METHOD`(默认`empty`):见[使用方法确定类型](#serdes-type-methods)。 + +从版本 2.2 开始,类型信息标头(如果由序列化器添加)将被反序列化器删除。可以通过将`removeTypeHeaders`属性设置为`false`,直接在反序列化器上或使用前面描述的配置属性,恢复到以前的行为。 + +另见[[tip-json]]。 + +| |从版本 2.8 开始,如果你按照[纲领性建设](#prog-json)中所示的编程方式构造序列化器或反序列化器,那么上述属性将由工厂应用,只要你没有显式地设置任何属性(使用`set*()`方法或使用 Fluent API)。
以前,在以编程方式创建时,配置属性从未被应用;如果直接显式地在对象上设置属性,情况仍然是这样。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 映射类型 + +从版本 2.2 开始,当使用 JSON 时,你现在可以通过使用前面列表中的属性来提供类型映射。以前,你必须在序列化器和反序列化器中自定义类型映射器。映射由`token:className`对的逗号分隔列表组成。在出站时,有效负载的类名被映射到相应的令牌。在入站时,类型头中的令牌将映射到相应的类名。 + +下面的示例创建了一组映射: + +``` +senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); +senderProps.put(JsonSerializer.TYPE_MAPPINGS, "cat:com.mycat.Cat, hat:com.myhat.hat"); +... +consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); +consumerProps.put(JsonDeSerializer.TYPE_MAPPINGS, "cat:com.yourcat.Cat, hat:com.yourhat.hat"); +``` + +| |相应的对象必须是兼容的。| +|---|---------------------------------------------| + +如果使用[Spring Boot](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-messaging.html#boot-features-kafka),则可以在`application.properties`(或 YAML)文件中提供这些属性。下面的示例展示了如何做到这一点: + +``` +spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer +spring.kafka.producer.properties.spring.json.type.mapping=cat:com.mycat.Cat,hat:com.myhat.Hat +``` + +| |对于更高级的配置(例如在序列化器和反序列化器中使用自定义的`ObjectMapper`),你应该使用接受预先构建的序列化器和反序列化器的生产者和消费者工厂构造函数。
下面的 Spring 引导示例覆盖了默认的工厂:

```
@Bean
public ConsumerFactory kafkaConsumerFactory(JsonDeserializer customValueDeserializer) {
Map properties = new HashMap<>();
// properties.put(..., ...)
// ...
return new DefaultKafkaConsumerFactory<>(properties,
new StringDeserializer(), customValueDeserializer);
}

@Bean
public ProducerFactory kafkaProducerFactory(JsonSerializer customValueSerializer) {

return new DefaultKafkaProducerFactory<>(properties.buildProducerProperties(),
new StringSerializer(), customValueSerializer);
}
```
还提供了设置器,作为使用这些构造函数的替代方案。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.2 开始,你可以通过使用其中一个重载的构造函数,显式地将反序列化器配置为使用所提供的目标类型,并忽略头中的类型信息,该构造函数具有布尔值`useHeadersIfPresent`(默认情况下是`true`)。下面的示例展示了如何做到这一点: + +``` +DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(props, + new IntegerDeserializer(), new JsonDeserializer<>(Cat1.class, false)); +``` + +###### 使用方法确定类型 + +从版本 2.5 开始,你现在可以通过属性配置反序列化器来调用一个方法来确定目标类型。如果存在,这将覆盖上面讨论的任何其他技术。如果数据是由不使用 Spring 序列化器的应用程序发布的,并且你需要根据数据或其他头来反序列化到不同类型,那么这可能是有用的。将这些属性设置为方法名-一个完全限定的类名,后面跟着方法名,中间隔一个句号`.`。方法必须声明为`public static`,具有三个签名之一`(String topic, byte[] data, Headers headers)`,`(byte[] data, Headers headers)`或`(byte[] data)`,并返回一个 Jackson`JavaType`。 + +* `JsonDeserializer.KEY_TYPE_METHOD`: `spring.json.key.type.method` + +* `JsonDeserializer.VALUE_TYPE_METHOD`: `spring.json.value.type.method` + +你可以使用任意标题或检查数据来确定类型。 + +例子 + +``` +JavaType thing1Type = TypeFactory.defaultInstance().constructType(Thing1.class); + +JavaType thing2Type = TypeFactory.defaultInstance().constructType(Thing2.class); + +public static JavaType thingOneOrThingTwo(byte[] data, Headers headers) { + // {"thisIsAFieldInThing1":"value", ... + if (data[21] == '1') { + return thing1Type; + } + else { + return thing2Type; + } +} +``` + +对于更复杂的数据检查,可以考虑使用`JsonPath`或类似的方法,但是,确定类型的测试越简单,过程就会越有效。 + +以下是以编程方式(在构造函数中向消费者工厂提供反序列化器时)创建反序列化器的示例: + +``` +JsonDeserializer deser = new JsonDeserializer<>() + .trustedPackages("*") + .typeResolver(SomeClass::thing1Thing2JavaTypeForTopic); + +... + +public static JavaType thing1Thing2JavaTypeForTopic(String topic, byte[] data, Headers headers) { + ... +} +``` + +###### 纲领性建设 + +从版本 2.3 开始,当以编程方式构建在生产者/消费者工厂中使用的序列化器/反序列化器时,你可以使用 Fluent API,这简化了配置。 + +``` +@Bean +public ProducerFactory pf() { + Map props = new HashMap<>(); + // props.put(..., ...) + // ... + DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(props, + new JsonSerializer() + .forKeys() + .noTypeInfo(), + new JsonSerializer() + .noTypeInfo()); + return pf; +} + +@Bean +public ConsumerFactory cf() { + Map props = new HashMap<>(); + // props.put(..., ...) + // ... + DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(props, + new JsonDeserializer<>(MyKeyType.class) + .forKeys() + .ignoreTypeHeaders(), + new JsonDeserializer<>(MyValueType.class) + .ignoreTypeHeaders()); + return cf; +} +``` + +要以编程方式提供类型映射,类似于[使用方法确定类型](#serdes-type-methods),请使用`typeFunction`属性。 + +例子 + +``` +JsonDeserializer deser = new JsonDeserializer<>() + .trustedPackages("*") + .typeFunction(MyUtils::thingOneOrThingTwo); +``` + +或者,只要不使用 Fluent API 配置属性,或者不使用`set*()`方法设置属性,工厂将使用配置属性配置序列化器/反序列化器;参见[配置属性](#serdes-json-config)。 + +##### 委托序列化器和反序列化器 + +###### 使用头文件 + +版本 2.3 引入了`DelegatingSerializer`和`DelegatingDeserializer`,它们允许使用不同的键和/或值类型来生成和消费记录。制作者必须将标题`DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR`设置为选择器值,用于选择要使用哪个序列化器作为该值,而`DelegatingSerializer.KEY_SERIALIZATION_SELECTOR`作为该键;如果找不到匹配项,则抛出`IllegalStateException`。 + +对于传入的记录,反序列化器使用相同的头来选择要使用的反序列化器;如果未找到匹配项或头不存在,则返回 RAW`byte[]`。 + +你可以通过构造函数将选择器的映射配置为`Serializer`/`Deserializer`,也可以通过 Kafka Producer/Consumer 属性配置它,使用键`DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG`和`DelegatingSerializer.KEY_SERIALIZATION_SELECTOR_CONFIG`。对于序列化器,producer 属性可以是`Map`,其中键是选择器,值是`Serializer`实例,序列化器`Class`或类名。该属性也可以是一串以逗号分隔的映射项,如下所示。 + +对于反序列化器,消费者属性可以是`Map`,其中键是选择器,值是`Deserializer`实例,反序列化器`Class`或类名。该属性也可以是一串以逗号分隔的映射项,如下所示。 + +要配置使用属性,请使用以下语法: + +``` +producerProps.put(DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG, + "thing1:com.example.MyThing1Serializer, thing2:com.example.MyThing2Serializer") + +consumerProps.put(DelegatingDeserializer.VALUE_SERIALIZATION_SELECTOR_CONFIG, + "thing1:com.example.MyThing1Deserializer, thing2:com.example.MyThing2Deserializer") +``` + +然后,制作人将`DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR`的标题设置为`thing1`或`thing2`。 + +这种技术支持向相同的主题(或不同的主题)发送不同的类型。 + +| |从版本 2.5.1 开始,如果类型(键或值)是`Serdes`(`Long`,`Integer`等)所支持的标准类型之一,则无需设置选择器标头。,相反,
,序列化器将把头设置为类型的类名。
不需要为这些类型配置序列化器或反序列化器,它们将被动态地创建(一次)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关将不同类型发送到不同主题的另一种技术,请参见[使用`RoutingKafkaTemplate`](#routing-template)。 + +###### 按类型分列 + +2.8 版引入了`DelegatingByTypeSerializer`。 + +``` +@Bean +public ProducerFactory producerFactory(Map config) { + return new DefaultKafkaProducerFactory<>(config, + null, new DelegatingByTypeSerializer(Map.of( + byte[].class, new ByteArraySerializer(), + Bytes.class, new BytesSerializer(), + String.class, new StringSerializer()))); +} +``` + +从版本 2.8.3 开始,你可以将序列化器配置为检查是否可以从目标对象分配映射键,这在委托序列化器可以序列化子类时很有用。在这种情况下,如果有可亲的匹配,则应该提供一个有序的`Map`,例如一个`LinkedHashMap`。 + +###### 按主题 + +从版本 2.8 开始,`DelegatingByTopicSerializer`和`DelegatingByTopicDeserializer`允许基于主题名称选择序列化器/反序列化器。regex`Pattern`s 用于查找要使用的实例。可以使用构造函数或通过属性(用逗号分隔的列表`pattern:serializer`)来配置映射。 + +``` +producerConfigs.put(DelegatingByTopicSerializer.VALUE_SERIALIZATION_TOPIC_CONFIG, + "topic[0-4]:" + ByteArraySerializer.class.getName() + + ", topic[5-9]:" + StringSerializer.class.getName()); +... +ConsumerConfigs.put(DelegatingByTopicDeserializer.VALUE_SERIALIZATION_TOPIC_CONFIG, + "topic[0-4]:" + ByteArrayDeserializer.class.getName() + + ", topic[5-9]:" + StringDeserializer.class.getName()); +``` + +使用`KEY_SERIALIZATION_TOPIC_CONFIG`作为键。 + +``` +@Bean +public ProducerFactory producerFactory(Map config) { + return new DefaultKafkaProducerFactory<>(config, + null, + new DelegatingByTopicSerializer(Map.of( + Pattern.compile("topic[0-4]"), new ByteArraySerializer(), + Pattern.compile("topic[5-9]"), new StringSerializer())), + new JsonSerializer()); // default +} +``` + +你可以使用`DelegatingByTopicSerialization.KEY_SERIALIZATION_TOPIC_DEFAULT`和`DelegatingByTopicSerialization.VALUE_SERIALIZATION_TOPIC_DEFAULT`指定一个默认的序列化器/反序列化器,当没有模式匹配时使用。 + +当设置为`false`时,另一个属性`DelegatingByTopicSerialization.CASE_SENSITIVE`(默认`true`)会使主题查找不区分大小写。 + +##### 重试反序列化器 + +`RetryingDeserializer`使用委托`Deserializer`和`RetryTemplate`来重试反序列化,当委托在反序列化过程中可能出现瞬时错误时,例如网络问题。 + +``` +ConsumerFactory cf = new DefaultKafkaConsumerFactory(myConsumerConfigs, + new RetryingDeserializer(myUnreliableKeyDeserializer, retryTemplate), + new RetryingDeserializer(myUnreliableValueDeserializer, retryTemplate)); +``` + +请参阅[spring-retry](https://github.com/spring-projects/spring-retry)项目,以配置带有重试策略、Back off 策略等的`RetryTemplate`项目。 + +##### Spring 消息传递消息转换 + +虽然`Serializer`和`Deserializer`API 从低级别的 Kafka`Consumer`和`Producer`透视图来看是非常简单和灵活的,但是在 Spring 消息传递级别,当使用`@KafkaListener`或[Spring Integration’s Apache Kafka Support](https://docs.spring.io/spring-integration/docs/current/reference/html/kafka.html#kafka)时,你可能需要更多的灵活性。为了让你能够轻松地转换`org.springframework.messaging.Message`, Spring for Apache Kafka 提供了一个`MessageConverter`的抽象,带有`MessagingMessageConverter`实现及其`JsonMessageConverter`(和子类)定制。你可以直接将`MessageConverter`注入`KafkaTemplate`实例中,并使用`AbstractKafkaListenerContainerFactory` Bean 对`@KafkaListener.containerFactory()`属性的定义。下面的示例展示了如何做到这一点: + +``` +@Bean +public KafkaListenerContainerFactory kafkaJsonListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + factory.setMessageConverter(new JsonMessageConverter()); + return factory; +} +... +@KafkaListener(topics = "jsonData", + containerFactory = "kafkaJsonListenerContainerFactory") +public void jsonListener(Cat cat) { +... +} +``` + +在使用 Spring 引导时,只需将转换器定义为`@Bean`,并且 Spring 引导自动配置将其连接到自动配置的模板和容器工厂。 + +当使用`@KafkaListener`时,将向消息转换器提供参数类型,以帮助进行转换。 + +| |只有在方法级别声明`@KafkaListener`注释时,这种类型推断才能实现。
具有类级别的`@KafkaListener`,有效负载类型用于选择要调用哪个`@KafkaHandler`方法,因此在选择方法之前必须已经进行了转换。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在消费者方面,你可以配置`JsonMessageConverter`;它可以处理`ConsumerRecord`类型的`byte[]`、`Bytes`和`String`值,因此应该与`ByteArrayDeserializer`一起使用,`BytesDeserializer`或`StringDeserializer`.
(`byte[]`和`Bytes`效率更高,因为它们避免了不必要的`byte[]`到`String`转换)。
还可以配置与解序器对应的`JsonMessageConverter`的特定子类,如果你愿意的话
R=“2031”/>在生产者端,r=“”“”2038“/>>>r=“><2038">>>><2038">>>>>>>使用`byte[]`或`Bytes`更有效,因为它们避免了`String`到`byte[]`的转换。

为了方便起见,从版本 2.3 开始,该框架还提供了`StringOrBytesSerializer`,它可以序列化所有三个值类型,以便可以与任何消息转换器一起使用。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.7.1 开始,可以将消息有效负载转换委托给`spring-messaging``SmartMessageConverter`;例如,这允许基于`MessageHeaders.CONTENT_TYPE`头进行转换。 + +| |在`ProducerRecord.value()`属性中调用`KafkaMessageConverter.fromMessage()`方法以将消息有效负载转换为`ProducerRecord`。
方法称为`KafkaMessageConverter.toMessage()`方法对于来自`ConsumerRecord`且有效负载为`ConsumerRecord.value()`属性的入站转换。
调用`SmartMessageConverter.toMessage()`方法来从传递到`Message`的`Message`中创建一个新的出站`Message`(通常由`KafkaTemplate.send(Message msg)`)。
类似地,在`KafkaMessageConverter.toMessage()`方法中,在转换器从`ConsumerRecord`创建了一个新的`Message`之后,调用`SmartMessageConverter.fromMessage()`方法,然后使用新转换的有效负载创建最终的入站消息。
在这两种情况下,如果返回`null`,则使用原始消息。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当在`KafkaTemplate`和侦听器容器工厂中使用默认转换器时,你可以通过在模板上调用`setMessagingConverter()`和在`@KafkaListener`方法上通过`contentMessageConverter`属性来配置`SmartMessageConverter`。 + +例子: + +``` +template.setMessagingConverter(mySmartConverter); +``` + +``` +@KafkaListener(id = "withSmartConverter", topics = "someTopic", + contentTypeConverter = "mySmartConverter") +public void smart(Thing thing) { + ... +} +``` + +###### 使用 Spring 数据投影接口 + +从版本 2.1.1 开始,你可以将 JSON 转换为 Spring 数据投影接口,而不是具体的类型。这允许对数据进行非常有选择性的、低耦合的绑定,包括从 JSON 文档中的多个位置查找值。例如,以下接口可以定义为消息有效负载类型: + +``` +interface SomeSample { + + @JsonPath({ "$.username", "$.user.name" }) + String getUsername(); + +} +``` + +``` +@KafkaListener(id="projection.listener", topics = "projection") +public void projection(SomeSample in) { + String username = in.getUsername(); + ... +} +``` + +默认情况下,访问器方法将用于在接收的 JSON 文档中查找属性名称 AS 字段。`@JsonPath`表达式允许定制值查找,甚至可以定义多个 JSON 路径表达式,从多个位置查找值,直到表达式返回实际值。 + +要启用此功能,请使用配置有适当委托转换器的`ProjectingMessageConverter`(用于出站转换和转换非投影接口)。你还必须将`spring-data:spring-data-commons`和`com.jayway.jsonpath:json-path`添加到类路径。 + +当用作`@KafkaListener`方法的参数时,接口类型将作为正常类型自动传递给转换器。 + +##### 使用`ErrorHandlingDeserializer` + +当反序列化器无法对消息进行反序列化时, Spring 无法处理该问题,因为它发生在`poll()`返回之前。为了解决这个问题,引入了`ErrorHandlingDeserializer`。这个反序列化器委托给一个真正的反序列化器(键或值)。如果委托未能反序列化记录内容,则`ErrorHandlingDeserializer`在包含原因和原始字节的头文件中返回一个`null`值和一个`DeserializationException`值。当你使用一个记录级别`MessageListener`时,如果`ConsumerRecord`包含一个用于键或值的`DeserializationException`头,则使用失败的`ErrorHandler`调用容器的`ConsumerRecord`。记录不会传递给监听器。 + +或者,你可以通过提供`failedDeserializationFunction`来配置`ErrorHandlingDeserializer`以创建自定义值,这是`Function`。调用此函数以创建`T`的实例,该实例将以通常的方式传递给侦听器。一个类型为`FailedDeserializationInfo`的对象,它包含提供给函数的所有上下文信息。你可以在头文件中找到`DeserializationException`(作为序列化的 Java 对象)。有关更多信息,请参见[Javadoc](https://docs.spring.io/spring-kafka/api/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer.html)中的`ErrorHandlingDeserializer`。 + +你可以使用`DefaultKafkaConsumerFactory`构造函数,它接受键和值`Deserializer`对象,并在适当的`ErrorHandlingDeserializer`实例中连线,你已经用适当的委托进行了配置。或者,你可以使用消费者配置属性(`ErrorHandlingDeserializer`使用的属性)来实例化委托。属性名为`ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS`和`ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS`。属性值可以是类或类名。下面的示例展示了如何设置这些属性: + +``` +... // other props +props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); +props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); +props.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, JsonDeserializer.class); +props.put(JsonDeserializer.KEY_DEFAULT_TYPE, "com.example.MyKey") +props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName()); +props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.example.MyValue") +props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.example") +return new DefaultKafkaConsumerFactory<>(props); +``` + +下面的示例使用`failedDeserializationFunction`。 + +``` +public class BadFoo extends Foo { + + private final FailedDeserializationInfo failedDeserializationInfo; + + public BadFoo(FailedDeserializationInfo failedDeserializationInfo) { + this.failedDeserializationInfo = failedDeserializationInfo; + } + + public FailedDeserializationInfo getFailedDeserializationInfo() { + return this.failedDeserializationInfo; + } + +} + +public class FailedFooProvider implements Function { + + @Override + public Foo apply(FailedDeserializationInfo info) { + return new BadFoo(info); + } + +} +``` + +前面的示例使用以下配置: + +``` +... +consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); +consumerProps.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class); +consumerProps.put(ErrorHandlingDeserializer.VALUE_FUNCTION, FailedFooProvider.class); +... +``` + +| |如果使用者配置了`ErrorHandlingDeserializer`,那么将`KafkaTemplate`及其生成器配置为序列化器非常重要,该序列化器可以处理普通对象以及 RAW`byte[]`值,这是反序列化异常的结果。
模板的泛型值类型应该是`Object`。
一种技术是使用`DelegatingByTypeSerializer`;示例如下:| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Bean +public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(), + new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(), + MyNormalObject.class, new JsonSerializer()))); +} + +@Bean +public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); +} +``` + +当在批处理侦听器中使用`ErrorHandlingDeserializer`时,必须检查消息头中的反序列化异常。当与`DefaultBatchErrorHandler`一起使用时,你可以使用该头确定异常在哪个记录上失败,并通过`BatchListenerFailedException`与错误处理程序通信。 + +``` +@KafkaListener(id = "test", topics = "test") +void listen(List in, @Header(KafkaHeaders.BATCH_CONVERTED_HEADERS) List> headers) { + for (int i = 0; i < in.size(); i++) { + Thing thing = in.get(i); + if (thing == null + && headers.get(i).get(SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER) != null) { + DeserializationException deserEx = ListenerUtils.byteArrayToDeserializationException(this.logger, + (byte[]) headers.get(i).get(SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER)); + if (deserEx != null) { + logger.error(deserEx, "Record at index " + i + " could not be deserialized"); + } + throw new BatchListenerFailedException("Deserialization", deserEx, i); + } + process(thing); + } +} +``` + +`ListenerUtils.byteArrayToDeserializationException()`可用于将标题转换为`DeserializationException`。 + +在消费`List`时,使用`ListenerUtils.getExceptionFromHeader()`代替: + +``` +@KafkaListener(id = "kgh2036", topics = "kgh2036") +void listen(List> in) { + for (int i = 0; i < in.size(); i++) { + ConsumerRecord rec = in.get(i); + if (rec.value() == null) { + DeserializationException deserEx = ListenerUtils.getExceptionFromHeader(rec, + SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER, this.logger); + if (deserEx != null) { + logger.error(deserEx, "Record at offset " + rec.offset() + " could not be deserialized"); + throw new BatchListenerFailedException("Deserialization", deserEx, i); + } + } + process(rec.value()); + } +} +``` + +##### 与批处理侦听器的有效负载转换 + +在使用批监听器容器工厂时,还可以在`BatchMessagingMessageConverter`中使用`JsonMessageConverter`来转换批处理消息。有关更多信息,请参见[序列化、反序列化和消息转换](#serdes)和[Spring Messaging Message Conversion](#messaging-message-conversion)。 + +默认情况下,转换的类型是从侦听器参数推断出来的。如果将`JsonMessageConverter`配置为`DefaultJackson2TypeMapper`,并将其`TypePrecedence`设置为`TYPE_ID`(而不是默认的`INFERRED`),则转换器将使用头中的类型信息(如果存在的话)。例如,这允许使用接口声明侦听器方法,而不是使用具体的类。此外,类型转换器支持映射,因此反序列化可以是与源不同的类型(只要数据是兼容的)。当你使用[class-level`@KafkaListener`实例](#class-level-kafkalistener)时,这也很有用,因为有效负载必须已经被转换,以确定要调用的方法。下面的示例创建使用此方法的 bean: + +``` +@Bean +public KafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + factory.setBatchListener(true); + factory.setMessageConverter(new BatchMessagingMessageConverter(converter())); + return factory; +} + +@Bean +public JsonMessageConverter converter() { + return new JsonMessageConverter(); +} +``` + +请注意,要使其工作,转换目标的方法签名必须是具有单一泛型参数类型的容器对象,例如: + +``` +@KafkaListener(topics = "blc1") +public void listen(List foos, @Header(KafkaHeaders.OFFSET) List offsets) { + ... +} +``` + +请注意,你仍然可以访问批处理头。 + +如果批处理转换器有一个支持它的记录转换器,那么你还可以接收一个消息列表,其中根据通用类型转换了有效负载。下面的示例展示了如何做到这一点: + +``` +@KafkaListener(topics = "blc3", groupId = "blc3") +public void listen1(List> fooMessages) { + ... +} +``` + +##### `ConversionService`定制 + +从版本 2.1.1 开始,默认`org.springframework.core.convert.ConversionService`用于解析侦听器方法调用的参数所使用的`org.springframework.core.convert.ConversionService`与实现以下任何接口的所有 bean 一起提供: + +* `org.springframework.core.convert.converter.Converter` + +* `org.springframework.core.convert.converter.GenericConverter` + +* `org.springframework.format.Formatter` + +这使你可以进一步定制侦听器反序列化,而无需更改`ConsumerFactory`和`KafkaListenerContainerFactory`的默认配置。 + +| |通过`KafkaListenerConfigurer` Bean 在`KafkaListenerEndpointRegistrar`上设置自定义的`MessageHandlerMethodFactory`将禁用此功能。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 将自定义`HandlerMethodArgumentResolver`添加到`@KafkaListener` + +从版本 2.4.2 开始,你可以添加自己的`HandlerMethodArgumentResolver`并解析自定义方法参数。你所需要的只是实现`KafkaListenerConfigurer`并使用来自类`setCustomMethodArgumentResolvers()`的方法`setCustomMethodArgumentResolvers()`。 + +``` +@Configuration +class CustomKafkaConfig implements KafkaListenerConfigurer { + + @Override + public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) { + registrar.setCustomMethodArgumentResolvers( + new HandlerMethodArgumentResolver() { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + return new CustomMethodArgument( + message.getHeaders().get(KafkaHeaders.RECEIVED_TOPIC, String.class) + ); + } + } + ); + } + +} +``` + +还可以通过在`KafkaListenerEndpointRegistrar` Bean 中添加自定义`MessageHandlerMethodFactory`来完全替换框架的参数解析。如果你这样做,并且你的应用程序需要处理 Tombstone 记录,使用`null``value()`(例如来自压缩主题),你应该向工厂添加`KafkaNullAwarePayloadArgumentResolver`;它必须是最后一个解析器,因为它支持所有类型,并且可以在没有`@Payload`注释的情况下匹配参数。如果你使用的是`DefaultMessageHandlerMethodFactory`,请将此解析器设置为最后一个自定义解析器;工厂将确保此解析器将在标准`PayloadMethodArgumentResolver`之前使用,该标准不知道`KafkaNull`的有效负载。 + +另见[“墓碑”记录的空载和日志压缩](#tombstones)。 + +#### 4.1.18.消息头 + +0.11.0.0 客户机引入了对消息中的头的支持。从版本 2.0 开始, Spring for Apache Kafka 现在支持将这些头映射到`spring-messaging``MessageHeaders`。 + +| |以前的版本将`ConsumerRecord`和`ProducerRecord`映射到 Spring-messaging`Message`,在这种情况下,值属性被映射到并从`payload`和其他属性(`topic`,`partition`,等等)映射到头部,
仍然是这种情况,但是现在可以映射额外的(任意的)标题了。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Apache Kafka 头具有一个简单的 API,如下面的接口定义所示: + +``` +public interface Header { + + String key(); + + byte[] value(); + +} +``` + +提供`KafkaHeaderMapper`策略来映射 Kafka`Headers`和`MessageHeaders`之间的头条目。其接口定义如下: + +``` +public interface KafkaHeaderMapper { + + void fromHeaders(MessageHeaders headers, Headers target); + + void toHeaders(Headers source, Map target); + +} +``` + +`DefaultKafkaHeaderMapper`将键映射到`MessageHeaders`头名称,并且为了支持出站消息的丰富头类型,执行了 JSON 转换。一个“特殊”头(键为`spring_json_header_types`)包含一个`:`的 JSON 映射。这个头用于入站侧,以提供每个头值到原始类型的适当转换。 + +在入站方面,所有 Kafka`Header`实例都映射到`MessageHeaders`。在出站端,默认情况下,所有`MessageHeaders`都被映射,除了`id`,`timestamp`,以及映射到`ConsumerRecord`属性的头。 + +通过向映射器提供模式,你可以指定要为出站消息映射哪些头。下面的清单显示了一些示例映射: + +``` +public DefaultKafkaHeaderMapper() { (1) + ... +} + +public DefaultKafkaHeaderMapper(ObjectMapper objectMapper) { (2) + ... +} + +public DefaultKafkaHeaderMapper(String... patterns) { (3) + ... +} + +public DefaultKafkaHeaderMapper(ObjectMapper objectMapper, String... patterns) { (4) + ... +} +``` + +|**1**|使用默认的 Jackson`ObjectMapper`并映射大多数头,如示例前面所讨论的。| +|-----|------------------------------------------------------------------------------------------------| +|**2**|使用提供的 Jackson`ObjectMapper`并映射大多数头,如示例前面所讨论的那样。| +|**3**|使用默认的 Jackson`ObjectMapper`,并根据提供的模式映射标头。| +|**4**|使用提供的 Jackson`ObjectMapper`并根据提供的模式映射标头。| + +模式非常简单,可以包含一个引导通配符(``**), a trailing wildcard, or both (for example, ``**`.cat.*`)。你可以使用一个前导`!`来否定模式。与标头名称(无论是正的还是负的)相匹配的第一个模式获胜。 + +当你提供自己的模式时,我们建议包括`!id`和`!timestamp`,因为这些头在入站侧是只读的。 + +| |默认情况下,映射器只对`java.lang`和`java.util`中的类进行反序列化。
你可以通过使用`addTrustedPackages`方法添加受信任的包来信任其他(或所有)包。
如果你从不受信任的源接收消息,你可能希望只添加你信任的那些包。
要信任所有包,你可以使用`mapper.addTrustedPackages("*")`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在与不了解 Mapper 的 JSON 格式的系统通信时,以 RAW 形式映射`String`标头值非常有用。| +|---|--------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.2.5 开始,你可以指定某些字符串值的头不应该使用 JSON 进行映射,而应该从 RAW`byte[]`映射到/。`AbstractKafkaHeaderMapper`具有新的属性;`mapAllStringsOut`当设置为 true 时,所有字符串值头将使用`byte[]`属性(默认`UTF-8`)转换为`byte[]`。此外,还有一个属性`rawMappedHeaders`,它是`header name : boolean`的映射;如果映射包含一个头名称,并且头包含一个`String`值,则将使用字符集将其映射为一个 RAW`byte[]`。此映射还用于使用字符集将原始传入的`byte[]`头映射到`String`,当且仅当映射值中的布尔值`true`。如果布尔值是`false`,或者标头名不在映射中,并且具有`true`值,则传入的标头会简单地映射为未映射的原始标头。 + +下面的测试用例演示了这种机制。 + +``` +@Test +public void testSpecificStringConvert() { + DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper(); + Map rawMappedHeaders = new HashMap<>(); + rawMappedHeaders.put("thisOnesAString", true); + rawMappedHeaders.put("thisOnesBytes", false); + mapper.setRawMappedHeaders(rawMappedHeaders); + Map headersMap = new HashMap<>(); + headersMap.put("thisOnesAString", "thing1"); + headersMap.put("thisOnesBytes", "thing2"); + headersMap.put("alwaysRaw", "thing3".getBytes()); + MessageHeaders headers = new MessageHeaders(headersMap); + Headers target = new RecordHeaders(); + mapper.fromHeaders(headers, target); + assertThat(target).containsExactlyInAnyOrder( + new RecordHeader("thisOnesAString", "thing1".getBytes()), + new RecordHeader("thisOnesBytes", "thing2".getBytes()), + new RecordHeader("alwaysRaw", "thing3".getBytes())); + headersMap.clear(); + mapper.toHeaders(target, headersMap); + assertThat(headersMap).contains( + entry("thisOnesAString", "thing1"), + entry("thisOnesBytes", "thing2".getBytes()), + entry("alwaysRaw", "thing3".getBytes())); +} +``` + +默认情况下,`DefaultKafkaHeaderMapper`在`MessagingMessageConverter`和`BatchMessagingMessageConverter`中使用`DefaultKafkaHeaderMapper`,只要 Jackson 在类路径上。 + +有了批处理转换器,转换后的头在`KafkaHeaders.BATCH_CONVERTED_HEADERS`中是可用的,如`List>`,其中映射在列表的一个位置对应于数据在有效载荷中的位置。 + +如果没有转换器(要么是因为 Jackson 不存在,要么是显式地将其设置为`null`),则消费者记录的头在`KafkaHeaders.NATIVE_HEADERS`头中提供未转换的头。这个报头是`Headers`对象(或者在批处理转换器的情况下是`List`对象),其中列表中的位置对应于有效负载中的数据位置)。 + +| |某些类型不适合 JSON 序列化,对于这些类型,可能更喜欢简单的`toString()`序列化。
`DefaultKafkaHeaderMapper`有一个名为`addToStringClasses()`的方法,该方法允许你提供在出站映射时应该以这种方式处理的类的名称,
,它们被映射为`String`。
默认情况下,只有`org.springframework.util.MimeType`和`org.springframework.http.MediaType`是这样映射的。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |从版本 2.3 开始,字符串标头的处理被简化了。
这样的标头不再被 JSON 编码,默认情况下(即,它们没有附加`"…​"`)。
类型仍然被添加到 JSON\_types 头中,以便接收系统可以转换回字符串(从`byte[]`)。
映射器可以处理旧版本产生的(解码)标题(它检查)对于领先的`"`);这样,使用 2.3 的应用程序可以使用旧版本的记录。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |为了与早期版本兼容,将`encodeStrings`设置为`true`,如果使用 2.3 的版本产生的记录可能被使用早期版本的应用程序使用。
当所有应用程序都使用 2.3 或更高版本时,你可以将该属性保留在其默认值`false`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Bean +MessagingMessageConverter converter() { + MessagingMessageConverter converter = new MessagingMessageConverter(); + DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper(); + mapper.setEncodeStrings(true); + converter.setHeaderMapper(mapper); + return converter; +} +``` + +如果使用 Spring 引导,它将自动配置这个转换器 Bean 到自动配置的`KafkaTemplate`中;否则你应该将这个转换器添加到模板中。 + +#### 4.1.19.“墓碑”记录的空载和日志压缩 + +当你使用[对数压缩](https://kafka.apache.org/documentation/#compaction)时,你可以发送和接收带有`null`有效负载的消息,以识别删除的密钥。 + +由于其他原因,你也可以接收`null`值,例如,当不能反序列化某个值时,可能会返回`null`值。 + +要通过使用`KafkaTemplate`发送`null`有效负载,可以将 null 传递到`send()`方法的值参数中。这方面的一个例外是`send(Message message)`变体。由于`spring-messaging``Message`不能具有`null`有效载荷,因此可以使用一种称为`KafkaNull`的特殊有效载荷类型,并且框架发送`null`。为了方便起见,提供了静态`KafkaNull.INSTANCE`。 + +当使用消息侦听器容器时,接收到的`ConsumerRecord`具有`null``value()`。 + +要将`@KafkaListener`配置为处理`null`有效负载,必须使用`@Payload`注释和`required = false`。如果这是一个压缩日志的墓碑消息,那么你通常还需要这个键,这样你的应用程序就可以确定哪个键被“删除”了。下面的示例展示了这样的配置: + +``` +@KafkaListener(id = "deletableListener", topics = "myTopic") +public void listen(@Payload(required = false) String value, @Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) String key) { + // value == null represents key deletion +} +``` + +当使用具有多个`@KafkaHandler`方法的类级`@KafkaListener`时,需要进行一些额外的配置。具体地说,你需要一个带有`@KafkaHandler`有效负载的`KafkaNull`方法。下面的示例展示了如何配置一个: + +``` +@KafkaListener(id = "multi", topics = "myTopic") +static class MultiListenerBean { + + @KafkaHandler + public void listen(String cat) { + ... + } + + @KafkaHandler + public void listen(Integer hat) { + ... + } + + @KafkaHandler + public void delete(@Payload(required = false) KafkaNull nul, @Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) int key) { + ... + } + +} +``` + +请注意,参数是`null`,而不是`KafkaNull`。 + +| |参见[[[tip-assign-all-parts]]。| +|---|----------------------------------------------------| + +| |此功能需要使用`KafkaNullAwarePayloadArgumentResolver`,当使用默认的`MessageHandlerMethodFactory`时,框架将对其进行配置。
当使用自定义的`MessageHandlerMethodFactory`时,请参阅[将自定义`HandlerMethodArgumentResolver`添加到`@KafkaListener`]。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.1.20.处理异常 + +本节描述了如何处理在使用 Spring 用于 Apache Kafka 时可能出现的各种异常。 + +##### 侦听器错误处理程序 + +从版本 2.0 开始,`@KafkaListener`注释有一个新属性:`errorHandler`。 + +你可以使用`errorHandler`来提供`KafkaListenerErrorHandler`实现的 Bean 名称。这个功能接口有一个方法,如下所示: + +``` +@FunctionalInterface +public interface KafkaListenerErrorHandler { + + Object handleError(Message message, ListenerExecutionFailedException exception) throws Exception; + +} +``` + +你可以访问消息转换器产生的 Spring-messaging`Message`对象,以及侦听器抛出的异常,该异常包装在`ListenerExecutionFailedException`中。错误处理程序可以抛出原始异常或新的异常,这些异常将被抛出到容器中。错误处理程序返回的任何内容都将被忽略。 + +从版本 2.7 开始,你可以在`MessagingMessageConverter`和`BatchMessagingMessageConverter`上设置`rawRecordHeader`属性,这会导致将 RAW`ConsumerRecord`添加到`KafkaHeaders.RAW_DATA`标头中转换的`Message`中。这是有用的,例如,如果你希望在侦听器错误处理程序中使用`DeadLetterPublishingRecoverer`。它可能用于请求/回复场景,在此场景中,你希望在重试一定次数后,在捕获死信主题中的失败记录后,将失败结果发送给发件人。 + +``` +@Bean +KafkaListenerErrorHandler eh(DeadLetterPublishingRecoverer recoverer) { + return (msg, ex) -> { + if (msg.getHeaders().get(KafkaHeaders.DELIVERY_ATTEMPT, Integer.class) > 9) { + recoverer.accept(msg.getHeaders().get(KafkaHeaders.RAW_DATA, ConsumerRecord.class), ex); + return "FAILED"; + } + throw ex; + }; +} +``` + +它有一个子接口(`ConsumerAwareListenerErrorHandler`),可以通过以下方法访问消费者对象: + +``` +Object handleError(Message message, ListenerExecutionFailedException exception, Consumer consumer); +``` + +如果你的错误处理程序实现了这个接口,那么你可以(例如)相应地调整偏移量。例如,要重置偏移量以重播失败的消息,你可以执行以下操作: + +``` +@Bean +public ConsumerAwareListenerErrorHandler listen3ErrorHandler() { + return (m, e, c) -> { + this.listen3Exception = e; + MessageHeaders headers = m.getHeaders(); + c.seek(new org.apache.kafka.common.TopicPartition( + headers.get(KafkaHeaders.RECEIVED_TOPIC, String.class), + headers.get(KafkaHeaders.RECEIVED_PARTITION_ID, Integer.class)), + headers.get(KafkaHeaders.OFFSET, Long.class)); + return null; + }; +} +``` + +类似地,你可以为批处理侦听器执行如下操作: + +``` +@Bean +public ConsumerAwareListenerErrorHandler listen10ErrorHandler() { + return (m, e, c) -> { + this.listen10Exception = e; + MessageHeaders headers = m.getHeaders(); + List topics = headers.get(KafkaHeaders.RECEIVED_TOPIC, List.class); + List partitions = headers.get(KafkaHeaders.RECEIVED_PARTITION_ID, List.class); + List offsets = headers.get(KafkaHeaders.OFFSET, List.class); + Map offsetsToReset = new HashMap<>(); + for (int i = 0; i < topics.size(); i++) { + int index = i; + offsetsToReset.compute(new TopicPartition(topics.get(i), partitions.get(i)), + (k, v) -> v == null ? offsets.get(index) : Math.min(v, offsets.get(index))); + } + offsetsToReset.forEach((k, v) -> c.seek(k, v)); + return null; + }; +} +``` + +这会将批处理中的每个主题/分区重置为批处理中的最低偏移量。 + +| |前面的两个示例是简单的实现,你可能希望在错误处理程序中进行更多的检查。| +|---|--------------------------------------------------------------------------------------------------------------------------| + +##### 容器错误处理程序 + +从版本 2.8 开始,遗留的`ErrorHandler`和`BatchErrorHandler`接口已被一个新的`CommonErrorHandler`所取代。这些错误处理程序可以同时处理记录和批处理侦听器的错误,从而允许单个侦听器容器工厂为这两种类型的侦听器创建容器。`CommonErrorHandler`替换大多数遗留框架错误处理程序的实现被提供,并且不推荐遗留错误处理程序。遗留接口仍然受到侦听器容器和侦听器容器工厂的支持;它们将在未来的版本中被弃用。 + +在使用事务时,默认情况下不会配置错误处理程序,因此异常将回滚事务。事务容器的错误处理由[`AfterRollbackProcessor`](#after-rollback)处理。如果你在使用事务时提供了自定义错误处理程序,那么如果你希望回滚事务,它必须抛出异常。 + +这个接口有一个默认的方法`isAckAfterHandle()`,容器调用它来确定如果错误处理程序返回而没有抛出异常,是否应该提交偏移量;默认情况下,它返回 true。 + +通常,框架提供的错误处理程序将在错误未得到“处理”时(例如,在执行查找操作后)抛出异常。默认情况下,此类异常由容器在`ERROR`级别记录。所有框架错误处理程序都扩展了`KafkaExceptionLogLevelAware`,它允许你控制记录这些异常的级别。 + +``` +/** + * Set the level at which the exception thrown by this handler is logged. + * @param logLevel the level (default ERROR). + */ +public void setLogLevel(KafkaException.Level logLevel) { + ... +} +``` + +你可以为容器工厂中的所有侦听器指定一个全局错误处理程序。下面的示例展示了如何做到这一点: + +``` +@Bean +public KafkaListenerContainerFactory> + kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + ... + factory.setCommonErrorHandler(myErrorHandler); + ... + return factory; +} +``` + +默认情况下,如果带注释的侦听器方法抛出异常,则将其抛出到容器中,并根据容器配置来处理消息。 + +容器在调用错误处理程序之前提交任何挂起的偏移量提交。 + +如果使用 Spring 引导,只需将错误处理程序添加为`@Bean`,然后引导将其添加到自动配置的工厂。 + +##### DefaulTerrorHandler + +这个新的错误处理程序替换了`SeekToCurrentErrorHandler`和`RecoveringBatchErrorHandler`,它们现在已经是几个版本的默认错误处理程序。一个不同之处是批处理侦听器的回退行为(当抛出`BatchListenerFailedException`以外的异常时)与[重试完整批](#retrying-batch-eh)是等价的。 + +错误处理程序可以恢复(跳过)持续失败的记录。默认情况下,在十次失败之后,将记录失败的记录(在`ERROR`级别)。你可以使用一个自定义的 recoverer(`BiConsumer`)和一个`BackOff`来配置处理程序,该 receverer 和`BackOff`控制每次交付的尝试和延迟。使用`FixedBackOff`和`FixedBackOff.UNLIMITED_ATTEMPTS`可以(有效地)导致无限次重试。下面的示例在三次尝试后配置恢复: + +``` +DefaultErrorHandler errorHandler = + new DefaultErrorHandler((record, exception) -> { + // recover after 3 failures, with no back off - e.g. send to a dead-letter topic + }, new FixedBackOff(0L, 2L)); +``` + +要用此处理程序的定制实例配置侦听器容器,请将其添加到容器工厂。 + +例如,对于`@KafkaListener`容器工厂,你可以添加`DefaultErrorHandler`,如下所示: + +``` +@Bean +public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); + factory.setConsumerFactory(consumerFactory()); + factory.getContainerProperties().setAckOnError(false); + factory.getContainerProperties().setAckMode(AckMode.RECORD); + factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 2L))); + return factory; +} +``` + +对于记录侦听器,这将重试一次交付多达 2 次(3 次交付尝试),并后退 1 秒,而不是默认配置(`FixedBackOff(0L, 9)`)。在重试结束后,只需记录失败的次数。 + +例如;如果`poll`返回六条记录(每个分区 0、1、2 有两条记录),并且侦听器在第四条记录上抛出异常,则容器通过提交它们的偏移量来确认前三条消息。`DefaultErrorHandler`寻求分区 1 的偏移量 1 和分区 2 的偏移量 0.下一个`poll()`返回这三条未处理的记录。 + +如果`AckMode`是`BATCH`,则容器在调用错误处理程序之前提交前两个分区的偏移量。 + +对于批处理侦听器,侦听器必须抛出`BatchListenerFailedException`,指示批处理中的哪些记录失败。 + +事件的顺序是: + +* 在索引之前提交记录的偏移量。 + +* 如果没有用尽重试,则执行查找,以便将所有剩余的记录(包括失败的记录)重新交付。 + +* 如果重复尝试已用尽,请尝试恢复失败的记录(仅缺省日志)并执行查找,以便重新交付剩余的记录(不包括失败的记录)。已提交已恢复记录的偏移量。 + +* 如果重试已用尽,而恢复失败,则进行查找,就好像重试尚未用尽一样。 + +在重试结束后,默认的 recoverer 会记录失败的记录。你可以使用自定义的 recoverer,或者由框架提供的一个,例如[`DeadLetterPublishingRecoverer`](#dead-letters)。 + +当使用 POJO 批处理侦听器(例如`List`)时,如果没有完整的消费者记录可添加到异常中,则只需添加失败记录的索引: + +``` +@KafkaListener(id = "recovering", topics = "someTopic") +public void listen(List things) { + for (int i = 0; i < records.size(); i++) { + try { + process(things.get(i)); + } + catch (Exception e) { + throw new BatchListenerFailedException("Failed to process", i); + } + } +} +``` + +当容器配置为`AckMode.MANUAL_IMMEDIATE`时,可以将错误处理程序配置为提交恢复记录的偏移量;将`commitRecovered`属性设置为`true`。 + +另见[发布死信记录](#dead-letters)。 + +当使用事务时,类似的功能由`DefaultAfterRollbackProcessor`提供。见[后回滚处理器](#after-rollback)。 + +`DefaultErrorHandler`认为某些异常是致命的,对于此类异常跳过重试;在第一次失败时调用 recuverer。默认情况下,被认为是致命的例外是: + +* `DeserializationException` + +* `MessageConversionException` + +* `ConversionException` + +* `MethodArgumentResolutionException` + +* `NoSuchMethodException` + +* `ClassCastException` + +因为这些异常不太可能在重试交付时得到解决。 + +你可以将更多的异常类型添加到不可重排的类别中,或者完全替换分类异常的映射。有关更多信息,请参见`DefaultErrorHandler.addNotRetryableException()`和`DefaultErrorHandler.setClassifications()`的 Javadocs,以及`spring-retry``BinaryExceptionClassifier`的 Javadocs。 + +下面是一个将`IllegalArgumentException`添加到不可重排异常的示例: + +``` +@Bean +public DefaultErrorHandler errorHandler(ConsumerRecordRecoverer recoverer) { + DefaultErrorHandler handler = new DefaultErrorHandler(recoverer); + handler.addNotRetryableExceptions(IllegalArgumentException.class); + return handler; +} +``` + +可以将错误处理程序配置为一个或多个`RetryListener`s,接收重试和恢复进度的通知。 + +``` +@FunctionalInterface +public interface RetryListener { + + void failedDelivery(ConsumerRecord record, Exception ex, int deliveryAttempt); + + default void recovered(ConsumerRecord record, Exception ex) { + } + + default void recoveryFailed(ConsumerRecord record, Exception original, Exception failure) { + } + +} +``` + +有关更多信息,请参见 Javadocs。 + +| |如果恢复程序失败(抛出异常),失败的记录将被包括在 Seeks 中。
如果恢复程序失败,`BackOff`将在默认情况下重置,并且在再次尝试恢复之前,重新交付将再次通过 back off。
在恢复失败之后跳过重试,将错误处理程序的`resetStateOnRecoveryFailure`设置为`false`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以向错误处理程序提供`BiFunction, Exception, BackOff>`,以基于失败的记录和/或异常来确定要使用的`BackOff`: + +``` +handler.setBackOffFunction((record, ex) -> { ... }); +``` + +如果函数返回`null`,将使用处理程序的默认`BackOff`。 + +将`resetStateOnExceptionChange`设置为`true`,如果异常类型在两次失败之间发生变化,则将重新启动重试序列(包括选择新的`BackOff`,如果这样配置的话)。默认情况下,不考虑异常类型。 + +另见[传递尝试标头](#delivery-header)。 + +#### 4.1.21.使用批处理错误处理程序的转换错误 + +从版本 2.8 开始,批处理侦听器现在可以正确处理转换错误,当使用`MessageConverter`和`ByteArrayDeserializer`、`BytesDeserializer`或`StringDeserializer`以及`DefaultErrorHandler`时。当发生转换错误时,将有效负载设置为 null,并将反序列化异常添加到记录头中,类似于`ErrorHandlingDeserializer`。侦听器中有一个`ConversionException`s 的列表可用,因此侦听器可以抛出一个`BatchListenerFailedException`,指示发生转换异常的第一个索引。 + +示例: + +``` +@KafkaListener(id = "test", topics = "topic") +void listen(List in, @Header(KafkaHeaders.CONVERSION_FAILURES) List exceptions) { + for (int i = 0; i < in.size(); i++) { + Foo foo = in.get(i); + if (foo == null && exceptions.get(i) != null) { + throw new BatchListenerFailedException("Conversion error", exceptions.get(i), i); + } + process(foo); + } +} +``` + +##### 重试完整批 + +这就是批侦听器`DefaultErrorHandler`的回退行为,其中侦听器抛出一个`BatchListenerFailedException`以外的异常。 + +不能保证当一个批被重新交付时,该批具有相同数量的记录和/或重新交付的记录的顺序相同。因此,不可能轻松地保持批处理的重试状态。`FallbackBatchErrorHandler`采取如下方法。如果批处理侦听器抛出一个不是`BatchListenerFailedException`的异常,则从内存中的批记录执行重试。为了避免在扩展的重试过程中发生再平衡,错误处理程序会暂停使用者,在每次重试之前对其进行轮询,并再次调用侦听器。如果/当重试用完时,将为批处理中的每个记录调用`ConsumerRecordRecoverer`。如果 Recoverer 抛出一个异常,或者线程在睡眠期间被中断,则该批记录将在下一次投票时重新交付。在退出之前,无论结果如何,消费者都会被恢复。 + +| |此机制不能用于事务。| +|---|------------------------------------------------| + +在等待`BackOff`间隔期间,错误处理程序将进行短暂的休眠循环,直到达到所需的延迟,同时检查容器是否已停止,从而允许在`stop()`之后不久退出休眠,而不是导致延迟。 + +##### 容器停止错误处理程序 + +如果侦听器抛出异常,`CommonContainerStoppingErrorHandler`将停止容器。对于记录侦听器,当`AckMode`为`RECORD`时,将提交已处理记录的偏移。对于记录侦听器,当`AckMode`是任意手动值时,将提交已确认记录的偏移量。对于记录侦听器,当`AckMode`是`BATCH`时,或者对于批处理侦听器,当容器重新启动时,整个批处理将被重新播放。 + +在容器停止之后,抛出一个包装`ListenerExecutionFailedException`的异常。这将导致事务回滚(如果启用了事务)。 + +##### 委派错误处理程序 + +根据异常类型的不同,`CommonDelegatingErrorHandler`可以委托给不同的错误处理程序。例如,你可能希望对大多数异常调用`DefaultErrorHandler`,或者对其他异常调用`CommonContainerStoppingErrorHandler`。 + +##### 日志错误处理程序 + +`CommonLoggingErrorHandler`只记录异常;使用记录侦听器,上一次投票的剩余记录将传递给侦听器。对于批处理侦听器,将记录批处理中的所有记录。 + +##### 对记录和批处理侦听器使用不同的常见错误处理程序 + +如果你希望对记录和批处理侦听器使用不同的错误处理策略,则提供`CommonMixedErrorHandler`,允许为每个侦听器类型配置特定的错误处理程序。 + +##### 常见错误处理程序 summery + +* `DefaultErrorHandler` + +* `CommonContainerStoppingErrorHandler` + +* `CommonDelegatingErrorHandler` + +* `CommonLoggingErrorHandler` + +* `CommonMixedErrorHandler` + +##### 遗留错误处理程序及其替换程序 + +| Legacy Error Handler |替换| +|----------------------------------------|-------------------------------------------------------------------------------------------------------------| +| `LoggingErrorHandler` |`CommonLoggingErrorHandler`| +| `BatchLoggingErrorHandler` |`CommonLoggingErrorHandler`| +| `ConditionalDelegatingErrorHandler` |`DelegatingErrorHandler`| +|`ConditionalDelegatingBatchErrorHandler`|`DelegatingErrorHandler`| +| `ContainerStoppingErrorHandler` |`CommonContainerStoppingErrorHandler`| +| `ContainerStoppingBatchErrorHandler` |`CommonContainerStoppingErrorHandler`| +| `SeekToCurrentErrorHandler` |`DefaultErrorHandler`| +| `SeekToCurrentBatchErrorHandler` |没有替换,使用`DefaultErrorHandler`与无限`BackOff`。| +| `RecoveringBatchErrorHandler` |`DefaultErrorHandler`| +| `RetryingBatchErrorHandler` |没有替换-使用`DefaultErrorHandler`并抛出除`BatchListenerFailedException`以外的异常。| + +##### 后回滚处理器 + +在使用事务时,如果侦听器抛出一个异常(如果存在错误处理程序,则抛出一个异常),事务将被回滚。默认情况下,任何未处理的记录(包括失败的记录)都会在下一次投票时重新获取。这是通过在`DefaultAfterRollbackProcessor`中执行`seek`操作来实现的。使用批处理侦听器,整个批记录将被重新处理(容器不知道批处理中的哪一条记录失败了)。要修改此行为,可以使用自定义`AfterRollbackProcessor`配置侦听器容器。例如,对于基于记录的侦听器,你可能希望跟踪失败的记录,并在尝试了一定次数后放弃,也许可以将其发布到一个死信不疑的主题中。 + +从版本 2.2 开始,`DefaultAfterRollbackProcessor`现在可以恢复(跳过)一条持续失败的记录。默认情况下,在十次失败之后,将记录失败的记录(在`ERROR`级别)。你可以使用自定义的 recoverer(`BiConsumer`)和最大故障来配置处理器。将`maxFailures`属性设置为负数会导致无限次重试。下面的示例在三次尝试后配置恢复: + +``` +AfterRollbackProcessor processor = + new DefaultAfterRollbackProcessor((record, exception) -> { + // recover after 3 failures, with no back off - e.g. send to a dead-letter topic + }, new FixedBackOff(0L, 2L)); +``` + +当不使用事务时,可以通过配置`DefaultErrorHandler`来实现类似的功能。见[容器错误处理程序](#error-handlers)。 + +| |批处理侦听器不可能进行恢复,因为框架不知道批处理中的哪条记录一直失败。
在这种情况下,应用程序侦听器必须处理一直失败的记录。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另见[发布死信记录](#dead-letters)。 + +从版本 2.2.5 开始,可以在新事务中调用`DefaultAfterRollbackProcessor`(在失败的事务回滚后启动)。然后,如果你使用`DeadLetterPublishingRecoverer`来发布失败的记录,处理器将把恢复的记录在原始主题/分区中的偏移量发送给事务。要启用此功能,请在`DefaultAfterRollbackProcessor`上设置`commitRecovered`和`kafkaTemplate`属性。 + +| |如果回收器失败(抛出异常),失败的记录将包括在 SEEKS 中,
从版本 2.5.5 开始,如果回收器失败,`BackOff`将默认重置,在再次尝试恢复之前,重新交付将再次通过 Back off,
与较早的版本,`BackOff`未重置,在下一个失败时重新尝试恢复。
要恢复到上一个行为,请将处理器的`resetStateOnRecoveryFailure`属性设置为`false`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.6 开始,你现在可以为处理器提供一个`BiFunction, Exception, BackOff>`,以基于失败的记录和/或异常来确定要使用的`BackOff`: + +``` +handler.setBackOffFunction((record, ex) -> { ... }); +``` + +如果函数返回`null`,将使用处理器的默认`BackOff`。 + +从版本 2.6.3 开始,将`resetStateOnExceptionChange`设置为`true`,如果异常类型在两次失败之间发生变化,则将重新启动重试序列(包括选择一个新的`BackOff`,如果这样配置的话)。默认情况下,不考虑异常类型。 + +从版本 2.3.1 开始,类似于`DefaultErrorHandler`,`DefaultAfterRollbackProcessor`认为某些异常是致命的,并且对于此类异常跳过重试;在第一次失败时调用 recoverer。默认情况下,被认为是致命的例外是: + +* `DeserializationException` + +* `MessageConversionException` + +* `ConversionException` + +* `MethodArgumentResolutionException` + +* `NoSuchMethodException` + +* `ClassCastException` + +因为这些异常不太可能在重试交付时得到解决。 + +你可以将更多的异常类型添加到不可重排的类别中,或者完全替换分类异常的映射。有关更多信息,请参见`DefaultAfterRollbackProcessor.setClassifications()`的 Javadocs,以及`spring-retry``BinaryExceptionClassifier`的 Javadocs。 + +下面是一个将`IllegalArgumentException`添加到不可重排异常的示例: + +``` +@Bean +public DefaultAfterRollbackProcessor errorHandler(BiConsumer, Exception> recoverer) { + DefaultAfterRollbackProcessor processor = new DefaultAfterRollbackProcessor(recoverer); + processor.addNotRetryableException(IllegalArgumentException.class); + return processor; +} +``` + +另见[传递尝试标头](#delivery-header)。 + +| |使用 current`kafka-clients`,容器无法检测`ProducerFencedException`是由再平衡引起的,还是由于超时或过期而导致生产者的`transactional.id`已被撤销,
,因为在大多数情况下,它是由再平衡引起的,容器不调用`AfterRollbackProcessor`(因为不再分配分区,所以不适合查找分区)。
如果你确保超时足够大,可以处理每个事务并定期执行“空”事务(例如,通过`ListenerContainerIdleEvent`)可以避免由于超时和过期而设置栅栏。
或者,你可以将`stopContainerWhenFenced`容器属性设置为`true`,然后容器将停止,避免记录丢失。
你可以使用`ConsumerStoppedEvent`并检查`Reason`的`Reason`属性以检测此条件。
由于该事件还具有对容器的引用,因此可以使用此事件重新启动容器。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.7 开始,在等待`BackOff`间隔期间,错误处理程序将进行短暂的休眠循环,直到达到所需的延迟,同时检查容器是否已停止,允许睡眠在`stop()`后很快退出,而不是导致延迟。 + +从版本 2.7 开始,处理器可以配置一个或多个`RetryListener`s,接收重试和恢复进度的通知。 + +``` +@FunctionalInterface +public interface RetryListener { + + void failedDelivery(ConsumerRecord record, Exception ex, int deliveryAttempt); + + default void recovered(ConsumerRecord record, Exception ex) { + } + + default void recoveryFailed(ConsumerRecord record, Exception original, Exception failure) { + } + +} +``` + +有关更多信息,请参见 Javadocs。 + +##### 投递尝试头 + +以下内容仅适用于记录侦听器,而不是批处理侦听器。 + +从版本 2.5 开始,当使用实现`DeliveryAttemptAware`或`AfterRollbackProcessor`的`AfterRollbackProcessor`时,可以在记录中添加`KafkaHeaders.DELIVERY_ATTEMPT`头(`kafka_deliveryAttempt`)。这个标头的值是一个从 1 开始的递增整数。当接收到 RAW`ConsumerRecord`时,该整数在`byte[4]`中。 + +``` +int delivery = ByteBuffer.wrap(record.headers() + .lastHeader(KafkaHeaders.DELIVERY_ATTEMPT).value()) + .getInt() +``` + +当将`@KafkaListener`与`DefaultKafkaHeaderMapper`或`SimpleKafkaHeaderMapper`一起使用时,可以通过将`@Header(KafkaHeaders.DELIVERY_ATTEMPT) int delivery`作为参数添加到侦听器方法中来获得。 + +要启用这个头的填充,将容器属性`deliveryAttemptHeader`设置为`true`。默认情况下禁用它,以避免查找每个记录的状态并添加标题的(小)开销。 + +`DefaultErrorHandler`和`DefaultAfterRollbackProcessor`支持此功能。 + +##### 发布死信记录 + +当某项记录的失败次数达到最大值时,可以使用记录恢复程序配置`DefaultErrorHandler`和`DefaultAfterRollbackProcessor`。该框架提供`DeadLetterPublishingRecoverer`,用于将失败的消息发布到另一个主题。recoverer 需要一个`KafkaTemplate`,用于发送记录。你还可以选择用`BiFunction, Exception, TopicPartition>`配置它,调用它是为了解析目标主题和分区。 + +| |默认情况下,死信记录被发送到一个名为`.DLT`的主题(原始主题名称后缀为`.DLT`),并发送到与原始记录相同的分区。
因此,当你使用默认的解析器时,死信主题**必须至少有与原始主题一样多的分区。**| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果返回的`TopicPartition`有一个负分区,则该分区未在`ProducerRecord`中设置,因此该分区由 Kafka 选择。从版本 2.2.4 开始,任何`ListenerExecutionFailedException`(例如,当在`@KafkaListener`方法中检测到异常时抛出)都将使用`groupId`属性进行增强。这允许目标解析器使用这个,除了在`ConsumerRecord`中选择死信主题的信息之外。 + +下面的示例展示了如何连接自定义目标解析器: + +``` +DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template, + (r, e) -> { + if (e instanceof FooException) { + return new TopicPartition(r.topic() + ".Foo.failures", r.partition()); + } + else { + return new TopicPartition(r.topic() + ".other.failures", r.partition()); + } + }); +ErrorHandler errorHandler = new DefaultErrorHandler(recoverer, new FixedBackOff(0L, 2L)); +``` + +发送到死信主题的记录通过以下标题进行了增强: + +* `KafkaHeaders.DLT_EXCEPTION_FQCN`:异常类名称(一般是`ListenerExecutionFailedException`,但也可以是其他的)。 + +* `KafkaHeaders.DLT_EXCEPTION_CAUSE_FQCN`:异常导致类名,如果存在的话(自版本 2.8 起)。 + +* `KafkaHeaders.DLT_EXCEPTION_STACKTRACE`:异常堆栈跟踪。 + +* `KafkaHeaders.DLT_EXCEPTION_MESSAGE`:异常消息。 + +* `KafkaHeaders.DLT_KEY_EXCEPTION_FQCN`:异常类名(仅限键反序列化错误)。 + +* `KafkaHeaders.DLT_KEY_EXCEPTION_STACKTRACE`:异常堆栈跟踪(仅限键反序列化错误)。 + +* `KafkaHeaders.DLT_KEY_EXCEPTION_MESSAGE`:异常消息(仅限键反序列化错误)。 + +* `KafkaHeaders.DLT_ORIGINAL_TOPIC`:原始主题。 + +* `KafkaHeaders.DLT_ORIGINAL_PARTITION`:原始分区。 + +* `KafkaHeaders.DLT_ORIGINAL_OFFSET`:原始偏移量。 + +* `KafkaHeaders.DLT_ORIGINAL_TIMESTAMP`:原始时间戳。 + +* `KafkaHeaders.DLT_ORIGINAL_TIMESTAMP_TYPE`:原始时间戳类型。 + +* `KafkaHeaders.DLT_ORIGINAL_CONSUMER_GROUP`:未能处理记录的原始消费者组(自版本 2.8 起)。 + +关键异常仅由`DeserializationException`s 引起,因此不存在`DLT_KEY_EXCEPTION_CAUSE_FQCN`。 + +有两种机制可以添加更多的头。 + +1. 子类 recoverer 和 override`createProducerRecord()`-调用`super.createProducerRecord()`并添加更多标题。 + +2. 提供一个`BiFunction`来接收消费者记录和异常,返回一个`Headers`对象;头从那里将被复制到最终的生产者记录。使用`setHeadersFunction()`设置`BiFunction`。 + +第二种方法实现起来更简单,但第一种方法有更多可用信息,包括已经组装好的标准标头。 + +从版本 2.3 开始,当与`ErrorHandlingDeserializer`一起使用时,发布者将把死信生成器记录中的记录`value()`恢复到无法反序列化的原始值。以前,`value()`是空的,用户代码必须从消息头中解码`DeserializationException`。此外,你还可以向发布者提供多个`KafkaTemplate`s;这可能是必需的,例如,如果你希望从`DeserializationException`中发布`byte[]`,以及使用不同的序列化器从已成功反序列化的记录中发布的值。下面是一个使用`KafkaTemplate`s 和`byte[]`序列化器配置发布服务器的示例: + +``` +@Bean +public DeadLetterPublishingRecoverer publisher(KafkaTemplate stringTemplate, + KafkaTemplate bytesTemplate) { + + Map, KafkaTemplate> templates = new LinkedHashMap<>(); + templates.put(String.class, stringTemplate); + templates.put(byte[].class, bytesTemplate); + return new DeadLetterPublishingRecoverer(templates); +} +``` + +发布者使用映射键来定位适合即将发布的`value()`的模板。建议使用`LinkedHashMap`,以便按顺序检查密钥。 + +当发布`null`值时,当有多个模板时,recoverer 将为`Void`类寻找一个模板;如果不存在,将使用`values().iterator()`中的第一个模板。 + +从 2.7 开始,你可以使用`setFailIfSendResultIsError`方法,以便在消息发布失败时引发异常。你还可以使用`setWaitForSendResultTimeout`设置用于验证发送方成功的超时。 + +| |如果回收器失败(抛出异常),失败的记录将包括在 SEEKS 中,
从版本 2.5.5 开始,如果回收器失败,`BackOff`将默认重置,在再次尝试恢复之前,重新交付将再次通过 back off,
与更早的版本,未重置`BackOff`,并在下一个失败时重新尝试恢复。
要恢复到上一个行为,请将错误处理程序的`resetStateOnRecoveryFailure`属性设置为`false`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.6.3 开始,将`resetStateOnExceptionChange`设置为`true`,如果异常类型在两次失败之间发生变化,则将重新启动重试序列(包括选择一个新的`BackOff`,如果这样配置的话)。默认情况下,不考虑异常类型。 + +从版本 2.3 开始,Recoverer 还可以与 Kafka Streams 一起使用-有关更多信息,请参见[从反序列化异常恢复](#streams-deser-recovery)。 + +`ErrorHandlingDeserializer`在头`ErrorHandlingDeserializer.VALUE_DESERIALIZER_EXCEPTION_HEADER`和`ErrorHandlingDeserializer.KEY_DESERIALIZER_EXCEPTION_HEADER`(使用 Java 序列化)中添加了反序列化异常。默认情况下,这些标题不会保留在发布到死信主题的消息中。从版本 2.7 开始,如果键和值都反序列化失败,那么这两个键的原始值都会在发送到 DLT 的记录中填充。 + +如果接收到的记录是相互依赖的,但可能会到达顺序错误,那么将失败的记录重新发布到原始主题的尾部(多次)可能会很有用,而不是直接将其发送到死信主题。例如,见[这个堆栈溢出问题](https://stackoverflow.com/questions/64646996)。 + +下面的错误处理程序配置将完全做到这一点: + +``` +@Bean +public ErrorHandler eh(KafkaOperations template) { + return new DefaultErrorHandler(new DeadLetterPublishingRecoverer(template, + (rec, ex) -> { + org.apache.kafka.common.header.Header retries = rec.headers().lastHeader("retries"); + if (retries == null) { + retries = new RecordHeader("retries", new byte[] { 1 }); + rec.headers().add(retries); + } + else { + retries.value()[0]++; + } + return retries.value()[0] > 5 + ? new TopicPartition("topic.DLT", rec.partition()) + : new TopicPartition("topic", rec.partition()); + }), new FixedBackOff(0L, 0L)); +} +``` + +从版本 2.7 开始,recoverer 将检查目标解析程序选择的分区是否确实存在。如果不存在分区,则将`ProducerRecord`中的分区设置为`null`,从而允许`KafkaProducer`选择该分区。可以通过将`verifyPartition`属性设置为`false`来禁用此检查。 + +##### 管理死信记录头 + +参考上面的[发布死信记录](#dead-letters),`DeadLetterPublishingRecoverer`有两个属性,当这些头已经存在时(例如,当重新处理失败的死信记录时,包括使用[非阻塞重试](#retry-topic)时),这些属性用于管理头。 + +* `appendOriginalHeaders`(默认`true`) + +* `stripPreviousExceptionHeaders`(默认`true`自 2.8 版本) + +Apache Kafka 支持同名的多个头;要获得“latest”值,可以使用`headers.lastHeader(headerName)`;要在多个头上获得迭代器,可以使用`headers.headers(headerName).iterator()`。 + +当重复重新发布失败的记录时,这些标头可能会增加(并最终由于`RecordTooLargeException`而导致发布失败);对于异常标头,尤其是对于堆栈跟踪标头,尤其如此。 + +产生这两个属性的原因是,虽然你可能只希望保留最后一个异常信息,但你可能希望保留记录在每次失败时通过的主题的历史记录。 + +`appendOriginalHeaders`应用于所有名为`**ORIGINAL**`的标头,而`stripPreviousExceptionHeaders`应用于所有名为`**EXCEPTION**`的标头。 + +另见[故障报头管理](#retry-headers)与[非阻塞重试](#retry-topic)。 + +##### `ExponentialBackOffWithMaxRetries`实现 + +Spring 框架提供了许多`BackOff`实现方式。默认情况下,`ExponentialBackOff`将无限期地重试;如果要在多次重试后放弃,则需要计算`maxElapsedTime`。由于版本 2.7.3, Spring for Apache Kafka 提供了`ExponentialBackOffWithMaxRetries`,这是一个子类,它接收`maxRetries`属性并自动计算`maxElapsedTime`,这更方便一些。 + +``` +@Bean +DefaultErrorHandler handler() { + ExponentialBackOffWithMaxRetries bo = new ExponentialBackOffWithMaxRetries(6); + bo.setInitialInterval(1_000L); + bo.setMultiplier(2.0); + bo.setMaxInterval(10_000L); + return new DefaultErrorHandler(myRecoverer, bo); +} +``` + +这将在`1, 2, 4, 8, 10, 10`秒后重试,然后再调用 recoverer。 + +#### 4.1.22.Jaas 和 Kerberos + +从版本 2.0 开始,添加了一个`KafkaJaasLoginModuleInitializer`类来帮助 Kerberos 配置。你可以使用所需的配置将这个 Bean 添加到你的应用程序上下文中。下面的示例配置了这样的 Bean: + +``` +@Bean +public KafkaJaasLoginModuleInitializer jaasConfig() throws IOException { + KafkaJaasLoginModuleInitializer jaasConfig = new KafkaJaasLoginModuleInitializer(); + jaasConfig.setControlFlag("REQUIRED"); + Map options = new HashMap<>(); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("keyTab", "/etc/security/keytabs/kafka_client.keytab"); + options.put("principal", "[email protected]"); + jaasConfig.setOptions(options); + return jaasConfig; +} +``` + +### 4.2. Apache Kafka Streams 支持 + +从版本 1.1.4 开始, Spring for Apache Kafka 为[Kafka溪流](https://kafka.apache.org/documentation/streams)提供了一流的支持。要在 Spring 应用程序中使用它,`kafka-streams`jar 必须存在于 Classpath 上。它是 Spring for Apache Kafka 项目的可选依赖项,并且不是通过传递方式下载的。 + +#### 4.2.1.基础知识 + +参考文献 Apache Kafka Streams 文档建议使用以下 API 的方式: + +``` +// Use the builders to define the actual processing topology, e.g. to specify +// from which input topics to read, which stream operations (filter, map, etc.) +// should be called, and so on. + +StreamsBuilder builder = ...; // when using the Kafka Streams DSL + +// Use the configuration to tell your application where the Kafka cluster is, +// which serializers/deserializers to use by default, to specify security settings, +// and so on. +StreamsConfig config = ...; + +KafkaStreams streams = new KafkaStreams(builder, config); + +// Start the Kafka Streams instance +streams.start(); + +// Stop the Kafka Streams instance +streams.close(); +``` + +因此,我们有两个主要组成部分: + +* `StreamsBuilder`:使用 API 构建`KStream`(或`KTable`)实例。 + +* `KafkaStreams`:管理这些实例的生命周期。 + +| |由单个`StreamsBuilder`实例暴露给`KStream`实例的所有`KafkaStreams`实例同时启动和停止,即使它们具有不同的逻辑。,换句话说,
,由`StreamsBuilder`定义的所有流都与单个生命周期控件绑定。
一旦`KafkaStreams`实例被`streams.close()`关闭,就无法重新启动。
相反,必须创建一个新的`KafkaStreams`实例来重新启动流处理。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.2.2. Spring 管理 + +为了简化从 Spring 应用程序上下文视角使用 Kafka 流并通过容器使用生命周期管理, Spring for Apache Kafka 引入了`StreamsBuilderFactoryBean`。这是一个`AbstractFactoryBean`实现,用于将`StreamsBuilder`单例实例公开为 Bean。下面的示例创建了这样的 Bean: + +``` +@Bean +public FactoryBean myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) { + return new StreamsBuilderFactoryBean(streamsConfig); +} +``` + +| |从版本 2.2 开始,流配置现在提供为`KafkaStreamsConfiguration`对象,而不是`StreamsConfig`对象。| +|---|------------------------------------------------------------------------------------------------------------------------------------------| + +`StreamsBuilderFactoryBean`还实现了`SmartLifecycle`来管理内部`KafkaStreams`实例的生命周期。与 Kafka Streams API 类似,在启动`KafkaStreams`之前,必须定义`KStream`实例。这也适用于 Kafka Streams 的 Spring API。因此,当你在`StreamsBuilderFactoryBean`上使用默认的`autoStartup = true`时,你必须在刷新应用程序上下文之前在`KStream`上声明`KStream`实例。例如,`KStream`可以是一个常规的 Bean 定义,而 Kafka Streams API 的使用没有任何影响。下面的示例展示了如何做到这一点: + +``` +@Bean +public KStream kStream(StreamsBuilder kStreamBuilder) { + KStream stream = kStreamBuilder.stream(STREAMING_TOPIC1); + // Fluent KStream API + return stream; +} +``` + +如果希望手动控制生命周期(例如,通过某些条件停止和启动),则可以通过使用工厂 Bean(`StreamsBuilderFactoryBean`)直接引用[prefix](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-factory-extension-factorybean)。由于`StreamsBuilderFactoryBean`使用其内部`KafkaStreams`实例,因此可以安全地停止并重新启动它。在每个`start()`上创建一个新的`KafkaStreams`。如果你希望单独控制`KStream`实例的生命周期,那么你也可以考虑使用不同的`StreamsBuilderFactoryBean`实例。 + +你还可以在`StreamsBuilderFactoryBean`上指定`KafkaStreams.StateListener`、`Thread.UncaughtExceptionHandler`和`StateRestoreListener`选项,这些选项被委托给内部`KafkaStreams`实例。此外,除了在`StreamsBuilderFactoryBean`上以*版本 2.1.5*间接设置这些选项外,还可以使用`KafkaStreams`回调接口来配置内部`KafkaStreams`实例。请注意,`KafkaStreamsCustomizer`覆盖了`StreamsBuilderFactoryBean`提供的选项。如果需要直接执行一些`KafkaStreams`操作,则可以使用`StreamsBuilderFactoryBean.getKafkaStreams()`访问内部`KafkaStreams`实例。可以按类型自动连接`StreamsBuilderFactoryBean` Bean,但应确保在 Bean 定义中使用完整的类型,如下例所示: + +``` +@Bean +public StreamsBuilderFactoryBean myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) { + return new StreamsBuilderFactoryBean(streamsConfig); +} +... +@Autowired +private StreamsBuilderFactoryBean myKStreamBuilderFactoryBean; +``` + +或者,如果使用接口 Bean 定义,则可以按名称添加`@Qualifier`用于注入。下面的示例展示了如何做到这一点: + +``` +@Bean +public FactoryBean myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) { + return new StreamsBuilderFactoryBean(streamsConfig); +} +... +@Autowired +@Qualifier("&myKStreamBuilder") +private StreamsBuilderFactoryBean myKStreamBuilderFactoryBean; +``` + +从版本 2.4.1 开始,工厂 Bean 有一个新的属性`infrastructureCustomizer`,类型为`KafkaStreamsInfrastructureCustomizer`;这允许在创建流之前自定义`StreamsBuilder`(例如添加状态存储)和/或`Topology`。 + +``` +public interface KafkaStreamsInfrastructureCustomizer { + + void configureBuilder(StreamsBuilder builder); + + void configureTopology(Topology topology); + +} +``` + +提供了默认的无操作实现,以避免在不需要两个方法的情况下不得不实现这两个方法。 + +提供了一个`CompositeKafkaStreamsInfrastructureCustomizer`,用于在需要应用多个自定义程序时。 + +#### 4.2.3.Kafkastreams 测微仪支持 + +在版本 2.5.3 中引入的,可以配置`KafkaStreamsMicrometerListener`来为工厂 Bean 管理的`KafkaStreams`对象自动注册千分表: + +``` +streamsBuilderFactoryBean.addListener(new KafkaStreamsMicrometerListener(meterRegistry, + Collections.singletonList(new ImmutableTag("customTag", "customTagValue")))); +``` + +#### 4.2.4.流 JSON 序列化和反序列化 + +对于在以 JSON 格式读取或写入主题或状态存储时序列化和反序列化数据, Spring for Apache Kafka 提供了一个`JsonSerde`实现,该实现使用 JSON,将其委托给`JsonSerializer`和`JsonDeserializer`中描述的[序列化、反序列化和消息转换](#serdes)。`JsonSerde`实现通过其构造函数(目标类型或`ObjectMapper`)提供相同的配置选项。在下面的示例中,我们使用`JsonSerde`序列化和反序列化 Kafka 流的`Cat`有效负载(只要需要实例,`JsonSerde`就可以以类似的方式使用): + +``` +stream.through(Serdes.Integer(), new JsonSerde<>(Cat.class), "cats"); +``` + +从版本 2.3 开始,当以编程方式构建在生产者/消费者工厂中使用的序列化器/反序列化器时,你可以使用 Fluent API,这简化了配置。 + +``` +stream.through(new JsonSerde<>(MyKeyType.class) + .forKeys() + .noTypeInfo(), + new JsonSerde<>(MyValueType.class) + .noTypeInfo(), + "myTypes"); +``` + +#### 4.2.5.使用`KafkaStreamBrancher` + +`KafkaStreamBrancher`类引入了一种在`KStream`之上构建条件分支的更方便的方法。 + +考虑以下不使用`KafkaStreamBrancher`的示例: + +``` +KStream[] branches = builder.stream("source").branch( + (key, value) -> value.contains("A"), + (key, value) -> value.contains("B"), + (key, value) -> true + ); +branches[0].to("A"); +branches[1].to("B"); +branches[2].to("C"); +``` + +下面的示例使用`KafkaStreamBrancher`: + +``` +new KafkaStreamBrancher() + .branch((key, value) -> value.contains("A"), ks -> ks.to("A")) + .branch((key, value) -> value.contains("B"), ks -> ks.to("B")) + //default branch should not necessarily be defined in the end of the chain! + .defaultBranch(ks -> ks.to("C")) + .onTopOf(builder.stream("source")); + //onTopOf method returns the provided stream so we can continue with method chaining +``` + +#### 4.2.6.配置 + +要配置 Kafka Streams 环境,`StreamsBuilderFactoryBean`需要一个`KafkaStreamsConfiguration`实例。有关所有可能的选项,请参见 Apache kafka[文件](https://kafka.apache.org/0102/documentation/#streamsconfigs)。 + +| |从版本 2.2 开始,流配置现在以`KafkaStreamsConfiguration`对象的形式提供,而不是以`StreamsConfig`的形式提供。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------| + +为了避免在大多数情况下使用样板代码,特别是在开发微服务时, Spring for Apache Kafka 提供了`@EnableKafkaStreams`注释,你应该将其放置在`@Configuration`类上。只需要声明一个名为`KafkaStreamsConfiguration` Bean 的`defaultKafkaStreamsConfig`。在应用程序上下文中自动声明一个名为`StreamsBuilderFactoryBean` Bean 的`defaultKafkaStreamsBuilder`。你也可以声明和使用任何额外的`StreamsBuilderFactoryBean`bean。通过提供实现`StreamsBuilderFactoryBeanConfigurer`的 Bean,你可以对该 Bean 执行额外的自定义。如果有多个这样的 bean,则将根据其`Ordered.order`属性应用它们。 + +默认情况下,当工厂 Bean 停止时,将调用`KafkaStreams.cleanUp()`方法。从版本 2.1.2 开始,工厂 Bean 有额外的构造函数,接受一个`CleanupConfig`对象,该对象具有属性,可以让你控制在`cleanUp()`或`stop()`期间是否调用`cleanUp()`方法。从版本 2.7 开始,默认情况是永远不清理本地状态。 + +#### 4.2.7.页眉 Enricher + +版本 2.3 增加了`HeaderEnricher`的`Transformer`实现。这可用于在流处理中添加头;头的值是 SPEL 表达式;表达式求值的根对象具有 3 个属性: + +* `context`-`ProcessorContext`,允许访问当前记录的元数据 + +* `key`-当前记录的键 + +* `value`-当前记录的值 + +表达式必须返回`byte[]`或`String`(使用`UTF-8`将其转换为`byte[]`)。 + +要在流中使用 Enrich: + +``` +.transform(() -> enricher) +``` + +转换器不改变`key`或`value`;它只是添加了标题。 + +| |如果你的流是多线程的,那么你需要为每个记录添加一个新的实例。| +|---|--------------------------------------------------------------------------| + +``` +.transform(() -> new HeaderEnricher<..., ...>(expressionMap)) +``` + +下面是一个简单的示例,添加了一个文字头和一个变量: + +``` +Map headers = new HashMap<>(); +headers.put("header1", new LiteralExpression("value1")); +SpelExpressionParser parser = new SpelExpressionParser(); +headers.put("header2", parser.parseExpression("context.timestamp() + ' @' + context.offset()")); +HeaderEnricher enricher = new HeaderEnricher<>(headers); +KStream stream = builder.stream(INPUT); +stream + .transform(() -> enricher) + .to(OUTPUT); +``` + +#### 4.2.8.`MessagingTransformer` + +版本 2.3 增加了`MessagingTransformer`,这允许 Kafka Streams 拓扑与 Spring 消息传递组件进行交互,例如 Spring 集成流。转换器要求实现`MessagingFunction`。 + +``` +@FunctionalInterface +public interface MessagingFunction { + + Message exchange(Message message); + +} +``` + +Spring 集成自动提供了一种使用其`GatewayProxyFactoryBean`的实现方式。它还需要一个`MessagingMessageConverter`来将键、值和元数据(包括头)转换为/来自 Spring 消息传递`Message`。参见[[从`KStream`调用 Spring 集成流](https://DOCS. Spring.io/ Spring-integration/DOCS/current/reference/html/kafka.html#Streams-integration)]以获得更多信息。 + +#### 4.2.9.从反序列化异常恢复 + +版本 2.3 引入了`RecoveringDeserializationExceptionHandler`,它可以在发生反序列化异常时采取一些操作。请参考关于`DeserializationExceptionHandler`的 Kafka 文档,其中`RecoveringDeserializationExceptionHandler`是一个实现。`RecoveringDeserializationExceptionHandler`配置为`ConsumerRecordRecoverer`实现。该框架提供了`DeadLetterPublishingRecoverer`,它将失败的记录发送到死信主题。有关此回收器的更多信息,请参见[发布死信记录](#dead-letters)。 + +要配置 recoverer,请将以下属性添加到你的 Streams 配置中: + +``` +@Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME) +public KafkaStreamsConfiguration kStreamsConfigs() { + Map props = new HashMap<>(); + ... + props.put(StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, + RecoveringDeserializationExceptionHandler.class); + props.put(RecoveringDeserializationExceptionHandler.KSTREAM_DESERIALIZATION_RECOVERER, recoverer()); + ... + return new KafkaStreamsConfiguration(props); +} + +@Bean +public DeadLetterPublishingRecoverer recoverer() { + return new DeadLetterPublishingRecoverer(kafkaTemplate(), + (record, ex) -> new TopicPartition("recovererDLQ", -1)); +} +``` + +当然,`recoverer()` Bean 可以是你自己的`ConsumerRecordRecoverer`的实现。 + +#### 4.2.10.Kafka Streams 示例 + +下面的示例结合了我们在本章中讨论的所有主题: + +``` +@Configuration +@EnableKafka +@EnableKafkaStreams +public static class KafkaStreamsConfig { + + @Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME) + public KafkaStreamsConfiguration kStreamsConfigs() { + Map props = new HashMap<>(); + props.put(StreamsConfig.APPLICATION_ID_CONFIG, "testStreams"); + props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.Integer().getClass().getName()); + props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName()); + props.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG, WallclockTimestampExtractor.class.getName()); + return new KafkaStreamsConfiguration(props); + } + + @Bean + public StreamsBuilderFactoryBeanConfigurer configurer() { + return fb -> fb.setStateListener((newState, oldState) -> { + System.out.println("State transition from " + oldState + " to " + newState); + }); + } + + @Bean + public KStream kStream(StreamsBuilder kStreamBuilder) { + KStream stream = kStreamBuilder.stream("streamingTopic1"); + stream + .mapValues((ValueMapper) String::toUpperCase) + .groupByKey() + .windowedBy(TimeWindows.of(Duration.ofMillis(1000))) + .reduce((String value1, String value2) -> value1 + value2, + Named.as("windowStore")) + .toStream() + .map((windowedId, value) -> new KeyValue<>(windowedId.key(), value)) + .filter((i, s) -> s.length() > 40) + .to("streamingTopic2"); + + stream.print(Printed.toSysOut()); + + return stream; + } + +} +``` + +### 4.3.测试应用程序 + +`spring-kafka-test`JAR 包含一些有用的实用程序,以帮助测试你的应用程序。 + +#### 4.3.1.Kafkatestutils + +`o.s.kafka.test.utils.KafkaTestUtils`提供了许多静态助手方法来使用记录、检索各种记录偏移量以及其他方法。有关完整的详细信息,请参阅其[Javadocs](https://docs.spring.io/spring-kafka/docs/current/api/org/springframework/kafka/test/utils/KafkaTestUtils.html)。 + +#### 4.3.2.朱尼特 + +`o.s.kafka.test.utils.KafkaTestUtils`还提供了一些静态方法来设置生产者和消费者属性。下面的清单显示了这些方法签名: + +``` +/** + * Set up test properties for an {@code } consumer. + * @param group the group id. + * @param autoCommit the auto commit. + * @param embeddedKafka a {@link EmbeddedKafkaBroker} instance. + * @return the properties. + */ +public static Map consumerProps(String group, String autoCommit, + EmbeddedKafkaBroker embeddedKafka) { ... } + +/** + * Set up test properties for an {@code } producer. + * @param embeddedKafka a {@link EmbeddedKafkaBroker} instance. + * @return the properties. + */ +public static Map producerProps(EmbeddedKafkaBroker embeddedKafka) { ... } +``` + +| |从版本 2.5 开始,`consumerProps`方法将`ConsumerConfig.AUTO_OFFSET_RESET_CONFIG`设置为`earliest`。
这是因为,在大多数情况下,你希望使用者使用在测试用例中发送的任何消息。
`ConsumerConfig`默认值是`latest`,这意味着在使用者开始之前,已经通过测试发送的消息将不会收到这些记录。,
恢复到以前的行为,在调用该方法之后,将属性设置为`latest`

当使用嵌入式代理时,通常最好的做法是为每个测试使用不同的主题,以防止交叉对话。,
如果由于某种原因这是不可能的,请注意,`consumeFromEmbeddedTopics`方法的默认行为是在分配之后将分配的分区查找到开始处,
因为它无法访问消费者属性,你必须使用重载方法,该方法接受`seekToEnd`布尔参数,以查找到结束而不是开始。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +为`EmbeddedKafkaBroker`提供了一个 JUnit4`@Rule`包装器,用于创建嵌入式 Kafka 和嵌入式 ZooKeeper 服务器。(有关使用`@EmbeddedKafka`与 JUnit5 一起使用`@EmbeddedKafka`的信息,请参见[@Embeddedkafka 注释](#embedded-kafka-annotation))。下面的清单显示了这些方法的签名: + +``` +/** + * Create embedded Kafka brokers. + * @param count the number of brokers. + * @param controlledShutdown passed into TestUtils.createBrokerConfig. + * @param topics the topics to create (2 partitions per). + */ +public EmbeddedKafkaRule(int count, boolean controlledShutdown, String... topics) { ... } + +/** + * + * Create embedded Kafka brokers. + * @param count the number of brokers. + * @param controlledShutdown passed into TestUtils.createBrokerConfig. + * @param partitions partitions per topic. + * @param topics the topics to create. + */ +public EmbeddedKafkaRule(int count, boolean controlledShutdown, int partitions, String... topics) { ... } +``` + +`EmbeddedKafkaBroker`类有一个实用程序方法,它允许你使用它创建的所有主题。下面的示例展示了如何使用它: + +``` +Map consumerProps = KafkaTestUtils.consumerProps("testT", "false", embeddedKafka); +DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory( + consumerProps); +Consumer consumer = cf.createConsumer(); +embeddedKafka.consumeFromAllEmbeddedTopics(consumer); +``` + +`KafkaTestUtils`有一些实用方法来从使用者那里获取结果。下面的清单显示了这些方法签名: + +``` +/** + * Poll the consumer, expecting a single record for the specified topic. + * @param consumer the consumer. + * @param topic the topic. + * @return the record. + * @throws org.junit.ComparisonFailure if exactly one record is not received. + */ +public static ConsumerRecord getSingleRecord(Consumer consumer, String topic) { ... } + +/** + * Poll the consumer for records. + * @param consumer the consumer. + * @return the records. + */ +public static ConsumerRecords getRecords(Consumer consumer) { ... } +``` + +下面的示例展示了如何使用`KafkaTestUtils`: + +``` +... +template.sendDefault(0, 2, "bar"); +ConsumerRecord received = KafkaTestUtils.getSingleRecord(consumer, "topic"); +... +``` + +当`EmbeddedKafkaBroker`启动嵌入式 Kafka 和嵌入式 ZooKeeper 服务器时,将名为`spring.embedded.kafka.brokers`的系统属性设置为 Kafka 代理的地址,并将名为`spring.embedded.zookeeper.connect`的系统属性设置为 ZooKeeper 的地址。为此属性提供了方便的常量(`EmbeddedKafkaBroker.SPRING_EMBEDDED_KAFKA_BROKERS`和`EmbeddedKafkaBroker.SPRING_EMBEDDED_ZOOKEEPER_CONNECT`)。 + +使用`EmbeddedKafkaBroker.brokerProperties(Map)`,你可以为 Kafka 服务器提供其他属性。有关可能的代理属性的更多信息,请参见[Kafka配置](https://kafka.apache.org/documentation/#brokerconfigs)。 + +#### 4.3.3.配置主题 + +下面的示例配置创建了带有五个分区的`cat`和`hat`主题,带有 10 个分区的`thing1`主题,以及带有 15 个分区的`thing2`主题: + +``` +public class MyTests { + + @ClassRule + private static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, false, 5, "cat", "hat"); + + @Test + public void test() { + embeddedKafkaRule.getEmbeddedKafka() + .addTopics(new NewTopic("thing1", 10, (short) 1), new NewTopic("thing2", 15, (short) 1)); + ... + } + +} +``` + +默认情况下,`addTopics`在出现问题(例如添加已经存在的主题)时将抛出异常。版本 2.6 添加了该方法的新版本,该版本返回`Map`;关键是主题名称,对于成功,值是`null`,对于失败,值是`Exception`。 + +#### 4.3.4.对多个测试类使用相同的代理 + +这样做并没有内置的支持,但是你可以使用相同的代理对多个测试类进行类似于以下的操作: + +``` +public final class EmbeddedKafkaHolder { + + private static EmbeddedKafkaBroker embeddedKafka = new EmbeddedKafkaBroker(1, false) + .brokerListProperty("spring.kafka.bootstrap-servers"); + + private static boolean started; + + public static EmbeddedKafkaBroker getEmbeddedKafka() { + if (!started) { + try { + embeddedKafka.afterPropertiesSet(); + } + catch (Exception e) { + throw new KafkaException("Embedded broker failed to start", e); + } + started = true; + } + return embeddedKafka; + } + + private EmbeddedKafkaHolder() { + super(); + } + +} +``` + +这假定启动环境为 Spring,并且嵌入式代理替换了 BootStrap Servers 属性。 + +然后,在每个测试类中,你可以使用类似于以下内容的内容: + +``` +static { + EmbeddedKafkaHolder.getEmbeddedKafka().addTopics("topic1", "topic2"); +} + +private static final EmbeddedKafkaBroker broker = EmbeddedKafkaHolder.getEmbeddedKafka(); +``` + +如果不使用 Spring boot,则可以使用`broker.getBrokersAsString()`获得 bootstrap 服务器。 + +| |前面的示例没有提供在所有测试完成后关闭代理的机制,
如果你在 Gradle 守护程序中运行测试,这可能是个问题,
在这种情况下,你不应该使用这种技术,或者,当测试完成时,你应该在`EmbeddedKafkaBroker`上使用调用`destroy()`的方法。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.3.5.@Embeddedkafka 注释 + +我们通常建议你使用`@ClassRule`规则,以避免在测试之间启动和停止代理(并为每个测试使用不同的主题)。从版本 2.0 开始,如果使用 Spring 的测试应用程序上下文缓存,还可以声明`EmbeddedKafkaBroker` Bean,因此单个代理可以跨多个测试类使用。为了方便起见,我们提供了一个名为`@EmbeddedKafka`的测试类级注释来注册`EmbeddedKafkaBroker` Bean。下面的示例展示了如何使用它: + +``` +@RunWith(SpringRunner.class) +@DirtiesContext +@EmbeddedKafka(partitions = 1, + topics = { + KafkaStreamsTests.STREAMING_TOPIC1, + KafkaStreamsTests.STREAMING_TOPIC2 }) +public class KafkaStreamsTests { + + @Autowired + private EmbeddedKafkaBroker embeddedKafka; + + @Test + public void someTest() { + Map consumerProps = KafkaTestUtils.consumerProps("testGroup", "true", this.embeddedKafka); + consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + ConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); + Consumer consumer = cf.createConsumer(); + this.embeddedKafka.consumeFromAnEmbeddedTopic(consumer, KafkaStreamsTests.STREAMING_TOPIC2); + ConsumerRecords replies = KafkaTestUtils.getRecords(consumer); + assertThat(replies.count()).isGreaterThanOrEqualTo(1); + } + + @Configuration + @EnableKafkaStreams + public static class KafkaStreamsConfiguration { + + @Value("${" + EmbeddedKafkaBroker.SPRING_EMBEDDED_KAFKA_BROKERS + "}") + private String brokerAddresses; + + @Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME) + public KafkaStreamsConfiguration kStreamsConfigs() { + Map props = new HashMap<>(); + props.put(StreamsConfig.APPLICATION_ID_CONFIG, "testStreams"); + props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, this.brokerAddresses); + return new KafkaStreamsConfiguration(props); + } + + } + +} +``` + +从版本 2.2.4 开始,你还可以使用`@EmbeddedKafka`注释来指定 Kafka Ports 属性。 + +下面的示例设置`topics`、`brokerProperties`和`brokerPropertiesLocation`属性的`@EmbeddedKafka`支持属性占位符解析: + +``` +@TestPropertySource(locations = "classpath:/test.properties") +@EmbeddedKafka(topics = { "any-topic", "${kafka.topics.another-topic}" }, + brokerProperties = { "log.dir=${kafka.broker.logs-dir}", + "listeners=PLAINTEXT://localhost:${kafka.broker.port}", + "auto.create.topics.enable=${kafka.broker.topics-enable:true}" }, + brokerPropertiesLocation = "classpath:/broker.properties") +``` + +在前面的示例中,属性占位符`${kafka.topics.another-topic}`、`${kafka.broker.logs-dir}`和`${kafka.broker.port}`是从 Spring `Environment`解析的。此外,代理属性是从`broker.properties` Classpath 资源中加载的,该资源由`brokerPropertiesLocation`指定。属性占位符是为`brokerPropertiesLocation`URL 和资源中找到的任何属性占位符解析的。由`brokerProperties`定义的属性覆盖在`brokerPropertiesLocation`中找到的属性。 + +你可以在 JUnit4 或 JUnit5 中使用`@EmbeddedKafka`注释。 + +#### 4.3.6.@EmbeddedKafka 注释与 JUnit5 + +从版本 2.3 开始,有两种方法可以使用 JUnit5 的`@EmbeddedKafka`注释。当与`@SpringJunitConfig`注释一起使用时,嵌入式代理将添加到测试应用程序上下文中。你可以在类或方法级别将代理自动连接到你的测试中,以获得代理地址列表。 + +当**不是**使用 Spring 测试上下文时,`EmbdeddedKafkaCondition`将创建代理;该条件包括一个参数解析程序,因此你可以在测试方法中访问代理… + +``` +@EmbeddedKafka +public class EmbeddedKafkaConditionTests { + + @Test + public void test(EmbeddedKafkaBroker broker) { + String brokerList = broker.getBrokersAsString(); + ... + } + +} +``` + +如果用`ExtendedWith(SpringExtension.class)`注释的类也没有用`ExtendedWith(SpringExtension.class)`注释(或 meta 注释),则将创建一个独立的(而不是 Spring 测试上下文)代理。`@SpringJunitConfig`和`@SpringBootTest`是这样的元注释,并且基于上下文的代理将在也存在这些注释时使用。 + +| |当有 Spring 可用的测试应用程序上下文时,topics 和 broker 属性可以包含属性占位符,只要在某个地方定义了属性,这些占位符就会被解析。
如果没有 Spring 可用的上下文,这些占位符就不会被解析。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.3.7.`@SpringBootTest`注释中的嵌入式代理 + +[Spring Initializr](https://start.spring.io/)现在自动将测试范围中的`spring-kafka-test`依赖项添加到项目配置中。 + +| |如果你的应用程序使用`spring-cloud-stream`中的 Kafka 活页夹,并且如果你想使用嵌入式代理进行测试,则必须删除`spring-cloud-stream-test-support`依赖项,因为它用测试用例的测试绑定器替换了实际的绑定器。
如果你希望某些测试使用测试绑定器,而某些测试使用嵌入式代理,使用真实活页夹的测试需要通过排除测试类中的活页夹自动配置来禁用测试活页夹。
下面的示例展示了如何这样做:

```
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.autoconfigure.exclude="
+ "org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration")
public class MyApplicationTests {
...
}
```| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在 Spring 引导应用程序测试中有几种使用嵌入式代理的方法。 + +它们包括: + +* [JUnit4 类规则](#kafka-testing-junit4-class-rule) + +* [`@EmbeddedKafka`注释或`EmbeddedKafkaBroker` Bean(#kafka-testing-embeddedkafka-annotation) + +##### JUnit4 类规则 + +下面的示例展示了如何使用 JUnit4 类规则来创建嵌入式代理: + +``` +@RunWith(SpringRunner.class) +@SpringBootTest +public class MyApplicationTests { + + @ClassRule + public static EmbeddedKafkaRule broker = new EmbeddedKafkaRule(1, + false, "someTopic") + .brokerListProperty("spring.kafka.bootstrap-servers"); + } + + @Autowired + private KafkaTemplate template; + + @Test + public void test() { + ... + } + +} +``` + +注意,由于这是一个 Spring 引导应用程序,因此我们将覆盖代理列表属性以设置引导属性。 + +##### `@EmbeddedKafka`注释或`EmbeddedKafkaBroker` Bean + +下面的示例展示了如何使用`@EmbeddedKafka`注释来创建嵌入式代理: + +``` +@RunWith(SpringRunner.class) +@EmbeddedKafka(topics = "someTopic", + bootstrapServersProperty = "spring.kafka.bootstrap-servers") +public class MyApplicationTests { + + @Autowired + private KafkaTemplate template; + + @Test + public void test() { + ... + } + +} +``` + +#### 4.3.8.汉克雷斯特火柴人 + +`o.s.kafka.test.hamcrest.KafkaMatchers`提供了以下匹配器: + +``` +/** + * @param key the key + * @param the type. + * @return a Matcher that matches the key in a consumer record. + */ +public static Matcher> hasKey(K key) { ... } + +/** + * @param value the value. + * @param the type. + * @return a Matcher that matches the value in a consumer record. + */ +public static Matcher> hasValue(V value) { ... } + +/** + * @param partition the partition. + * @return a Matcher that matches the partition in a consumer record. + */ +public static Matcher> hasPartition(int partition) { ... } + +/** + * Matcher testing the timestamp of a {@link ConsumerRecord} assuming the topic has been set with + * {@link org.apache.kafka.common.record.TimestampType#CREATE_TIME CreateTime}. + * + * @param ts timestamp of the consumer record. + * @return a Matcher that matches the timestamp in a consumer record. + */ +public static Matcher> hasTimestamp(long ts) { + return hasTimestamp(TimestampType.CREATE_TIME, ts); +} + +/** + * Matcher testing the timestamp of a {@link ConsumerRecord} + * @param type timestamp type of the record + * @param ts timestamp of the consumer record. + * @return a Matcher that matches the timestamp in a consumer record. + */ +public static Matcher> hasTimestamp(TimestampType type, long ts) { + return new ConsumerRecordTimestampMatcher(type, ts); +} +``` + +#### 4.3.9.AssertJ 条件 + +你可以使用以下 AssertJ 条件: + +``` +/** + * @param key the key + * @param the type. + * @return a Condition that matches the key in a consumer record. + */ +public static Condition> key(K key) { ... } + +/** + * @param value the value. + * @param the type. + * @return a Condition that matches the value in a consumer record. + */ +public static Condition> value(V value) { ... } + +/** + * @param key the key. + * @param value the value. + * @param the key type. + * @param the value type. + * @return a Condition that matches the key in a consumer record. + * @since 2.2.12 + */ +public static Condition> keyValue(K key, V value) { ... } + +/** + * @param partition the partition. + * @return a Condition that matches the partition in a consumer record. + */ +public static Condition> partition(int partition) { ... } + +/** + * @param value the timestamp. + * @return a Condition that matches the timestamp value in a consumer record. + */ +public static Condition> timestamp(long value) { + return new ConsumerRecordTimestampCondition(TimestampType.CREATE_TIME, value); +} + +/** + * @param type the type of timestamp + * @param value the timestamp. + * @return a Condition that matches the timestamp value in a consumer record. + */ +public static Condition> timestamp(TimestampType type, long value) { + return new ConsumerRecordTimestampCondition(type, value); +} +``` + +#### 4.3.10.例子 + +下面的示例汇总了本章涵盖的大多数主题: + +``` +public class KafkaTemplateTests { + + private static final String TEMPLATE_TOPIC = "templateTopic"; + + @ClassRule + public static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, true, TEMPLATE_TOPIC); + + @Test + public void testTemplate() throws Exception { + Map consumerProps = KafkaTestUtils.consumerProps("testT", "false", + embeddedKafka.getEmbeddedKafka()); + DefaultKafkaConsumerFactory cf = + new DefaultKafkaConsumerFactory(consumerProps); + ContainerProperties containerProperties = new ContainerProperties(TEMPLATE_TOPIC); + KafkaMessageListenerContainer container = + new KafkaMessageListenerContainer<>(cf, containerProperties); + final BlockingQueue> records = new LinkedBlockingQueue<>(); + container.setupMessageListener(new MessageListener() { + + @Override + public void onMessage(ConsumerRecord record) { + System.out.println(record); + records.add(record); + } + + }); + container.setBeanName("templateTests"); + container.start(); + ContainerTestUtils.waitForAssignment(container, + embeddedKafka.getEmbeddedKafka().getPartitionsPerTopic()); + Map producerProps = + KafkaTestUtils.producerProps(embeddedKafka.getEmbeddedKafka()); + ProducerFactory pf = + new DefaultKafkaProducerFactory(producerProps); + KafkaTemplate template = new KafkaTemplate<>(pf); + template.setDefaultTopic(TEMPLATE_TOPIC); + template.sendDefault("foo"); + assertThat(records.poll(10, TimeUnit.SECONDS), hasValue("foo")); + template.sendDefault(0, 2, "bar"); + ConsumerRecord received = records.poll(10, TimeUnit.SECONDS); + assertThat(received, hasKey(2)); + assertThat(received, hasPartition(0)); + assertThat(received, hasValue("bar")); + template.send(TEMPLATE_TOPIC, 0, 2, "baz"); + received = records.poll(10, TimeUnit.SECONDS); + assertThat(received, hasKey(2)); + assertThat(received, hasPartition(0)); + assertThat(received, hasValue("baz")); + } + +} +``` + +前面的示例使用了 Hamcrest Matchers。使用`AssertJ`,最后一部分看起来像以下代码: + +``` +assertThat(records.poll(10, TimeUnit.SECONDS)).has(value("foo")); +template.sendDefault(0, 2, "bar"); +ConsumerRecord received = records.poll(10, TimeUnit.SECONDS); +// using individual assertions +assertThat(received).has(key(2)); +assertThat(received).has(value("bar")); +assertThat(received).has(partition(0)); +template.send(TEMPLATE_TOPIC, 0, 2, "baz"); +received = records.poll(10, TimeUnit.SECONDS); +// using allOf() +assertThat(received).has(allOf(keyValue(2, "baz"), partition(0))); +``` + +### 4.4.非阻塞重试 + +| |这是一个实验性的功能,通常的不中断 API 更改的规则不适用于此功能,直到删除了实验性的指定。
鼓励用户尝试该功能并通过 GitHub 问题或 GitHub 讨论提供反馈。
这仅与 API 有关;该功能被认为是完整且健壮的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +使用 Kafka 实现非阻塞重试/DLT 功能通常需要设置额外的主题并创建和配置相应的侦听器。由于 2.7 Spring for Apache,Kafka 通过`@RetryableTopic`注释和`RetryTopicConfiguration`类提供了对此的支持,以简化该引导。 + +#### 4.4.1.模式的工作原理 + +如果消息处理失败,该消息将被转发到带有后退时间戳的重试主题。然后,重试主题使用者检查时间戳,如果没有到期,它会暂停该主题分区的消耗。当它到期时,将恢复分区消耗,并再次消耗消息。如果消息处理再次失败,则消息将被转发到下一个重试主题,并重复该模式,直到处理成功,或者尝试已尽,并将消息发送到死信主题(如果已配置)。 + +为了说明这一点,如果你有一个“main-topic”主题,并且希望设置非阻塞重试,该重试的指数回退为 1000ms,乘数为 2 和 4max,那么它将创建 main-topic-retry-1000、main-topic-retry-2000、main-topic-retry-4000 和 main-topic-dlt 主题,并配置相应的消费者。该框架还负责创建主题以及设置和配置侦听器。 + +| |通过使用这种策略,你将失去Kafka对该主题的排序保证。| +|---|---------------------------------------------------------------------------| + +| |你可以设置你喜欢的`AckMode`模式,但建议使用`RECORD`模式。| +|---|---------------------------------------------------------------------| + +| |目前,此功能不支持类级别`@KafkaListener`注释| +|---|----------------------------------------------------------------------------------------| + +#### 4.4.2.退后延迟精度 + +##### 概述和保证 + +所有的消息处理和退线都由使用者线程处理,因此,在尽力而为的基础上保证了延迟精度。如果一条消息的处理时间超过了下一条消息对该消费者的回退期,则下一条消息的延迟将高于预期。此外,对于较短的延迟(大约 1s 或更短),线程必须进行的维护工作(例如提交偏移)可能会延迟消息处理的执行。如果重试主题的使用者正在处理多个分区,则精度也会受到影响,因为我们依赖于从轮询中唤醒使用者并具有完整的 polltimeouts 来进行时间调整。 + +话虽如此,对于处理单个分区的消费者来说,在大多数情况下,消息的处理时间应该在 100ms 以下。 + +| |保证一条消息在到期前永远不会被处理。| +|---|----------------------------------------------------------------------------| + +##### 调整延迟精度 + +消息的处理延迟精度依赖于两个`ContainerProperties`:`ContainerProperties.pollTimeout`和`ContainerProperties.idlePartitionEventInterval`。这两个属性将在重试主题和 DLT 的`ListenerContainerFactory`中自动设置为该主题最小延迟值的四分之一,最小值为 250ms,最大值为 5000ms。只有当属性有其默认值时,才会设置这些值-如果你自己更改其中一个值,你的更改将不会被重写。通过这种方式,你可以根据需要调整重试主题的精度和性能。 + +| |你可以为 main 和 retry 主题设置单独的`ListenerContainerFactory`实例-这样你就可以设置不同的设置,以更好地满足你的需求,例如,为 main 主题设置更高的轮询超时设置,为 retry 主题设置更低的轮询超时设置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.4.3.配置 + +##### 使用`@RetryableTopic`注释 + +要为`@KafkaListener`注释方法配置重试主题和 DLT,只需向其添加`@RetryableTopic`注释,而 Spring 对于 Apache Kafka 将使用默认配置引导所有必要的主题和使用者。 + +``` +@RetryableTopic(kafkaTemplate = "myRetryableTopicKafkaTemplate") +@KafkaListener(topics = "my-annotated-topic", groupId = "myGroupId") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +你可以在同一个类中指定一个方法,通过使用`@DltHandler`注释来处理 DLT 消息。如果没有提供 Dlthandler 方法,则创建一个默认的使用者,该使用者只记录消费。 + +``` +@DltHandler +public void processMessage(MyPojo message) { +// ... message processing, persistence, etc +} +``` + +| |如果你没有指定 Kafkatemplate 名称,则将查找名称为`retryTopicDefaultKafkaTemplate`的 Bean。
如果没有找到 Bean,则抛出异常。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用`RetryTopicConfiguration`bean + +你还可以通过在`@Configuration`带注释的类中创建`RetryTopicConfiguration`bean 来配置非阻塞重试支持。 + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .create(template); +} +``` + +这将为使用默认配置以“@Kafkalistener”注释的方法中的所有主题创建重试主题和 DLT,以及相应的消费者。消息转发需要`KafkaTemplate`实例。 + +为了实现对如何处理每个主题的非阻塞重试的更细粒度的控制,可以提供一个以上的`RetryTopicConfiguration` Bean。 + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .fixedBackoff(3000) + .maxAttempts(5) + .includeTopics("my-topic", "my-other-topic") + .create(template); +} + +@Bean +public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .exponentialBackoff(1000, 2, 5000) + .maxAttempts(4) + .excludeTopics("my-topic", "my-other-topic") + .retryOn(MyException.class) + .create(template); +} +``` + +| |重试主题和 DLT 的消费者将被分配给一个消费者组,该组 ID 是你在`groupId`参数`@KafkaListener`中提供的带有该主题后缀的注释的组 ID 的组合。如果你不提供,他们都属于同一个组,在重试主题上的再平衡将导致在主主题上的不必要的再平衡。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果使用者配置了一个[`ErrorHandlingDeserializer`](#error-handling-deserializer),要处理荒漠化异常,就必须使用一个序列化器配置`KafkaTemplate`及其生成器,该序列化器可以处理普通对象以及 RAW`byte[]`值,这是反序列化异常的结果。
模板的泛型值类型应该是`Object`。
一种技术是使用`DelegatingByTypeSerializer`;示例如下:| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Bean +public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(), + new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(), + MyNormalObject.class, new JsonSerializer()))); +} + +@Bean +public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); +} +``` + +#### 4.4.4.特征 + +大多数特性都适用于`@RetryableTopic`注释和`RetryTopicConfiguration`bean。 + +##### 退避配置 + +退避配置依赖于`Spring Retry`项目中的`BackOffPolicy`接口。 + +它包括: + +* 固定后退 + +* 指数式后退 + +* 随机指数回退 + +* 均匀随机退避 + +* 不退缩 + +* 自定义后退 + +``` +@RetryableTopic(attempts = 5, + backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000)) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .fixedBackoff(3000) + .maxAttempts(4) + .build(); +} +``` + +还可以提供 Spring Retry 的`SleepingBackOffPolicy`的自定义实现: + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .customBackOff(new MyCustomBackOffPolicy()) + .maxAttempts(5) + .build(); +} +``` + +| |默认的退避策略是 FixedBackOffPolicy,最大尝试次数为 3 次,间隔时间为 1000ms。| +|---|---------------------------------------------------------------------------------------------------| + +| |第一次尝试与 maxtripts 相对应,因此,如果你提供的 maxtripes 值为 4,那么将出现原始尝试加 3 次重试。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------| + +##### 单话题固定延迟重试 + +如果你使用固定的延迟策略,例如`FixedBackOffPolicy`或`NoBackOffPolicy`,你可以使用一个主题来完成非阻塞重试。此主题将使用提供的或默认的后缀作为后缀,并且不会附加索引或延迟值。 + +``` +@RetryableTopic(backoff = @Backoff(2000), fixedDelayTopicStrategy = FixedDelayStrategy.SINGLE_TOPIC) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .fixedBackoff(3000) + .maxAttempts(5) + .useSingleTopicForFixedDelays() + .build(); +} +``` + +| |默认的行为是为每次尝试创建单独的重试主题,并附上它们的索引值:retry-0、retry-1、…| +|---|------------------------------------------------------------------------------------------------------------------------------| + +##### 全局超时 + +你可以为重试过程设置全局超时。如果达到了这个时间,则下一次使用者抛出异常时,消息将直接传递到 DLT,或者如果没有可用的 DLT,消息将结束处理。 + +``` +@RetryableTopic(backoff = @Backoff(2000), timeout = 5000) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .fixedBackoff(2000) + .timeoutAfter(5000) + .build(); +} +``` + +| |默认值是没有超时设置的,这也可以通过提供-1 作为超时值来实现。| +|---|-----------------------------------------------------------------------------------------------------| + +##### 异常分类器 + +你可以指定要重试的异常和不要重试的异常。你还可以将其设置为遍历原因以查找嵌套的异常。 + +``` +@RetryableTopic(include = {MyRetryException.class, MyOtherRetryException.class}, traversingCauses = true) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + throw new RuntimeException(new MyRetryException()); // Will retry +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .notRetryOn(MyDontRetryException.class) + .create(template); +} +``` + +| |默认的行为是对所有异常进行重试,而不是遍历原因。| +|---|-----------------------------------------------------------------------------| + +从 2.8.3 开始,有一个致命异常的全局列表,它将导致记录在没有任何重试的情况下被发送到 DLT。有关致命异常的默认列表,请参见[违约恐怖处理者](#default-eh)。你可以通过以下方式向该列表添加或删除异常: + +``` +@Bean(name = RetryTopicInternalBeanNames.DESTINATION_TOPIC_CONTAINER_NAME) +public DefaultDestinationTopicResolver topicResolver(ApplicationContext applicationContext, + @Qualifier(RetryTopicInternalBeanNames + .INTERNAL_BACKOFF_CLOCK_BEAN_NAME) Clock clock) { + DefaultDestinationTopicResolver ddtr = new DefaultDestinationTopicResolver(clock, applicationContext); + ddtr.addNotRetryableExceptions(MyFatalException.class); + ddtr.removeNotRetryableException(ConversionException.class); + return ddtr; +} +``` + +| |要禁用致命异常的分类,请使用`setClassifications`中的`DefaultDestinationTopicResolver`方法清除默认列表。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +##### 包含和排除主题 + +你可以通过.includeTopic(字符串主题)、.includeTopics(集合 \主题)、.excludeTopic(字符串主题)和.excludeTopics(集合 \主题)方法来决定哪些主题将由 Bean 处理。 + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .includeTopics(List.of("my-included-topic", "my-other-included-topic")) + .create(template); +} + +@Bean +public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .excludeTopic("my-excluded-topic") + .create(template); +} +``` + +| |默认的行为是包含所有主题。| +|---|----------------------------------------------| + +##### topics 自动创建 + +除非另有说明,否则框架将使用`NewTopic`bean 自动创建所需的主题,这些 bean 由`KafkaAdmin` Bean 使用。你可以指定创建主题所使用的分区数量和复制因子,并且可以关闭此功能。 + +| |请注意,如果你不使用 Spring boot,则必须提供 KafkaAdmin Bean 才能使用此功能。| +|---|----------------------------------------------------------------------------------------------------------------| + +``` +@RetryableTopic(numPartitions = 2, replicationFactor = 3) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} + +@RetryableTopic(autoCreateTopics = false) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .autoCreateTopicsWith(2, 3) + .create(template); +} + +@Bean +public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .doNotAutoCreateRetryTopics() + .create(template); +} +``` + +| |默认情况下,主题是用一个分区和一个复制因子自动创建的。| +|---|-----------------------------------------------------------------------------------------| + +##### 故障报头管理 + +在考虑如何管理故障报头(原始报头和异常报头)时,框架将委托给`DeadLetterPublishingRecover`,以决定是否追加或替换报头。 + +默认情况下,它显式地将`appendOriginalHeaders`设置为`false`,并将`stripPreviousExceptionHeaders`设置为`DeadLetterPublishingRecover`使用的默认值。 + +这意味着默认配置只保留第一个“原始”和最后一个异常标头。这是为了避免在涉及许多重试步骤时创建过大的消息(例如,由于堆栈跟踪标头)。 + +有关更多信息,请参见[管理死信记录头](#dlpr-headers)。 + +要重新配置框架以对这些属性使用不同的设置,请通过添加`recovererCustomizer`来替换标准`DeadLetterPublishingRecovererFactory` Bean: + +``` +@Bean(RetryTopicInternalBeanNames.DEAD_LETTER_PUBLISHING_RECOVERER_FACTORY_BEAN_NAME) +DeadLetterPublishingRecovererFactory factory(DestinationTopicResolver resolver) { + DeadLetterPublishingRecovererFactory factory = new DeadLetterPublishingRecovererFactory(resolver); + factory.setDeadLetterPublishingRecovererCustomizer(dlpr -> { + dlpr.appendOriginalHeaders(true); + dlpr.setStripPreviousExceptionHeaders(false); + }); + return factory; +} +``` + +#### 4.4.5.主题命名 + +Retry Topics 和 DLT 的命名方法是使用提供的或默认值对主主题进行后缀,并附加该主题的延迟或索引。 + +例子: + +“my-topic”“my-topic-retry-0”,“my-topic-retry-1”,…,“my-topic-dlt” + +“my-other-topic”“my-topic-myretrySuffix-1000”,“my-topic-myretrySuffix-2000”,…,“my-topic-mydltSufix”。 + +##### 重试主题和 DLT 后缀 + +你可以指定 Retry 和 DLT 主题将使用的后缀。 + +``` +@RetryableTopic(retryTopicSuffix = "-my-retry-suffix", dltTopicSuffix = "-my-dlt-suffix") +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .retryTopicSuffix("-my-retry-suffix") + .dltTopicSuffix("-my-dlt-suffix") + .create(template); +} +``` + +| |默认后缀是“-retry”和“-dlt”,分别用于重试主题和 DLT。| +|---|------------------------------------------------------------------------------------| + +##### 附加主题索引或延迟 + +你可以在后缀之后追加主题的索引值,也可以在后缀之后追加延迟值。 + +``` +@RetryableTopic(topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .suffixTopicsWithIndexValues() + .create(template); + } +``` + +| |默认的行为是使用延迟值作为后缀,除了具有多个主题的固定延迟配置,在这种情况下,主题以主题的索引作为后缀。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 自定义命名策略 + +可以通过注册实现`RetryTopicNamesProviderFactory`的 Bean 来实现更复杂的命名策略。默认实现是`SuffixingRetryTopicNamesProviderFactory`,可以通过以下方式注册不同的实现: + +``` +@Bean +public RetryTopicNamesProviderFactory myRetryNamingProviderFactory() { + return new CustomRetryTopicNamesProviderFactory(); +} +``` + +作为示例,下面的实现除了标准后缀之外,还添加了一个前缀来 Retry/DL 主题名称: + +``` +public class CustomRetryTopicNamesProviderFactory implements RetryTopicNamesProviderFactory { + + @Override + public RetryTopicNamesProvider createRetryTopicNamesProvider( + DestinationTopic.Properties properties) { + + if(properties.isMainEndpoint()) { + return new SuffixingRetryTopicNamesProvider(properties); + } + else { + return new SuffixingRetryTopicNamesProvider(properties) { + + @Override + public String getTopicName(String topic) { + return "my-prefix-" + super.getTopicName(topic); + } + + }; + } + } + +} +``` + +#### 4.4.6.DLT 策略 + +该框架为使用 DLTS 提供了一些策略。你可以提供用于 DLT 处理的方法,也可以使用默认的日志记录方法,或者根本没有 DLT。你还可以选择如果 DLT 处理失败会发生什么。 + +##### DLT 处理方法 + +你可以指定用于处理该主题的 DLT 的方法,以及在处理失败时的行为。 + +要做到这一点,你可以在具有`@RetryableTopic`注释的类的方法中使用`@DltHandler`注释。请注意,相同的方法将用于该类中的所有`@RetryableTopic`注释方法。 + +``` +@RetryableTopic +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} + +@DltHandler +public void processMessage(MyPojo message) { +// ... message processing, persistence, etc +} +``` + +DLT 处理程序方法也可以通过 RetryTopicConfigurationBuilder.dlthandlerMethod 方法提供,将处理 DLT 消息的 Bean 名称和方法名称作为参数传递。 + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .dltProcessor("myCustomDltProcessor", "processDltMessage") + .create(template); +} + +@Component +public class MyCustomDltProcessor { + + private final MyDependency myDependency; + + public MyCustomDltProcessor(MyDependency myDependency) { + this.myDependency = myDependency; + } + + public void processDltMessage(MyPojo message) { + // ... message processing, persistence, etc + } +} +``` + +| |如果没有提供 DLT 处理程序,则使用默认的 retrytopicconfigurer.loggingdltListenerHandlerMethod。| +|---|--------------------------------------------------------------------------------------------------------| + +从版本 2.8 开始,如果你根本不想在此应用程序中使用 DLT,包括通过默认处理程序(或者你希望延迟使用),则可以控制 DLT 容器是否开始,这与容器工厂的`autoStartup`属性无关。 + +当使用`@RetryableTopic`注释时,将`autoStartDltHandler`属性设置为`false`;当使用配置生成器时,使用`.autoStartDltHandler(false)`。 + +稍后可以通过`KafkaListenerEndpointRegistry`启动 DLT 处理程序。 + +##### DLT 故障行为 + +如果 DLT 处理失败,有两种可能的行为可用:`ALWAYS_RETRY_ON_ERROR`和`FAIL_ON_ERROR`。 + +在前者中,记录被转发回 DLT 主题,因此它不会阻止其他 DLT 记录的处理。在后一种情况下,使用者在不转发消息的情况下结束执行。 + +``` +@RetryableTopic(dltProcessingFailureStrategy = + DltStrategy.FAIL_ON_ERROR) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .dltProcessor(MyCustomDltProcessor.class, "processDltMessage") + .doNotRetryOnDltFailure() + .create(template); +} +``` + +| |默认的行为是`ALWAYS_RETRY_ON_ERROR`。| +|---|---------------------------------------------------| + +| |从版本 2.8.3 开始,`ALWAYS_RETRY_ON_ERROR`将不会将一个记录路由回 DLT,如果该记录导致了一个致命的异常被抛出,
,例如`DeserializationException`,因为通常,这样的异常总是会被抛出。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +被认为是致命的例外是: + +* `DeserializationException` + +* `MessageConversionException` + +* `ConversionException` + +* `MethodArgumentResolutionException` + +* `NoSuchMethodException` + +* `ClassCastException` + +可以使用`DestinationTopicResolver` Bean 上的方法向该列表添加异常并从该列表中删除异常。 + +有关更多信息,请参见[异常分类器](#retry-topic-ex-classifier)。 + +##### 配置无 dlt + +该框架还提供了不为主题配置 DLT 的可能性。在这种情况下,在重审用尽之后,程序就结束了。 + +``` +@RetryableTopic(dltProcessingFailureStrategy = + DltStrategy.NO_DLT) +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .doNotConfigureDlt() + .create(template); +} +``` + +#### 4.4.7.指定 ListenerContainerFactory + +默认情况下,RetryTopic 配置将使用`@KafkaListener`注释中提供的工厂,但是你可以指定一个不同的工厂来创建 RetryTopic 和 DLT 侦听器容器。 + +对于`@RetryableTopic`注释,你可以提供工厂的 Bean 名称,并且使用`RetryTopicConfiguration` Bean 你可以提供 Bean 名称或实例本身。 + +``` +@RetryableTopic(listenerContainerFactory = "my-retry-topic-factory") +@KafkaListener(topics = "my-annotated-topic") +public void processMessage(MyPojo message) { + // ... message processing +} +``` + +``` +@Bean +public RetryTopicConfiguration myRetryTopic(KafkaTemplate template, + ConcurrentKafkaListenerContainerFactory factory) { + + return RetryTopicConfigurationBuilder + .newInstance() + .listenerFactory(factory) + .create(template); +} + +@Bean +public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate template) { + return RetryTopicConfigurationBuilder + .newInstance() + .listenerFactory("my-retry-topic-factory") + .create(template); +} +``` + +| |从 2.8.3 开始,你可以对可重试和不可重试的主题使用相同的工厂。| +|---|--------------------------------------------------------------------------------| + +如果需要将工厂配置行为恢复到 Prior2.8.3,则可以替换标准的`RetryTopicConfigurer` Bean,并将`useLegacyFactoryConfigurer`设置为`true`,例如: + +``` +@Bean(name = RetryTopicInternalBeanNames.RETRY_TOPIC_CONFIGURER) +public RetryTopicConfigurer retryTopicConfigurer(DestinationTopicProcessor destinationTopicProcessor, + ListenerContainerFactoryResolver containerFactoryResolver, + ListenerContainerFactoryConfigurer listenerContainerFactoryConfigurer, + BeanFactory beanFactory, + RetryTopicNamesProviderFactory retryTopicNamesProviderFactory) { + RetryTopicConfigurer retryTopicConfigurer = new RetryTopicConfigurer(destinationTopicProcessor, containerFactoryResolver, listenerContainerFactoryConfigurer, beanFactory, retryTopicNamesProviderFactory); + retryTopicConfigurer.useLegacyFactoryConfigurer(true); + return retryTopicConfigurer; +} +``` + +\==== 更改 KafkabackoffException 日志记录级别 + +当重试主题中的消息未到期消耗时,将抛出`KafkaBackOffException`。默认情况下,这种异常会在`DEBUG`级别记录,但是你可以通过在`ListenerContainerFactoryConfigurer`类中的`@Configuration`中设置错误处理程序自定义程序来更改此行为。 + +例如,要更改日志级别以发出警告,你可以添加: + +``` +@Bean(name = RetryTopicInternalBeanNames.LISTENER_CONTAINER_FACTORY_CONFIGURER_NAME) +public ListenerContainerFactoryConfigurer listenerContainer(KafkaConsumerBackoffManager kafkaConsumerBackoffManager, + DeadLetterPublishingRecovererFactory deadLetterPublishingRecovererFactory, + @Qualifier(RetryTopicInternalBeanNames + .INTERNAL_BACKOFF_CLOCK_BEAN_NAME) Clock clock) { + ListenerContainerFactoryConfigurer configurer = new ListenerContainerFactoryConfigurer(kafkaConsumerBackoffManager, deadLetterPublishingRecovererFactory, clock); + configurer.setErrorHandlerCustomizer(commonErrorHandler -> ((DefaultErrorHandler) commonErrorHandler).setLogLevel(KafkaException.Level.WARN)); + return configurer; +} +``` + +\== 提示、技巧和示例 + +\=== 手动分配所有分区 + +假设你总是希望读取所有分区的所有记录(例如,当使用压缩主题加载分布式缓存时),手动分配分区而不使用 Kafka 的组管理可能会很有用。当有许多分区时,这样做可能会很麻烦,因为你必须列出分区。如果分区的数量随着时间的推移而变化,这也是一个问题,因为每次分区数量发生变化时,你都必须重新编译应用程序。 + +下面是一个示例,说明如何在应用程序启动时使用 SPEL 表达式的能力动态地创建分区列表: + +``` +@KafkaListener(topicPartitions = @TopicPartition(topic = "compacted", + partitions = "#{@finder.partitions('compacted')}"), + partitionOffsets = @PartitionOffset(partition = "*", initialOffset = "0"))) +public void listen(@Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) String key, String payload) { + ... +} + +@Bean +public PartitionFinder finder(ConsumerFactory consumerFactory) { + return new PartitionFinder(consumerFactory); +} + +public static class PartitionFinder { + + private final ConsumerFactory consumerFactory; + + public PartitionFinder(ConsumerFactory consumerFactory) { + this.consumerFactory = consumerFactory; + } + + public String[] partitions(String topic) { + try (Consumer consumer = consumerFactory.createConsumer()) { + return consumer.partitionsFor(topic).stream() + .map(pi -> "" + pi.partition()) + .toArray(String[]::new); + } + } + +} +``` + +将此与`ConsumerConfig.AUTO_OFFSET_RESET_CONFIG=earliest`结合使用,将在每次启动应用程序时加载所有记录。你还应该将容器的`AckMode`设置为`MANUAL`,以防止容器提交`null`消费者组的偏移。然而,从版本 2.5.5 开始,如上面所示,你可以对所有分区应用初始偏移量;有关更多信息,请参见[显式分区分配](#manual-assignment)。 + +\=== 与其他事务管理器的 Kafka 事务示例 + +Spring 下面的引导应用程序是链接数据库和 Kafka 事务的一个示例。侦听器容器启动 Kafka 事务,`@Transactional`注释启动 DB 事务。首先提交 DB 事务;如果 Kafka 事务未能提交,则将重新交付记录,因此 DB 更新应该是幂等的。 + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public ApplicationRunner runner(KafkaTemplate template) { + return args -> template.executeInTransaction(t -> t.send("topic1", "test")); + } + + @Bean + public DataSourceTransactionManager dstm(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Component + public static class Listener { + + private final JdbcTemplate jdbcTemplate; + + private final KafkaTemplate kafkaTemplate; + + public Listener(JdbcTemplate jdbcTemplate, KafkaTemplate kafkaTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.kafkaTemplate = kafkaTemplate; + } + + @KafkaListener(id = "group1", topics = "topic1") + @Transactional("dstm") + public void listen1(String in) { + this.kafkaTemplate.send("topic2", in.toUpperCase()); + this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')"); + } + + @KafkaListener(id = "group2", topics = "topic2") + public void listen2(String in) { + System.out.println(in); + } + + } + + @Bean + public NewTopic topic1() { + return TopicBuilder.name("topic1").build(); + } + + @Bean + public NewTopic topic2() { + return TopicBuilder.name("topic2").build(); + } + +} +``` + +``` +spring.datasource.url=jdbc:mysql://localhost/integration?serverTimezone=UTC +spring.datasource.username=root +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.consumer.enable-auto-commit=false +spring.kafka.consumer.properties.isolation.level=read_committed + +spring.kafka.producer.transaction-id-prefix=tx- + +#logging.level.org.springframework.transaction=trace +#logging.level.org.springframework.kafka.transaction=debug +#logging.level.org.springframework.jdbc=debug +``` + +``` +create table mytable (data varchar(20)); +``` + +对于仅用于生产者的事务,事务同步工作: + +``` +@Transactional("dstm") +public void someMethod(String in) { + this.kafkaTemplate.send("topic2", in.toUpperCase()); + this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')"); +} +``` + +`KafkaTemplate`将使其事务与 DB 事务同步,并且在数据库之后发生提交/回滚。 + +如果你希望首先提交 Kafka 事务,并且仅在 Kafka 事务成功的情况下提交 DB 事务,请使用嵌套`@Transactional`方法: + +``` +@Transactional("dstm") +public void someMethod(String in) { + this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')"); + sendToKafka(in); +} + +@Transactional("kafkaTransactionManager") +public void sendToKafka(String in) { + this.kafkaTemplate.send("topic2", in.toUpperCase()); +} +``` + +\=== 定制 JSONSerializer 和 JSONDESerializer + +序列化器和反序列化器支持使用属性进行许多 cusomization,有关更多信息,请参见[JSON](#json-serde)。将这些对象实例化的代码(而不是 Spring)是`kafka-clients`代码,除非你将它们直接注入到消费者工厂和生产者工厂。如果你希望使用属性配置(去)序列化器,但是希望使用自定义`ObjectMapper`,那么只需创建一个子类并将自定义映射器传递到`super`构造函数中。例如: + +``` +public class CustomJsonSerializer extends JsonSerializer { + + public CustomJsonSerializer() { + super(customizedObjectMapper()); + } + + private static ObjectMapper customizedObjectMapper() { + ObjectMapper mapper = JacksonUtils.enhancedObjectMapper(); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } + +} +``` + +\== 其他资源 + +除了这个参考文档,我们还推荐了许多其他资源,这些资源可能有助于你了解 Spring 和 Apache Kafka。 + +* [Apache Kafka Project Home Page](https://kafka.apache.org/) + +* [Spring for Apache Kafka Home Page](https://projects.spring.io/spring-kafka/) + +* [Spring for Apache Kafka GitHub Repository](https://github.com/spring-projects/spring-kafka) + +* [Spring Integration GitHub Repository (Apache Kafka Module)](https://github.com/spring-projects/spring-integration) + +\== 覆盖 Spring 引导依赖项 + +当在 Spring 引导应用程序中对 Apache Kafka 使用 Spring 时, Apache Kafka 依赖关系版本由 Spring 引导的依赖关系管理确定。如果希望使用不同版本的`kafka-clients`或`kafka-streams`,并使用嵌入式 Kafka 代理进行测试,则需要覆盖 Spring 引导依赖项管理使用的版本,并为 Apache Kafka 添加两个`test`工件。 + +| |在 Microsoft Windows 上运行嵌入式代理时, Apache Kafka3.0.0 中存在一个 bug[Kafka-13391](https://issues.apache.org/jira/browse/KAFKA-13391)。
要在 Windows 上使用嵌入式代理,需要将 Apache Kafka 版本降级到 2.8.1,直到 3.0.1 可用。
使用 2.8.1 时,你还需要从`spring-kafka-test`中排除`zookeeper`依赖关系。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Maven + +``` + + 2.8.1 + + + + org.springframework.kafka + spring-kafka + + + + org.apache.kafka + kafka-streams + + + + org.springframework.kafka + spring-kafka-test + test + + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.kafka + kafka-clients + test + test + ${kafka.version} + + + + org.apache.kafka + kafka_2.13 + test + test + ${kafka.version} + +``` + +Gradle + +``` +ext['kafka.version'] = '2.8.1' + +dependencies { + implementation 'org.springframework.kafka:spring-kafka' + implementation "org.apache.kafka:kafka-streams" // optional - only needed when using kafka-streams + testImplementation ('org.springframework.kafka:spring-kafka-test') { + // needed if downgrading to Apache Kafka 2.8.1 + exclude group: 'org.apache.zookeeper', module: 'zookeeper' + } + testImplementation "org.apache.kafka:kafka-clients:${kafka.version}:test" + testImplementation "org.apache.kafka:kafka_2.13:${kafka.version}:test" +} +``` + +只有在测试中使用嵌入式 Kafka 代理时,才需要测试范围依赖关系。 + +\== 变更历史 + +\===2.6 到 2.7 之间的变化 + +\====Kafka 客户端版本 + +此版本需要 2.7.0`kafka-clients`。自 2.7.1 版本以来,它也与 2.8.0 客户端兼容;参见[[update-deps]]。 + +\==== 使用主题的非阻塞延迟重试 + +这个重要的新功能被添加到这个版本中。当严格的订购并不重要时,失败的交付可以发送到另一个主题,以供以后使用。可以配置一系列这样的重试主题,并增加延迟。有关更多信息,请参见[非阻塞重试](#retry-topic)。 + +\==== 侦听器容器更改 + +默认情况下,`onlyLogRecordMetadata`容器属性现在是`true`。 + +一个新的容器属性`stopImmediate`现在可用。 + +有关更多信息,请参见[侦听器容器属性](#container-props)。 + +在两次交付尝试之间使用`BackOff`的错误处理程序(例如`SeekToCurrentErrorHandler`和`DefaultAfterRollbackProcessor`)现在将在容器停止后不久退出 back off 间隔,而不是延迟停止。有关更多信息,请参见[后回滚处理器](#after-rollback)和[[seek-to-current]]。 + +扩展`FailedRecordProcessor`的错误处理程序和回滚后处理器现在可以配置一个或多个`RetryListener`s,以接收有关重试和恢复进度的信息。 + +有关更多信息,请参见[后回滚处理器](#after-rollback)、[[seek-to-current]和[[recovering-batch-eh]。 + +`RecordInterceptor`现在有了在侦听器返回后调用的其他方法(通常是通过抛出异常)。它还有一个子接口`ConsumerAwareRecordInterceptor`。此外,现在还有一个用于批处理侦听器的`BatchInterceptor`。有关更多信息,请参见[消息监听器容器](#message-listener-container)。 + +\====`@KafkaListener`变化 + +现在可以验证`@KafkaHandler`方法(类级侦听器)的有效负载参数。有关更多信息,请参见[`@KafkaListener``@Payload`验证]。 + +你现在可以在`MessagingMessageConverter`和`BatchMessagingMessageConverter`上设置`rawRecordHeader`属性,这会导致将 RAW`ConsumerRecord`添加到转换后的`Message`中。这是有用的,例如,如果你希望在侦听器错误处理程序中使用`DeadLetterPublishingRecoverer`。有关更多信息,请参见[侦听器错误处理程序](#listener-error-handlers)。 + +你现在可以在应用程序初始化期间修改`@KafkaListener`注释。有关更多信息,请参见[`@KafkaListener`属性修改]。 + +\====`DeadLetterPublishingRecover`变化 + +现在,如果键和值都反序列化失败,那么原始值将被发布到 DLT。以前,该值是填充的,但是`DeserializationException`键仍然保留在标题中。如果你对 recoverer 进行了子类划分并重写了`createProducerRecord`方法,则会有一个中断的 API 更改。 + +此外,在向目标解析器发布之前,recoverer 会验证目标解析器选择的分区是否确实存在。 + +有关更多信息,请参见[发布死信记录](#dead-letters)。 + +\====`ChainedKafkaTransactionManager`已弃用 + +有关更多信息,请参见[交易](#transactions)。 + +\====`ReplyingKafkaTemplate`变化 + +现在有一种机制,可以检查答复,如果存在某些条件,则在未来例外情况下失败。 + +增加了对发送和接收`spring-messaging``Message`s 的支持。 + +有关更多信息,请参见[使用`ReplyingKafkaTemplate`]。 + +\====Kafka 流媒体的变化 + +默认情况下,`StreamsBuilderFactoryBean`现在被配置为不清理本地状态。有关更多信息,请参见[配置](#streams-config)。 + +\====`KafkaAdmin`变化 + +已经添加了新的方法`createOrModifyTopics`和`describeTopics`。已经添加了`KafkaAdmin.NewTopics`,以便于在单个 Bean 中配置多个主题。有关更多信息,请参见[配置主题](#configuring-topics)。 + +\====`MessageConverter`变化 + +现在可以将`spring-messaging``SmartMessageConverter`添加到`MessagingMessageConverter`中,从而允许基于`contentType`头进行内容协商。有关更多信息,请参见[Spring Messaging Message Conversion](#messaging-message-conversion)。 + +\==== 测序`@KafkaListener`s + +有关更多信息,请参见[starting`@KafkaListener`s in sequence]。 + +\==== `ExponentialBackOffWithMaxRetries` + +提供了一个新的`BackOff`实现,使配置最大重试更加方便。有关更多信息,请参见[`ExponentialBackOffWithMaxRetries`实现](#EXP-backoff)。 + +\==== 条件委派错误处理程序 + +根据异常类型的不同,可以将这些新的错误处理程序配置为委托给不同的错误处理程序。有关更多信息,请参见[委派错误处理程序](#cond-eh)。 + +\===2.5 到 2.6 之间的变化 + +\====Kafka 客户端版本 + +此版本需要 2.6.0`kafka-clients`。 + +\==== 侦听器容器更改 + +默认的`EOSMode`现在是`BETA`。有关更多信息,请参见[一次语义学](#exactly-once)。 + +各种错误处理程序(扩展`FailedRecordProcessor`)和`DefaultAfterRollbackProcessor`现在重置`BackOff`如果恢复失败。此外,你现在可以基于失败的记录和/或异常选择要使用的`BackOff`。有关更多信息,请参见[[seek-to-current],[[recovering-batch-eh]],[发布死信记录](#dead-letters)和[后回滚处理器](#after-rollback)。 + +现在可以在容器属性中配置`adviceChain`。有关更多信息,请参见[侦听器容器属性](#container-props)。 + +当容器被配置为发布`ListenerContainerIdleEvent`s 时,当在发布空闲事件后接收到一条记录时,它现在发布`ListenerContainerNoLongerIdleEvent`。有关更多信息,请参见[应用程序事件](#events)和[检测空闲和无响应的消费者](#idle-containers)。 + +\====@kafkalistener 更改 + +当使用手动分区分配时,你现在可以指定一个通配符来确定哪些分区应该重置为初始偏移量。此外,如果侦听器实现`ConsumerSeekAware`,则在手动分配之后调用`onPartitionsAssigned()`。(也在版本 2.5.5 中添加)。有关更多信息,请参见[显式分区分配](#manual-assignment)。 + +在`AbstractConsumerSeekAware`中添加了方便的方法,以使查找更容易。有关更多信息,请参见[寻求一种特定的抵消](#seek)。 + +\====ErrorHandler 更改 + +现在可以将`FailedRecordProcessor`(例如`SeekToCurrentErrorHandler`,`DefaultAfterRollbackProcessor`,`RecoveringBatchErrorHandler`)的子类配置为重置重试状态,如果异常类型与此记录以前发生的类型不同。有关更多信息,请参见[[seek-to-current],[后回滚处理器](#after-rollback),[[recovering-batch-eh]]。 + +\==== 生产者工厂变更 + +现在,你可以为生产者设置一个最大的年龄,在此之后,他们将被关闭和重新创建。有关更多信息,请参见[交易](#transactions)。 + +现在,你可以在创建了`DefaultKafkaProducerFactory`之后更新配置映射。这可能是有用的,例如,如果你必须在凭据更改后更新 SSL 密钥/信任存储位置。有关更多信息,请参见[使用`DefaultKafkaProducerFactory`]。 + +\===2.4 到 2.5 之间的变化 + +本部分介绍了从 2.4 版本到 2.5 版本所做的更改。有关早期版本中的更改,请参见[[history]]。 + +\==== 消费者/生产者工厂变化 + +默认的使用者和生产者工厂现在可以在创建或关闭使用者或生产者时调用回调。提供了本机千分尺度量的实现方式。有关更多信息,请参见[工厂监听器](#factory-listeners)。 + +你现在可以在运行时更改 Bootstrap 服务器属性,从而实现对另一个 Kafka 集群的故障转移。有关更多信息,请参见[连接到 Kafka](#connecting)。 + +\====`StreamsBuilderFactoryBean`变化 + +工厂 Bean 现在可以在创建或销毁`KafkaStreams`时调用回调。给出了一种本机千分尺度量的实现方法。有关更多信息,请参见[Kafkastreams 测微仪支持](#streams-micrometer)。 + +\====Kafka 客户端版本 + +此版本需要 2.5.0`kafka-clients`。 + +\==== 类/包更改 + +`SeekUtils`已从`o.s.k.support`包移到`o.s.k.listener`。 + +\==== 传递尝试头 + +现在可以选择添加一个头,在使用某些错误处理程序和回滚处理程序之后跟踪交付尝试。有关更多信息,请参见[传递尝试标头](#delivery-header)。 + +\====@kafkalistener 更改 + +当返回类型`@KafkaListener`为`Message`时,如果需要,将自动填充默认的回复标题。有关更多信息,请参见[Reply Type Message\](#reply-message)。 + +当传入记录具有`null`键时,`KafkaHeaders.RECEIVED_MESSAGE_KEY`将不再填充`null`值;标题将完全省略。 + +`@KafkaListener`方法现在可以指定`ConsumerRecordMetadata`参数,而不是对元数据(如主题、分区等)使用离散的头。有关更多信息,请参见[消费者记录元数据](#consumer-record-metadata)。 + +\==== 侦听器容器更改 + +默认情况下,`assignmentCommitOption`容器属性现在是`LATEST_ONLY_NO_TX`。有关更多信息,请参见[侦听器容器属性](#container-props)。 + +在使用事务时,`subBatchPerPartition`容器属性现在默认为`true`。有关更多信息,请参见[交易](#transactions)。 + +现在提供了一个新的`RecoveringBatchErrorHandler`。有关更多信息,请参见[recovering-batch-eh]。 + +现在支持静态组成员身份。有关更多信息,请参见[消息监听器容器](#message-listener-container)。 + +在配置增量/合作再平衡时,如果偏移量无法使用非致命的`RebalanceInProgressException`提交,则容器将尝试重新提交重新平衡完成后仍分配给此实例的分区的偏移量。 + +默认的错误处理程序现在是用于记录侦听器的`SeekToCurrentErrorHandler`和用于批处理侦听器的`RecoveringBatchErrorHandler`。有关更多信息,请参见[容器错误处理程序](#error-handlers)。 + +你现在可以控制记录标准错误处理程序故意抛出的异常的级别。有关更多信息,请参见[容器错误处理程序](#error-handlers)。 + +添加了`getAssignmentsByClientId()`方法,使得确定并发容器中的哪个消费者被分配到哪个分区变得更容易。有关更多信息,请参见[侦听器容器属性](#container-props)。 + +现在可以在错误、调试日志等情况下禁止记录整个`ConsumerRecord`s。见`onlyLogRecordMetadata`in[侦听器容器属性](#container-props)。 + +\====KafkaTemplate 更改 + +`KafkaTemplate`现在可以维护千分尺计时器。有关更多信息,请参见[Monitoring](#micrometer)。 + +现在可以将`KafkaTemplate`配置为`ProducerConfig`属性,以覆盖生产者工厂中的那些属性。有关更多信息,请参见[使用`KafkaTemplate`]。 + +a`RoutingKafkaTemplate`现已提供。有关更多信息,请参见[使用`RoutingKafkaTemplate`]。 + +你现在可以使用`KafkaSendCallback`而不是`ListenerFutureCallback`来获得更窄的异常,从而更容易提取失败的`ProducerRecord`。有关更多信息,请参见[使用`KafkaTemplate`]。 + +\====Kafka 字符串序列化器/反序列化器 + +现在提供了新的`ToStringSerializer`/`StringDeserializer`s 以及相关的`SerDe`。有关更多信息,请参见[字符串序列化](#string-serde)。 + +\====JsonDeserializer + +`JsonDeserializer`现在具有更大的灵活性来确定反序列化类型。有关更多信息,请参见[使用方法确定类型](#serdes-type-methods)。 + +\==== 委托序列化器/反序列化器 + +当出站记录没有头时,`DelegatingSerializer`现在可以处理“标准”类型。有关更多信息,请参见[委派序列化器和反序列化器](#delegating-serialization)。 + +\==== 测试更改 + +现在,`KafkaTestUtils.consumerProps()`助手记录默认将`ConsumerConfig.AUTO_OFFSET_RESET_CONFIG`设置为`earliest`。有关更多信息,请参见[JUnit](#junit)。 + +\===2.3 到 2.4 之间的变化 + +\====Kafka 客户端版本 + +该版本需要 2.4.0`kafka-clients`或更高版本,并支持新的增量再平衡功能。 + +\====ConsumerAwareBalanceListener + +像`ConsumerRebalanceListener`一样,这个接口现在有一个额外的方法`onPartitionsLost`。有关更多信息,请参阅 Apache Kafka 文档。 + +与`ConsumerRebalanceListener`不同,默认实现执行**不是**调用`onPartitionsRevoked`。相反,侦听器容器将在调用`onPartitionsLost`之后调用该方法;因此,在实现`ConsumerAwareRebalanceListener`时,你不应该执行相同的操作。 + +有关更多信息,请参见[重新平衡听众](#rebalance-listeners)末尾的重要注释。 + +\====GenericErrorHandler + +`isAckAfterHandle()`默认实现现在默认返回 true。 + +\====Kafkatemplate + +现在`KafkaTemplate`除了事务性发布外,还支持非事务性发布。有关更多信息,请参见[`KafkaTemplate`事务性和非事务性发布]。 + +\==== 聚合引用 Kafkatemplate + +现在`BiConsumer`是`BiConsumer`。现在在超时之后(以及记录到达时)调用它;在超时之后调用的情况下,第二个参数是`true`。 + +有关更多信息,请参见[聚合多个回复](#aggregating-request-reply)。 + +\==== 侦听器容器 + +`ContainerProperties`提供了一个`authorizationExceptionRetryInterval`选项,让侦听器容器在`AuthorizationException`抛出任何`KafkaConsumer`之后重试。有关更多信息,请参见其 Javadocs 和[使用`KafkaMessageListenerContainer`]。 + +\====@kafkalistener + +`@KafkaListener`注释有一个新属性`splitIterables`;默认为 true。当应答侦听器返回`Iterable`时,此属性控制返回结果是作为单个记录发送,还是作为每个元素的记录发送。有关更多信息,请参见[使用`@SendTo`转发侦听器结果] + +现在可以将批处理侦听器配置为`BatchToRecordAdapter`;例如,这允许在一个事务中处理批处理,而侦听器一次获取一条记录。对于默认实现,可以使用`ConsumerRecordRecoverer`来处理批处理中的错误,而不会停止整个批处理的处理-这在使用事务时可能很有用。有关更多信息,请参见[具有批处理侦听器的事务](#transactions-batch)。 + +\==== Kafka流 + +`StreamsBuilderFactoryBean`接受一个新属性`KafkaStreamsInfrastructureCustomizer`。这允许在创建流之前配置构建器和/或拓扑。有关更多信息,请参见[Spring Management](#streams-spring)。 + +\===2.2 到 2.3 之间的变化 + +本节介绍了从版本 2.2 到版本 2.3 所做的更改。 + +\==== 提示、技巧和示例 + +增加了新的一章[[tips-n-tricks]](#tips-n-tricks)。请在该章中提交 Github 问题和/或删除对其他条目的请求。 + +\====Kafka 客户端版本 + +此版本需要 2.3.0`kafka-clients`或更高版本。 + +\==== 类/包更改 + +`TopicPartitionInitialOffset`不支持`TopicPartitionOffset`。 + +\==== 配置更改 + +从版本 2.3.4 开始,`missingTopicsFatal`容器属性默认为 false。如果这是真的,那么如果代理关闭,应用程序将无法启动;许多用户受到此更改的影响;考虑到 Kafka 是一个高可用性平台,我们没有预料到在没有活动代理的情况下启动应用程序将是一个常见的用例。 + +\==== 生产者和消费者的工厂变化 + +现在可以将`DefaultKafkaProducerFactory`配置为每个线程创建一个生成器。还可以在构造函数中提供`Supplier`实例,作为配置类(不需要 arg 构造函数)或使用`Serializer`实例进行构造的替代,然后在所有生产者之间共享这些实例。有关更多信息,请参见[使用`DefaultKafkaProducerFactory`]。 + +在`DefaultKafkaConsumerFactory`中的`Supplier`实例中也可以使用相同的选项。有关更多信息,请参见[使用`KafkaMessageListenerContainer`]。 + +\==== 侦听器容器更改 + +以前,当使用侦听器适配器(例如`@KafkaListener`s)调用侦听器时,错误处理程序会收到`ListenerExecutionFailedException`(实际的侦听器异常为`cause`s)。本机`GenericMessageListener`s 引发的异常不变地传递给错误处理程序。现在,始终是一个参数`ListenerExecutionFailedException`(实际的侦听器例外是`cause`),它提供对容器的`group.id`属性的访问。 + +因为侦听器容器有自己的提交偏移的机制,所以它更喜欢 kafka`ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG`是`false`。现在,它会自动将其设置为 False,除非在消费者工厂或容器的消费者属性覆盖中进行了专门设置。 + +默认情况下,`ackOnError`属性现在是`false`。有关更多信息,请参见[[Seek-to-Current]]。 + +现在可以在侦听器方法中获得消费者的`group.id`属性。有关更多信息,请参见[获取消费者`group.id`]。 + +容器有一个新的属性`recordInterceptor`,允许在调用侦听器之前检查或修改记录。在需要调用多个拦截器的情况下,还提供了`CompositeRecordInterceptor`。有关更多信息,请参见[消息监听器容器](#message-listener-container)。 + +`ConsumerSeekAware`具有新的方法,允许你执行相对于开始、结束或当前位置的查找,并查找大于或等于时间戳的第一偏移量。有关更多信息,请参见[寻求一种特定的抵消](#seek)。 + +现在提供了一个方便类`AbstractConsumerSeekAware`,以简化查找。有关更多信息,请参见[寻求一种特定的抵消](#seek)。 + +`ContainerProperties`提供了一个`idleBetweenPolls`选项,使侦听器容器中的主循环在`KafkaConsumer.poll()`调用之间休眠。有关更多信息,请参见其 Javadocs 和[使用`KafkaMessageListenerContainer`]。 + +当使用`AckMode.MANUAL`(或`MANUAL_IMMEDIATE`)时,现在可以通过在`Acknowledgment`上调用`nack`来导致重新交付。有关更多信息,请参见[提交补偿](#committing-offsets)。 + +现在可以使用微米计`Timer`s 来监视监听器的性能。有关更多信息,请参见[Monitoring](#micrometer)。 + +容器现在发布与启动相关的额外的消费者生命周期事件。有关更多信息,请参见[应用程序事件](#events)。 + +事务批处理侦听器现在可以支持僵尸围栏。有关更多信息,请参见[交易](#transactions)。 + +现在可以将侦听器容器工厂配置为`ContainerCustomizer`,以便在创建和配置每个容器之后进一步配置每个容器。有关更多信息,请参见[集装箱工厂](#container-factory)。 + +\====ErrorHandler 更改 + +现在`SeekToCurrentErrorHandler`将某些异常视为致命异常,并禁用这些异常的重试,在第一次失败时调用 recoverer。 + +现在可以将`SeekToCurrentErrorHandler`和`SeekToCurrentBatchErrorHandler`配置为在两次交付尝试之间应用`BackOff`(线程睡眠)。 + +从版本 2.3.2 开始,当错误处理程序在恢复失败的记录后返回时,将提交恢复记录的偏移量。 + +有关更多信息,请参见[[Seek-to-Current]]。 + +将`DeadLetterPublishingRecoverer`与`ErrorHandlingDeserializer`结合使用时,现在将发送到死信主题的消息的有效负载设置为无法反序列化的原始值。以前,从消息头中提取`null`和`DeserializationException`所需的用户代码。有关更多信息,请参见[发布死信记录](#dead-letters)。 + +\====topicbuilder + +提供了一个新的类`TopicBuilder`,以更方便地创建`NewTopic``@Bean`s,用于自动提供主题。有关更多信息,请参见[配置主题](#configuring-topics)。 + +\====Kafka 流媒体的变化 + +你现在可以执行由`@EnableKafkaStreams`创建的`StreamsBuilderFactoryBean`的附加配置。有关更多信息,请参见[流配置](#streams-config)。 + +现在提供了一个`RecoveringDeserializationExceptionHandler`,它允许恢复具有反序列化错误的记录。它可以与`DeadLetterPublishingRecoverer`结合使用,以将这些记录发送到死信主题。有关更多信息,请参见[从反序列化异常恢复](#streams-deser-recovery)。 + +已经提供了`HeaderEnricher`转换器,使用 SPEL 来生成标头值。有关更多信息,请参见[页眉 Enricher](#streams-header-enricher)。 + +已经提供了`MessagingTransformer`。这允许 Kafka Streams 拓扑与 Spring 消息传递组件(例如 Spring 集成流)进行交互。参见[`MessagingTransformer`](#Streams-Messaging)和[[[从`KStream`调用 Spring 集成流](https://DOCS. Spring.io/ Spring-integration/DOCS/current/reference/html/kafka.html#Streams-integration)]以获取更多信息。 + +\====JSON 组件更改 + +现在,所有的 JSON 感知组件都默认配置了由`JacksonUtils.enhancedObjectMapper()`生成的 Jackson`ObjectMapper`。`JsonDeserializer`现在提供了基于`TypeReference`的构造函数,以更好地处理目标泛型容器类型。还引入了`JacksonMimeTypeModule`,用于将`org.springframework.util.MimeType`序列化为普通字符串。有关更多信息,请参见其 Javadocs 和[序列化、反序列化和消息转换](#serdes)。 + +已经提供了一个`ByteArrayJsonMessageConverter`以及一个用于所有 JSON 转换器的新超类`JsonMessageConverter`。此外,`StringOrBytesSerializer`现在可用;它可以在`byte[]`、`Bytes`和`String`中序列化`String`值。有关更多信息,请参见[Spring Messaging Message Conversion](#messaging-message-conversion)。 + +`JsonSerializer`、`JsonDeserializer`和`JsonSerde`现在都有流畅的 API 来简化编程配置。有关更多信息,请参见 javadocs,[序列化、反序列化和消息转换](#serdes)和[流 JSON 序列化和反序列化](#serde)。 + +\====replyingkafkatemplate + +当一个回复超时时,Future 在特殊情况下用`KafkaReplyTimeoutException`而不是`KafkaException`完成。 + +另外,现在提供了一个重载的`sendAndReceive`方法,该方法允许在每个消息的基础上指定回复超时。 + +\==== 聚合引用 Kafkatemplate + +通过聚集来自多个接收者的回复,扩展`ReplyingKafkaTemplate`。有关更多信息,请参见[聚合多个回复](#aggregating-request-reply)。 + +\==== 事务更改 + +现在可以在`KafkaTemplate`和`KafkaTransactionManager`上覆盖生产者工厂的`transactionIdPrefix`。有关更多信息,请参见[`transactionIdPrefix`]。 + +\==== 新建委托序列化器/反序列化器 + +该框架现在提供了一个委托`Serializer`和`Deserializer`,利用一个头来支持使用多个键/值类型来生成和消费记录。有关更多信息,请参见[委派序列化器和反序列化器](#delegating-serialization)。 + +\==== 新建重试反序列化器 + +该框架现在提供了一个委托`RetryingDeserializer`,以便在可能出现诸如网络问题之类的瞬时错误时重试序列化。有关更多信息,请参见[重试反序列化器](#retrying-deserialization)。 + +\===2.1 到 2.2 之间的变化 + +\====Kafka 客户端版本 + +此版本需要 2.0.0`kafka-clients`或更高版本。 + +\==== 类和包的更改 + +将`ContainerProperties`类从`org.springframework.kafka.listener.config`移动到`org.springframework.kafka.listener`。 + +将`AckMode`枚举从`AbstractMessageListenerContainer`移动到`ContainerProperties`。 + +将`setBatchErrorHandler()`和`setErrorHandler()`方法从`ContainerProperties`移动到`AbstractMessageListenerContainer`和`AbstractKafkaListenerContainerFactory`。 + +回滚处理后 \==== + +提供了一种新的`AfterRollbackProcessor`策略。有关更多信息,请参见[后回滚处理器](#after-rollback)。 + +\====`ConcurrentKafkaListenerContainerFactory`变化 + +你现在可以使用`ConcurrentKafkaListenerContainerFactory`来创建和配置任何`ConcurrentMessageListenerContainer`,而不仅仅是那些`@KafkaListener`注释。有关更多信息,请参见[集装箱工厂](#container-factory)。 + +\==== 侦听器容器更改 + +添加了一个新的容器属性(`missingTopicsFatal`)。有关更多信息,请参见[使用`KafkaMessageListenerContainer`]。 + +当消费者停止时,现在会发出`ConsumerStoppedEvent`。有关更多信息,请参见[螺纹安全](#thread-safety)。 + +批处理侦听器可以选择性地接收完整的`ConsumerRecords`对象,而不是`List`对象。有关更多信息,请参见[批处理侦听器](#batch-listeners)。 + +`DefaultAfterRollbackProcessor`和`SeekToCurrentErrorHandler`现在可以恢复持续失败的记录,并且在默认情况下,在 10 次失败后恢复。可以将它们配置为将失败的记录发布到一文不值的主题。 + +从版本 2.2.4 开始,可以在选择死信主题名称时使用消费者的组 ID。 + +有关更多信息,请参见[后回滚处理器](#after-rollback)、[[[seek-to-current]]和[发布死信记录](#dead-letters)。 + +已经添加了`ConsumerStoppingEvent`。有关更多信息,请参见[应用程序事件](#events)。 + +现在可以将`SeekToCurrentErrorHandler`配置为在容器配置`AckMode.MANUAL_IMMEDIATE`时提交恢复的记录的偏移量(自 2.2.4 起)。有关更多信息,请参见[[Seek-to-Current]]。 + +\====@kafkalistener 更改 + +现在可以通过在注释上设置属性来覆盖侦听器容器工厂的`concurrency`和`autoStartup`属性。现在可以添加配置来确定将哪些头(如果有的话)复制到回复消息中。有关更多信息,请参见[`@KafkaListener`注释]。 + +你现在可以在自己的注释上使用`@KafkaListener`作为元注释。有关更多信息,请参见[`@KafkaListener`作为元注释]。 + +现在更容易为`@Payload`的验证配置`Validator`。有关更多信息,请参见[`@KafkaListener``@Payload`验证]。 + +现在,你可以直接在注释上指定 Kafka 消费者属性;这些属性将覆盖在消费者工厂中定义的具有相同名称的任何属性(自版本 2.2.4 以来)。有关更多信息,请参见[注释属性](#annotation-properties)。 + +\==== 页眉映射更改 + +现在,`MimeType`和`MediaType`类型的头被映射为`RecordHeader`值中的简单字符串。以前,它们被映射为 JSON,只有`MimeType`被解码。`MediaType`无法被解码。它们现在是用于互操作性的简单字符串。 + +此外,`DefaultKafkaHeaderMapper`有一个新的`addToStringClasses`方法,允许使用`toString()`而不是 JSON 来映射类型的规范。有关更多信息,请参见[消息头](#headers)。 + +\==== 嵌入式Kafka更改 + +`KafkaEmbedded`类及其`KafkaRule`接口已被弃用,而支持`EmbeddedKafkaBroker`及其 JUnit4`EmbeddedKafkaRule`包装器。现在,`@EmbeddedKafka`注释填充`EmbeddedKafkaBroker` Bean,而不是不受欢迎的`KafkaEmbedded`。此更改允许在 JUnit5 测试中使用`@EmbeddedKafka`。`@EmbeddedKafka`注释现在具有属性`ports`来指定填充`EmbeddedKafkaBroker`的端口。有关更多信息,请参见[测试应用程序](#testing)。 + +\====JSONSerializer/反序列化增强 + +现在,你可以通过使用生产者和消费者属性来提供类型映射信息。 + +在反序列化器上可以使用新的构造函数,以允许使用提供的目标类型覆盖类型头信息。 + +现在,`JsonDeserializer`默认情况下会删除任何类型信息标题。 + +现在可以通过使用 Kafka 属性(自 2.2.3 起)配置`JsonDeserializer`忽略类型信息标题。 + +有关更多信息,请参见[序列化、反序列化和消息转换](#serdes)。 + +\====Kafka 流媒体的变化 + +流配置 Bean 现在必须是`KafkaStreamsConfiguration`对象,而不是`StreamsConfig`对象。 + +将`StreamsBuilderFactoryBean`从包`…​core`移动到`…​config`。 + +当在`KStream`实例之上构建条件分支时,引入了`KafkaStreamBrancher`以获得更好的最终用户体验。 + +有关更多信息,请参见[Apache Kafka Streams Support](#streams-kafka-streams)和[配置](#streams-config)。 + +\==== 事务 ID + +当一个事务由侦听器容器启动时,`transactional.id`现在是`transactionIdPrefix`所附加的`..`。此更改允许对僵尸进行适当的击剑,[如此处所述](https://www.confluent.io/blog/transactions-apache-kafka/)。 + +\===2.0 到 2.1 之间的变化 + +\====Kafka 客户端版本 + +此版本需要 1.0.0`kafka-clients`或更高版本。 + +1.1.x 客户端在版本 2.2 中得到了原生支持。 + +\====JSON 改进 + +`StringJsonMessageConverter`和`JsonSerializer`现在在`Headers`中添加类型信息,让转换器和`JsonDeserializer`在接收时根据消息本身而不是固定的配置类型创建特定类型。有关更多信息,请参见[序列化、反序列化和消息转换](#serdes)。 + +\==== 容器停止错误处理程序 + +现在为记录和批处理侦听器提供了容器错误处理程序,这些侦听器将侦听器抛出的任何异常视为致命的/它们会停止容器。有关更多信息,请参见[处理异常](#annotation-error-handling)。 + +\==== 暂停和恢复集装箱 + +监听器容器现在有`pause()`和`resume()`方法(从版本 2.1.3 开始)。有关更多信息,请参见[暂停和恢复监听器容器](#pause-resume)。 + +\==== 有状态重试 + +从版本 2.1.3 开始,你可以配置有状态重试。有关更多信息,请参见[[stateful-retry]]。 + +\==== 客户 ID + +从版本 2.1.1 开始,你现在可以在`@KafkaListener`上设置`client.id`前缀。以前,要定制客户机 ID,需要为每个侦听器提供一个单独的消费者工厂(和容器工厂)。前缀后缀为`-n`,以便在使用并发时提供唯一的客户机 ID。 + +\==== 日志偏移提交 + +默认情况下,主题偏移提交的日志记录是在`DEBUG`日志级别下执行的。从版本 2.1.2 开始,`ContainerProperties`中的一个名为`commitLogLevel`的新属性允许你为这些消息指定日志级别。有关更多信息,请参见[使用`KafkaMessageListenerContainer`]。 + +\==== 默认 @kafkahandler + +从版本 2.1.3 开始,你可以将类级别`@KafkaHandler`上的一个`@KafkaListener`注释指定为默认值。有关更多信息,请参见[`@KafkaListener`on a class]。 + +\====replyingkafkatemplate + +从版本 2.1.3 开始,提供了一个`KafkaTemplate`子类来支持请求/回复语义。有关更多信息,请参见[使用`ReplyingKafkaTemplate`]。 + +\====ChainedKafkatRansactionManager + +版本 2.1.3 引入了`ChainedKafkaTransactionManager`。(现已弃用)。 + +\==== 从 2.0 开始的迁移指南 + +参见[2.0 到 2.1 迁移](https://github.com/spring-projects/spring-kafka/wiki/Spring-for-Apache-Kafka-2.0-to-2.1-Migration-Guide)指南。 + +\===1.3 到 2.0 之间的变化 + +\==== Spring 框架和 Java 版本 + +Spring for Apache Kafka 项目现在需要 Spring Framework5.0 和 Java8. + +\====`@KafkaListener`变化 + +现在,你可以使用`@KafkaListener`方法(以及类和`@KafkaHandler`方法)注释`@SendTo`方法。如果方法返回结果,则将其转发到指定的主题。有关更多信息,请参见[使用`@SendTo`转发侦听器结果]。 + +\==== 消息侦听器 + +消息侦听器现在可以知道`Consumer`对象。有关更多信息,请参见[消息侦听器](#message-listeners)。 + +\==== 使用`ConsumerAwareRebalanceListener` + +重新平衡监听器现在可以在重新平衡通知期间访问`Consumer`对象。有关更多信息,请参见[重新平衡听众](#rebalance-listeners)。 + +\===1.2 到 1.3 之间的变化 + +\==== 支持事务 + +0.11.0.0 客户端库增加了对事务的支持。添加了`KafkaTransactionManager`和对事务的其他支持。有关更多信息,请参见[交易](#transactions)。 + +\==== 支持标头 + +0.11.0.0 客户端库增加了对消息头的支持。现在可以将这些映射到`spring-messaging``MessageHeaders`。有关更多信息,请参见[消息头](#headers)。 + +\==== 创建主题 + +0.11.0.0 客户端库提供了`AdminClient`,你可以使用它来创建主题。`KafkaAdmin`使用此客户端自动添加定义为`@Bean`实例的主题。 + +\==== 支持 Kafka 时间戳 + +`KafkaTemplate`现在支持一个 API 来添加带有时间戳的记录。关于`timestamp`的支持,引入了新的`KafkaHeaders`。此外,还添加了新的`KafkaConditions.timestamp()`和`KafkaMatchers.hasTimestamp()`测试实用程序。请参阅[using`KafkaTemplate`]、[`@KafkaListener`Annotation]和[测试应用程序](#testing)以获取更多详细信息。 + +\====`@KafkaListener`变化 + +现在可以配置`KafkaListenerErrorHandler`来处理异常。有关更多信息,请参见[处理异常](#annotation-error-handling)。 + +默认情况下,`@KafkaListener``id`属性现在被用作`group.id`属性,覆盖在消费者工厂中配置的属性(如果存在的话)。此外,你可以在注释上显式地配置`groupId`。以前,你需要一个单独的容器工厂(和消费者工厂)来为侦听器使用不同的`group.id`值。要恢复先前使用配置的工厂`group.id`的行为,请将注释上的`idIsGroup`属性设置为`false`。 + +\====`@EmbeddedKafka`注解 + +为了方便起见,提供了一个测试类级`@EmbeddedKafka`注释,将`KafkaEmbedded`注册为 Bean。有关更多信息,请参见[测试应用程序](#testing)。 + +\====Kerberos 配置 + +现在提供了配置 Kerberos 的支持。有关更多信息,请参见[Jaas 和 Kerberos](#kerberos)。 + +\===1.1 到 1.2 之间的变化 + +此版本使用 0.10.2.x 客户端。 + +\===1.0 到 1.1 之间的变化 + +\==== Kafka客户端 + +此版本使用 Apache Kafka0.10.x.x 客户端。 + +\==== 批处理侦听器 + +侦听器可以被配置为接收由`consumer.poll()`操作返回的整批消息,而不是一次接收一个消息。 + +\====NULL 有效载荷 + +当你使用日志压缩时,空有效负载用于“删除”键。 + +==== 初始偏移 + +当显式分配分区时,你现在可以为消费者组配置相对于当前位置的初始偏移量,而不是绝对偏移量或相对于当前端的偏移量。 + +\====Seek + +你现在可以查找每个主题或分区的位置。在使用组管理和 Kafka 分配分区时,可以使用此设置初始化期间的初始位置。你还可以在检测到空闲容器时或在应用程序执行过程中的任意一点查找空闲容器。有关更多信息,请参见[寻求一种特定的抵消](#seek)。 \ No newline at end of file -- GitLab