2.8.7
该文档也以PDF 格式提供。 |
© 2016 - 2021 VMware, Inc.
本文档的副本可以供您自己使用和分发给其他人,前提是您不对此类副本收取任何费用,并且进一步前提是每份副本都包含本版权声明,无论是印刷版还是电子版。
1.前言
Spring for Apache Kafka 项目将核心 Spring 概念应用于基于 Kafka 的消息传递解决方案的开发。我们提供“模板”作为发送消息的高级抽象。我们还为消息驱动的 POJO 提供支持。
2. 有什么新鲜事?
2.1。自 2.7 以来 2.8 中的新功能
本节介绍从 2.7 版到 2.8 版所做的更改。有关早期版本的更改,请参阅更改历史记录。
2.1.1。卡夫卡客户端版本
此版本需要 3.0.1kafka-clients
使用事务时,kafka-clients 3.0.0 及更高版本不再支持EOSMode.V2 (aka BETA ) (并自动回退到V1 - aka ALPHA )与早于 2.5 的经纪人;因此,如果您的代理较旧(或升级您的代理)
,您必须覆盖默认值EOSMode ( )。V2 V1 |
有关详细信息,请参阅Exactly Once Semantics和KIP-447。
2.1.2. 包更改
与类型映射相关的类和接口已从…support.converter
移至…support.mapping
.
-
AbstractJavaTypeMapper
-
ClassMapper
-
DefaultJackson2JavaTypeMapper
-
Jackson2JavaTypeMapper
2.1.3。乱序手动提交
现在可以将侦听器容器配置为接受无序的手动偏移提交(通常是异步的)。容器将推迟提交,直到确认丢失的偏移量。有关详细信息,请参阅手动提交偏移量。
2.1.4。@KafkaListener
变化
现在可以指定侦听器方法是否是方法本身的批处理侦听器。这允许相同的容器工厂用于记录和批处理侦听器。
有关详细信息,请参阅批处理侦听器。
批处理侦听器现在可以处理转换异常。
有关更多信息,请参阅使用批处理错误处理程序的转换错误。
RecordFilterStrategy
,当与批处理侦听器一起使用时,现在可以在一次调用中过滤整个批处理。有关详细信息,请参阅Batch Listeners末尾的注释。
@KafkaListener
注释现在具有属性,可以仅针对此侦听filter
器覆盖容器工厂的属性。RecordFilterStrategy
2.1.5。KafkaTemplate
变化
给定主题、分区和偏移量,您现在可以接收一条记录。有关详细信息,请参阅用于KafkaTemplate
接收。
2.1.6。CommonErrorHandler
添加
用于记录批处理侦听器的遗留GenericErrorHandler
及其子接口层次结构已被新的单一接口所取代,其CommonErrorHandler
实现对应于GenericErrorHandler
. 有关详细信息,请参阅容器错误处理程序和迁移自定义旧错误处理程序实现CommonErrorHandler
。
2.1.7。侦听器容器更改
interceptBeforeTx
容器属性现在true
是默认的。
除了以前的 s 之外,该authorizationExceptionRetryInterval
属性已重命名为authExceptionRetryInterval
并现在适用于s 。这两个异常都被认为是致命的,并且容器将默认停止,除非设置了此属性。AuthenticationException
AuthorizationException
有关详细信息,请参阅使用KafkaMessageListenerContainer
和侦听器容器属性。
2.1.8。串行器/解串器更改
和DelegatingByTopicSerializer
现在DelegatingByTopicDeserializer
提供。有关更多信息,请参阅委托序列化器和反序列化器。
2.1.9。DeadLetterPublishingRecover
变化
该属性stripPreviousExceptionHeaders
现在true
是默认的。
现在有几种技术可以自定义将哪些标题添加到输出记录中。
有关详细信息,请参阅管理死信记录标头。
2.1.10。可重试的主题更改
现在您可以将同一个工厂用于可重试和不可重试的主题。有关详细信息,请参阅指定 ListenerContainerFactory。
现在有一个可管理的全局致命异常列表,这将使失败的记录直接进入 DLT。请参阅异常分类器以了解如何管理它。
您现在可以结合使用阻塞和非阻塞重试。有关详细信息,请参阅组合阻塞和非阻塞重试。
使用可重试主题功能时抛出的 KafkaBackOffException 现在记录在 DEBUG 级别。如果您需要将日志记录级别更改回 WARN 或将其设置为任何其他级别,请参阅更改 KafkaBackOffException 日志记录级别。
3.简介
参考文档的第一部分是 Spring for Apache Kafka 的高级概述,以及可以帮助您尽快启动和运行的底层概念和一些代码片段。
3.1。快速浏览
先决条件:您必须安装并运行 Apache Kafka。然后,您必须将 Spring for Apache Kafka ( spring-kafka
) JAR 及其所有依赖项放在您的类路径中。最简单的方法是在构建工具中声明一个依赖项。
如果您不使用 Spring Boot,请将spring-kafka
jar 声明为项目中的依赖项。
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.8.7</version>
</dependency>
使用 Spring Boot 时(并且您还没有使用 start.spring.io 创建项目),省略版本,Boot 将自动引入与您的 Boot 版本兼容的正确版本: |
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
但是,最快的入门方法是使用start.spring.io(或 Spring Tool Suits 和 Intellij IDEA 中的向导)并创建一个项目,选择“Spring for Apache Kafka”作为依赖项。
3.1.2。入门
最简单的入门方法是使用start.spring.io(或 Spring Tool Suits 和 Intellij IDEA 中的向导)并创建一个项目,选择“Spring for Apache Kafka”作为依赖项。有关其自以为是的基础设施 bean 自动配置的更多信息,请参阅Spring Boot 文档。
这是一个最小的消费者应用程序。
Spring Boot 消费者应用程序
@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);
}
}
spring.kafka.consumer.auto-offset-reset=earliest
NewTopic
bean 导致在代理上创建主题;如果主题已经存在,则不需要。
Spring Boot 生产者应用程序
@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<String, String> template) {
return args -> {
template.send("topic1", "test");
};
}
}
使用 Java 配置(无 Spring Boot)
Spring for Apache Kafka 旨在用于 Spring 应用程序上下文。例如,如果您在 Spring 上下文之外自己创建侦听器容器,则除非您满足…Aware 容器实现的所有接口,否则并非所有功能都可以工作。
|
这是一个不使用 Spring Boot 的应用程序示例;它同时具有 aConsumer
和Producer
.
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<Integer, String> template;
public Sender(KafkaTemplate<Integer, String> 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<Integer, String>
kafkaListenerContainerFactory(ConsumerFactory<Integer, String> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
return factory;
}
@Bean
public ConsumerFactory<Integer, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerProps());
}
private Map<String, Object> consumerProps() {
Map<String, Object> 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<Integer, String> template) {
return new Sender(template);
}
@Bean
public Listener listener() {
return new Listener();
}
@Bean
public ProducerFactory<Integer, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(senderProps());
}
private Map<String, Object> senderProps() {
Map<String, Object> 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<Integer, String> kafkaTemplate(ProducerFactory<Integer, String> producerFactory) {
return new KafkaTemplate<Integer, String>(producerFactory);
}
}
如您所见,在不使用 Spring Boot 时,您必须定义几个基础设施 bean。
4. 参考
参考文档的这一部分详细介绍了组成 Spring for Apache Kafka 的各种组件。主要章节介绍了使用 Spring 开发 Kafka 应用程序的核心类。
4.1。将 Spring 用于 Apache Kafka
本节详细解释了影响使用 Spring for Apache Kafka 的各种问题。有关快速但不太详细的介绍,请参阅快速浏览。
4.1.1。连接到卡夫卡
从 2.5 版开始,每一个都扩展了KafkaResourceFactory
. 这允许在运行时更改引导服务器,方法是将 a 添加Supplier<String>
到它们的配置中:setBootstrapServersSupplier(() → …)
. 这将为所有新连接调用以获取服务器列表。消费者和生产者通常是长寿的。要关闭现有的生产者,请调用reset()
. DefaultKafkaProducerFactory
要关闭现有的消费者,请在和/或任何其他侦听器容器 bean 上调用stop()
(然后start()
) 。KafkaListenerEndpointRegistry
stop()
start()
为方便起见,该框架还提供了一个ABSwitchCluster
支持两组引导服务器;其中之一随时处于活动状态。通过调用配置ABSwitchCluster
并将其添加到生产者和消费者工厂,以及. 当你想切换时,调用或调用生产者工厂建立新的连接;对于消费者和所有侦听器容器。使用s和bean时。KafkaAdmin
setBootstrapServersSupplier()
primary()
secondary()
reset()
stop()
start()
@KafkaListener
stop()
start()
KafkaListenerEndpointRegistry
有关更多信息,请参阅 Javadocs。
工厂听众
从 2.5 版开始,DefaultKafkaProducerFactory
和DefaultKafkaConsumerFactory
可以配置Listener
为在创建或关闭生产者或消费者时接收通知。
interface Listener<K, V> {
default void producerAdded(String id, Producer<K, V> producer) {
}
default void producerRemoved(String id, Producer<K, V> producer) {
}
}
interface Listener<K, V> {
default void consumerAdded(String id, Consumer<K, V> consumer) {
}
default void consumerRemoved(String id, Consumer<K, V> consumer) {
}
}
在每种情况下,都是通过将属性(从创建后获得)id
附加到工厂属性来创建的,由 . 分隔。client-id
metrics()
beanName
.
例如,这些侦听器可用于在创建KafkaClientMetrics
新客户端时创建和绑定 Micrometer 实例(并在客户端关闭时关闭它)。
该框架提供的侦听器正是这样做的;见千分尺原生度量。
4.1.2. 配置主题
如果您KafkaAdmin
在应用程序上下文中定义一个 bean,它可以自动将主题添加到代理。为此,您可以NewTopic
@Bean
为每个主题添加一个应用程序上下文。2.3 版引入了一个新类TopicBuilder
,使创建此类 bean 更加方便。以下示例显示了如何执行此操作:
@Bean
public KafkaAdmin admin() {
Map<String, Object> 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();
}
从 2.6 版开始,您可以省略.partitions()
和/或replicas()
代理默认值将应用于这些属性。代理版本必须至少为 2.4.0 才能支持此功能 - 请参阅KIP-464。
@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();
}
从 2.7 版本开始,您可以NewTopic
在单个KafkaAdmin.NewTopics
bean 定义中声明多个 s:
@Bean
public KafkaAdmin.NewTopics topics456() {
return new NewTopics(
TopicBuilder.name("defaultBoth")
.build(),
TopicBuilder.name("defaultPart")
.replicas(1)
.build(),
TopicBuilder.name("defaultRepl")
.partitions(3)
.build());
}
使用 Spring Boot 时,KafkaAdmin 会自动注册一个 bean,因此您只需要NewTopic (and/or NewTopics ) @Bean 。
|
默认情况下,如果代理不可用,则会记录一条消息,但会继续加载上下文。您可以以编程方式调用管理员的initialize()
方法稍后再试。如果您希望这种情况被认为是致命的,请将管理员的fatalIfBrokerNotAvailable
属性设置为true
. 然后上下文无法初始化。
如果 broker 支持(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<SendResult<K, V>> sendDefault(V data);
ListenableFuture<SendResult<K, V>> sendDefault(K key, V data);
ListenableFuture<SendResult<K, V>> sendDefault(Integer partition, K key, V data);
ListenableFuture<SendResult<K, V>> sendDefault(Integer partition, Long timestamp, K key, V data);
ListenableFuture<SendResult<K, V>> send(String topic, V data);
ListenableFuture<SendResult<K, V>> send(String topic, K key, V data);
ListenableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, V data);
ListenableFuture<SendResult<K, V>> send(String topic, Integer partition, Long timestamp, K key, V data);
ListenableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record);
ListenableFuture<SendResult<K, V>> send(Message<?> message);
Map<MetricName, ? extends Metric> metrics();
List<PartitionInfo> partitionsFor(String topic);
<T> T execute(ProducerCallback<K, V, T> callback);
// Flush the producer.
void flush();
interface ProducerCallback<K, V, T> {
T doInKafka(Producer<K, V> producer);
}
有关更多详细信息,请参阅Javadoc。
sendDefault
API 要求已向模板提供默认主题。
API 将 atimestamp
作为参数并将此时间戳存储在记录中。用户提供的时间戳如何存储取决于 Kafka 主题上配置的时间戳类型。如果主题配置为使用CREATE_TIME
,则记录用户指定的时间戳(如果未指定,则生成)。如果主题配置为使用LOG_APPEND_TIME
,则忽略用户指定的时间戳,并且代理添加本地代理时间。
要使用模板,您可以配置生产者工厂并在模板的构造函数中提供它。以下示例显示了如何执行此操作:
@Bean
public ProducerFactory<Integer, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 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<Integer, String> kafkaTemplate() {
return new KafkaTemplate<Integer, String>(producerFactory());
}
从 2.5 版开始,您现在可以覆盖工厂的ProducerConfig
属性以创建具有来自同一工厂的不同生产者配置的模板。
@Bean
public KafkaTemplate<String, String> stringTemplate(ProducerFactory<String, String> pf) {
return new KafkaTemplate<>(pf);
}
@Bean
public KafkaTemplate<String, byte[]> bytesTemplate(ProducerFactory<String, byte[]> pf) {
return new KafkaTemplate<>(pf,
Collections.singletonMap(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class));
}
请注意,ProducerFactory<?, ?>
可以使用不同的窄泛型类型引用类型的 bean(例如 Spring Boot 自动配置的 bean)。
您还可以使用标准<bean/>
定义来配置模板。
然后,要使用模板,您可以调用其中一种方法。
当您使用带有Message<?>
参数的方法时,主题、分区和键信息将在消息头中提供,其中包括以下项目:
-
KafkaHeaders.TOPIC
-
KafkaHeaders.PARTITION_ID
-
KafkaHeaders.MESSAGE_KEY
-
KafkaHeaders.TIMESTAMP
消息有效负载是数据。
或者,您可以KafkaTemplate
使用 a配置ProducerListener
以获取带有发送结果(成功或失败)的异步回调,而不是等待Future
完成。以下清单显示了ProducerListener
接口的定义:
public interface ProducerListener<K, V> {
void onSuccess(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata);
void onError(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata,
Exception exception);
}
默认情况下,模板配置有LoggingProducerListener
,它记录错误并且在发送成功时不执行任何操作。
为方便起见,提供了默认方法实现,以防您只想实现其中一种方法。
请注意,发送方法返回一个ListenableFuture<SendResult>
. 您可以向侦听器注册回调以异步接收发送的结果。以下示例显示了如何执行此操作:
ListenableFuture<SendResult<Integer, String>> future = template.send("myTopic", "something");
future.addCallback(new ListenableFutureCallback<SendResult<Integer, String>>() {
@Override
public void onSuccess(SendResult<Integer, String> result) {
...
}
@Override
public void onFailure(Throwable ex) {
...
}
});
SendResult
有两个属性 aProducerRecord
和RecordMetadata
. 有关这些对象的信息,请参阅 Kafka API 文档。
Throwable
inonFailure
可以转换为KafkaProducerException
; 它的failedProducerRecord
属性包含失败的记录。
从 2.5 版开始,您可以使用 aKafkaSendCallback
而不是 a ListenableFutureCallback
,从而更容易提取 failed ProducerRecord
,避免需要强制转换Throwable
:
ListenableFuture<SendResult<Integer, String>> future = template.send("topic", 1, "thing");
future.addCallback(new KafkaSendCallback<Integer, String>() {
@Override
public void onSuccess(SendResult<Integer, String> result) {
...
}
@Override
public void onFailure(KafkaProducerException ex) {
ProducerRecord<Integer, String> failed = ex.getFailedProducerRecord();
...
}
});
您还可以使用一对 lambda:
ListenableFuture<SendResult<Integer, String>> future = template.send("topic", 1, "thing");
future.addCallback(result -> {
...
}, (KafkaFailureCallback<Integer, String>) ex -> {
ProducerRecord<Integer, String> failed = ex.getFailedProducerRecord();
...
});
如果你想阻塞发送线程等待结果,你可以调用future的get()
方法;建议使用带超时的方法。您可能希望flush()
在等待之前调用,或者为了方便起见,模板有一个带有参数的构造函数,该autoFlush
参数会导致模板flush()
在每次发送时执行。linger.ms
仅当您设置了生产者属性并希望立即发送部分批次时才需要刷新。
例子
本节展示了向 Kafka 发送消息的示例:
public void sendToKafka(final MyOutputData data) {
final ProducerRecord<String, String> record = createRecord(data);
ListenableFuture<SendResult<Integer, String>> future = template.send(record);
future.addCallback(new KafkaSendCallback<Integer, String>() {
@Override
public void onSuccess(SendResult<Integer, String> result) {
handleSuccess(data);
}
@Override
public void onFailure(KafkaProducerException ex) {
handleFailure(data, record, ex);
}
});
}
public void sendToKafka(final MyOutputData data) {
final ProducerRecord<String, String> record = createRecord(data);
try {
template.send(record).get(10, TimeUnit.SECONDS);
handleSuccess(data);
}
catch (ExecutionException e) {
handleFailure(data, record, e.getCause());
}
catch (TimeoutException | InterruptedException e) {
handleFailure(data, record, e);
}
}
请注意,导致的原因ExecutionException
与KafkaProducerException
属性有关failedProducerRecord
。
使用RoutingKafkaTemplate
从 2.5 版开始,您可以使用 aRoutingKafkaTemplate
在运行时根据目标topic
名称选择生产者。
路由模板不支持事务、、、execute 或flush 操作,metrics 因为这些操作不知道主题。
|
该模板需要实例的java.util.regex.Pattern
映射ProducerFactory<Object, Object>
。这个地图应该是有序的(例如 a LinkedHashMap
),因为它是按顺序遍历的;您应该在开始时添加更具体的模式。
以下简单的 Spring Boot 应用程序提供了一个示例,说明如何使用相同的模板发送到不同的主题,每个主题使用不同的值序列化器。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context,
ProducerFactory<Object, Object> pf) {
// Clone the PF with a different Serializer, register with Spring for shutdown
Map<String, Object> configs = new HashMap<>(pf.getConfigurationProperties());
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);
DefaultKafkaProducerFactory<Object, Object> bytesPF = new DefaultKafkaProducerFactory<>(configs);
context.registerBean(DefaultKafkaProducerFactory.class, "bytesPF", bytesPF);
Map<Pattern, ProducerFactory<Object, Object>> map = new LinkedHashMap<>();
map.put(Pattern.compile("two"), bytesPF);
map.put(Pattern.compile(".+"), pf); // Default PF with StringSerializer
return new RoutingKafkaTemplate(map);
}
@Bean
public ApplicationRunner runner(RoutingKafkaTemplate routingTemplate) {
return args -> {
routingTemplate.send("one", "thing1");
routingTemplate.send("two", "thing2".getBytes());
};
}
}
此示例的相应@KafkaListener
s 显示在Annotation Properties中。
有关实现类似结果的另一种技术,但具有将不同类型发送到同一主题的附加功能,请参阅委托序列化器和反序列化器。
使用DefaultKafkaProducerFactory
如UsingKafkaTemplate
中所见, aProducerFactory
用于创建生产者。
默认情况下,不使用TransactionsDefaultKafkaProducerFactory
时,会创建一个供所有客户端使用的单例生产者,如KafkaProducer
javadocs 中所建议的那样。但是,如果您调用flush()
模板,这可能会导致使用同一生产者的其他线程出现延迟。从 2.3 版开始,DefaultKafkaProducerFactory
有一个新属性producerPerThread
。当设置为true
时,工厂将为每个线程创建(并缓存)一个单独的生产者,以避免此问题。
When producerPerThread is ,当不再需要生产者时true ,用户代码必须调用工厂。closeThreadBoundProducer() 这将物理关闭生产者并将其从ThreadLocal . 调用reset() 或destroy() 不会清理这些生产者。
|
创建 a时,可以通过调用仅接受属性 Map 的构造函数从配置中获取DefaultKafkaProducerFactory
键和/或值类(参见Using中的示例),或者可以将实例传递给构造函数(在这种情况下,所有s 共享相同的实例)。或者,您可以提供s(从 2.3 版开始),用于为每个 s 获取单独的实例:Serializer
KafkaTemplate
Serializer
DefaultKafkaProducerFactory
Producer
Supplier<Serializer>
Serializer
Producer
@Bean
public ProducerFactory<Integer, CustomValue> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs(), null, () -> new CustomValueSerializer());
}
@Bean
public KafkaTemplate<Integer, CustomValue> kafkaTemplate() {
return new KafkaTemplate<Integer, CustomValue>(producerFactory());
}
从版本 2.5.10 开始,您现在可以在创建工厂后更新生产者属性。这可能很有用,例如,如果您必须在凭据更改后更新 SSL 密钥/信任存储位置。这些更改不会影响现有的生产者实例;调用reset()
关闭任何现有的生产者,以便使用新属性创建新的生产者。注意:您不能将事务性生产者工厂更改为非事务性,反之亦然。
现在提供了两种新方法:
void updateConfigs(Map<String, Object> updates);
void removeConfig(String configKey);
从 2.8 版开始,如果您将序列化程序作为对象提供(在构造函数中或通过设置器),工厂将调用该configure()
方法以使用配置属性对其进行配置。
使用ReplyingKafkaTemplate
2.1.3 版引入了一个子类KafkaTemplate
来提供请求/回复语义。该类已命名ReplyingKafkaTemplate
并具有两个附加方法;下面显示了方法签名:
RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record);
RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record,
Duration replyTimeout);
(另请参阅使用s请求/回复Message<?>
)。
结果是ListenableFuture
异步填充结果(或异常,超时)。结果还有一个sendFuture
属性,它是调用的结果KafkaTemplate.send()
。您可以使用这个未来来确定发送操作的结果。
如果使用第一种方法,或者replyTimeout
参数是null
,则使用模板的defaultReplyTimeout
属性(默认为 5 秒)。
以下 Spring Boot 应用程序显示了如何使用该功能的示例:
@SpringBootApplication
public class KRequestingApplication {
public static void main(String[] args) {
SpringApplication.run(KRequestingApplication.class, args).close();
}
@Bean
public ApplicationRunner runner(ReplyingKafkaTemplate<String, String, String> template) {
return args -> {
ProducerRecord<String, String> record = new ProducerRecord<>("kRequests", "foo");
RequestReplyFuture<String, String, String> replyFuture = template.sendAndReceive(record);
SendResult<String, String> sendResult = replyFuture.getSendFuture().get(10, TimeUnit.SECONDS);
System.out.println("Sent ok: " + sendResult.getRecordMetadata());
ConsumerRecord<String, String> consumerRecord = replyFuture.get(10, TimeUnit.SECONDS);
System.out.println("Return value: " + consumerRecord.value());
};
}
@Bean
public ReplyingKafkaTemplate<String, String, String> replyingTemplate(
ProducerFactory<String, String> pf,
ConcurrentMessageListenerContainer<String, String> repliesContainer) {
return new ReplyingKafkaTemplate<>(pf, repliesContainer);
}
@Bean
public ConcurrentMessageListenerContainer<String, String> repliesContainer(
ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {
ConcurrentMessageListenerContainer<String, String> repliesContainer =
containerFactory.createContainer("kReplies");
repliesContainer.getContainerProperties().setGroupId("repliesGroup");
repliesContainer.setAutoStartup(false);
return repliesContainer;
}
@Bean
public NewTopic kRequests() {
return TopicBuilder.name("kRequests")
.partitions(10)
.replicas(2)
.build();
}
@Bean
public NewTopic kReplies() {
return TopicBuilder.name("kReplies")
.partitions(10)
.replicas(2)
.build();
}
}
请注意,我们可以使用 Boot 的自动配置容器工厂来创建回复容器。
如果将非平凡的反序列化器用于回复,请考虑使用ErrorHandlingDeserializer
委托给您配置的反序列化器的。如此配置后,RequestReplyFuture
将异常完成,您可以在其属性中捕获ExecutionException
, 。DeserializationException
cause
从 2.6.7 版本开始,除了检测DeserializationException
s 之外,模板还将调用该replyErrorChecker
函数(如果提供)。如果它返回异常,future 将异常完成。
这是一个例子:
template.setReplyErrorChecker(record -> {
Header error = record.headers().lastHeader("serverSentAnError");
if (error != null) {
return new MyException(new String(error.value()));
}
else {
return null;
}
});
...
RequestReplyFuture<Integer, String, String> future = template.sendAndReceive(record);
try {
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
ConsumerRecord<Integer, String> consumerRecord = future.get(10, TimeUnit.SECONDS);
...
}
catch (InterruptedException e) {
...
}
catch (ExecutionException e) {
if (e.getCause instanceof MyException) {
...
}
}
catch (TimeoutException e) {
...
}
模板设置了一个标头(KafkaHeaders.CORRELATION_ID
默认命名),必须由服务器端回显。
在这种情况下,以下@KafkaListener
应用程序会响应:
@SpringBootApplication
public class KReplyingApplication {
public static void main(String[] args) {
SpringApplication.run(KReplyingApplication.class, args);
}
@KafkaListener(id="server", topics = "kRequests")
@SendTo // use default replyTo expression
public String listen(String in) {
System.out.println("Server received: " + in);
return in.toUpperCase();
}
@Bean
public NewTopic kRequests() {
return TopicBuilder.name("kRequests")
.partitions(10)
.replicas(2)
.build();
}
@Bean // not required if Jackson is on the classpath
public MessagingMessageConverter simpleMapperConverter() {
MessagingMessageConverter messagingMessageConverter = new MessagingMessageConverter();
messagingMessageConverter.setHeaderMapper(new SimpleKafkaHeaderMapper());
return messagingMessageConverter;
}
}
基础设施回显相关@KafkaListener
ID 并确定回复主题。
有关发送回复的更多信息,请参阅转发侦听器结果使用@SendTo
。该模板使用默认标题KafKaHeaders.REPLY_TOPIC
来指示回复的主题。
从版本 2.2 开始,模板尝试从配置的回复容器中检测回复主题或分区。如果容器配置为侦听单个主题或单个TopicPartitionOffset
,则用于设置回复标头。如果容器以其他方式配置,则用户必须设置回复标头。在这种情况下,INFO
初始化期间会写入一条日志消息。以下示例使用KafkaHeaders.REPLY_TOPIC
:
record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, "kReplies".getBytes()));
当您配置单个回复TopicPartitionOffset
时,您可以对多个模板使用相同的回复主题,只要每个实例侦听不同的分区即可。使用单个回复主题进行配置时,每个实例必须使用不同的group.id
. 在这种情况下,所有实例都会收到每个回复,但只有发送请求的实例才能找到相关 ID。这可能对自动缩放有用,但会产生额外的网络流量开销以及丢弃每个不需要的回复的小成本。当您使用此设置时,我们建议您将模板设置sharedReplyTopic
为true
,这会降低对 DEBUG 的意外回复的日志记录级别,而不是默认的 ERROR。
以下是配置回复容器以使用相同的共享回复主题的示例:
@Bean
public ConcurrentMessageListenerContainer<String, String> replyContainer(
ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {
ConcurrentMessageListenerContainer<String, String> container = containerFactory.createContainer("topic2");
container.getContainerProperties().setGroupId(UUID.randomUUID().toString()); // unique
Properties props = new Properties();
props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); // so the new group doesn't get old replies
container.getContainerProperties().setKafkaConsumerProperties(props);
return container;
}
如果您有多个客户端实例并且您没有按照上一段中的讨论配置它们,则每个实例都需要一个专用的回复主题。另一种方法是为每个实例设置KafkaHeaders.REPLY_PARTITION 并使用专用分区。Header 包含一个四字节的 int (big-endian) 。服务器必须使用此标头将回复路由到正确的分区(@KafkaListener 这样做)。但是,在这种情况下,回复容器不能使用 Kafka 的组管理功能,并且必须配置为侦听固定分区(通过TopicPartitionOffset 在其ContainerProperties 构造函数中使用 a)。
|
要求 Jackson在DefaultKafkaHeaderMapper 类路径上(对于@KafkaListener )。如果它不可用,则消息转换器没有标头映射器,因此您必须配置 aMessagingMessageConverter 和 a 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<K, V> sendAndReceive(Message<?> message);
<P> RequestReplyTypedMessageFuture<K, V, P> sendAndReceive(Message<?> message,
ParameterizedTypeReference<P> returnType);
这些将使用模板的默认值replyTimeout
,也有可能在方法调用中超时的重载版本。
如果消费者Deserializer
或模板MessageConverter
可以通过配置或回复消息中的类型元数据在没有任何附加信息的情况下转换有效负载,则使用第一种方法。
如果您需要为返回类型提供类型信息,请使用第二种方法,以帮助消息转换器。这也允许同一个模板接收不同的类型,即使回复中没有类型元数据,例如当服务器端不是 Spring 应用程序时。下面是后者的一个例子:
@Bean
ReplyingKafkaTemplate<String, String, String> template(
ProducerFactory<String, String> pf,
ConcurrentKafkaListenerContainerFactory<String, String> factory) {
ConcurrentMessageListenerContainer<String, String> replyContainer =
factory.createContainer("replies");
replyContainer.getContainerProperties().setGroupId("request.replies");
ReplyingKafkaTemplate<String, String, String> template =
new ReplyingKafkaTemplate<>(pf, replyContainer);
template.setMessageConverter(new ByteArrayJsonMessageConverter());
template.setDefaultTopic("requests");
return template;
}
RequestReplyTypedMessageFuture<String, String, Thing> future1 =
template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(),
new ParameterizedTypeReference<Thing>() { });
log.info(future1.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString());
Thing thing = future1.get(10, TimeUnit.SECONDS).getPayload();
log.info(thing.toString());
RequestReplyTypedMessageFuture<String, String, List<Thing>> future2 =
template.sendAndReceive(MessageBuilder.withPayload("getThings").build(),
new ParameterizedTypeReference<List<Thing>>() { });
log.info(future2.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString());
List<Thing> things = future2.get(10, TimeUnit.SECONDS).getPayload();
things.forEach(thing1 -> log.info(thing1.toString()));
回复类型消息<?>
当@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
and 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();
}
聚合多个回复
UsingReplyingKafkaTemplate
中的模板严格用于单个请求/回复场景。对于单个消息的多个接收者返回回复的情况,您可以使用AggregatingReplyingKafkaTemplate
. 这是Scatter-Gather Enterprise Integration Pattern客户端的实现。
与 一样ReplyingKafkaTemplate
,AggregatingReplyingKafkaTemplate
构造函数采用生产者工厂和侦听器容器来接收回复;它有第三个参数BiPredicate<List<ConsumerRecord<K, R>>, Boolean> releaseStrategy
,每次收到回复时都会查询该参数;当谓词返回时true
,使用s的集合ConsumerRecord
来完成方法Future
返回的sendAndReceive
。
还有一个附加属性returnPartialOnTimeout
(默认为 false)。当它设置为 时true
,不是用 a 完成未来,而是KafkaReplyTimeoutException
部分结果正常完成未来(只要已收到至少一个回复记录)。
从版本 2.3.5 开始,谓词也会在超时后调用(如果returnPartialOnTimeout
是true
)。第一个参数是当前记录列表;第二个是true
如果这个调用是由于超时。谓词可以修改记录列表。
AggregatingReplyingKafkaTemplate<Integer, String, String> template =
new AggregatingReplyingKafkaTemplate<>(producerFactory, container,
coll -> coll.size() == releaseSize);
...
RequestReplyFuture<Integer, String, Collection<ConsumerRecord<Integer, String>>> future =
template.sendAndReceive(record);
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
ConsumerRecord<Integer, Collection<ConsumerRecord<Integer, String>>> consumerRecord =
future.get(30, TimeUnit.SECONDS);
请注意,返回类型是 a ConsumerRecord
,其值是ConsumerRecord
s 的集合。“外部”ConsumerRecord
不是“真实”记录,它是由模板合成的,作为收到请求的实际回复记录的持有者。当正常发布时(发布策略返回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";
中的实数ConsumerRecord
sCollection
包含收到回复的实际主题。
回复的侦听器容器必须配置为AckMode.MANUAL 或AckMode.MANUAL_IMMEDIATE ;消费者属性enable.auto.commit 必须是false (自 2.3 版以来的默认值)。为了避免丢失消息的任何可能性,模板仅在有零个未完成的请求时提交偏移量,即当最后一个未完成的请求被释放策略释放时。重新平衡后,可能会出现重复的回复交付;对于任何正在进行的请求,这些都将被忽略;当收到已发布回复的重复回复时,您可能会看到错误日志消息。
|
如果你使用ErrorHandlingDeserializer 这个聚合模板,框架将不会自动检测DeserializationException s。相反,记录(带有null 值)将原封不动地返回,并在标头中包含反序列化异常。建议应用程序调用实用方法ReplyingKafkaTemplate.checkDeserialization() 方法来确定是否发生反序列化异常。有关更多信息,请参阅其 javadocs。该replyErrorChecker 聚合模板也不需要;您应该对回复的每个元素进行检查。
|
4.1.4。接收消息
您可以通过配置MessageListenerContainer
并提供消息侦听器或使用@KafkaListener
注释来接收消息。
消息监听器
当您使用消息侦听器容器时,您必须提供一个侦听器来接收数据。目前有八个受支持的消息侦听器接口。以下清单显示了这些接口:
public interface MessageListener<K, V> { (1)
void onMessage(ConsumerRecord<K, V> data);
}
public interface AcknowledgingMessageListener<K, V> { (2)
void onMessage(ConsumerRecord<K, V> data, Acknowledgment acknowledgment);
}
public interface ConsumerAwareMessageListener<K, V> extends MessageListener<K, V> { (3)
void onMessage(ConsumerRecord<K, V> data, Consumer<?, ?> consumer);
}
public interface AcknowledgingConsumerAwareMessageListener<K, V> extends MessageListener<K, V> { (4)
void onMessage(ConsumerRecord<K, V> data, Acknowledgment acknowledgment, Consumer<?, ?> consumer);
}
public interface BatchMessageListener<K, V> { (5)
void onMessage(List<ConsumerRecord<K, V>> data);
}
public interface BatchAcknowledgingMessageListener<K, V> { (6)
void onMessage(List<ConsumerRecord<K, V>> data, Acknowledgment acknowledgment);
}
public interface BatchConsumerAwareMessageListener<K, V> extends BatchMessageListener<K, V> { (7)
void onMessage(List<ConsumerRecord<K, V>> data, Consumer<?, ?> consumer);
}
public interface BatchAcknowledgingConsumerAwareMessageListener<K, V> extends BatchMessageListener<K, V> { (8)
void onMessage(List<ConsumerRecord<K, V>> data, Acknowledgment acknowledgment, Consumer<?, ?> consumer);
}
1 | 当使用自动提交或容器管理的提交方法之一时,使用此接口处理ConsumerRecord 从 Kafka 使用者操作接收的单个实例。poll() |
2 | 使用其中一种手动提交方法时,使用此接口处理ConsumerRecord 从 Kafka 消费者操作接收到的单个实例。poll() |
3 | 当使用自动提交或容器管理的提交方法之一时,使用此接口处理ConsumerRecord 从 Kafka 使用者操作接收的单个实例。提供对对象的访问。poll() Consumer |
4 | 使用其中一种手动提交方法时,使用此接口处理ConsumerRecord 从 Kafka 消费者操作接收到的单个实例。提供对对象的访问。poll() Consumer |
5 | 当使用自动提交或容器管理的提交方法之一时,使用此接口处理ConsumerRecord 从 Kafka 消费者操作接收到的所有实例。
使用此接口时不支持,因为为侦听器提供了完整的批处理。poll() AckMode.RECORD |
6 | 使用其中一种手动提交方法时,使用此接口处理ConsumerRecord 从 Kafka 消费者操作接收到的所有实例。poll() |
7 | 当使用自动提交或容器管理的提交方法之一时,使用此接口处理ConsumerRecord 从 Kafka 消费者操作接收到的所有实例。
使用此接口时不支持,因为为侦听器提供了完整的批处理。提供对对象的访问。poll() AckMode.RECORD Consumer |
8 | 使用其中一种手动提交方法时,使用此接口处理ConsumerRecord 从 Kafka 消费者操作接收到的所有实例。提供对对象的访问。poll() Consumer |
该Consumer 对象不是线程安全的。您只能在调用侦听器的线程上调用其方法。
|
您不应该执行任何Consumer<?, ?> 影响消费者位置和/或在您的侦听器中提交的偏移量的方法;容器需要管理这些信息。
|
消息侦听器容器
提供了两种MessageListenerContainer
实现:
-
KafkaMessageListenerContainer
-
ConcurrentMessageListenerContainer
KafkaMessageListenerContainer
接收来自单个线程上所有主题或分区的所有消息。ConcurrentMessageListenerContainer
委托给一个或多个实例KafkaMessageListenerContainer
以提供多线程消费。
从版本 2.2.7 开始,您可以RecordInterceptor
在监听器容器中添加一个;它将在调用允许检查或修改记录的侦听器之前调用。如果拦截器返回 null,则不调用侦听器。从版本 2.7 开始,它具有在侦听器退出后调用的附加方法(通常,或通过抛出异常)。此外,从 2.7 版开始,现在有一个,为Batch ListenersBatchInterceptor
提供类似的功能。此外,(and ) 提供对. 例如,这可能用于访问拦截器中的消费者指标。ConsumerAwareRecordInterceptor
BatchInterceptor
Consumer<?, ?>
您不应执行任何影响消费者位置和/或在这些拦截器中提交的偏移量的方法;容器需要管理这些信息。 |
如果拦截器改变了记录(通过创建一个新记录),则topic ,partition 和offset 必须保持不变以避免意外的副作用,例如记录丢失。
|
CompositeRecordInterceptor
and可CompositeBatchInterceptor
用于调用多个拦截器。
默认情况下,从 2.8 版本开始,当使用事务时,拦截器在事务开始之前被调用。您可以将侦听器容器的interceptBeforeTx
属性设置false
为在事务开始后调用拦截器。
从版本 2.3.8、2.4.6 开始,当并发大于一时,ConcurrentMessageListenerContainer
现在支持静态成员。的group.instance.id
后缀是-n
从n
开始1
。这与增加的 一起session.timeout.ms
,可用于减少重新平衡事件,例如,当应用程序实例重新启动时。
使用KafkaMessageListenerContainer
以下构造函数可用:
public KafkaMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,
ContainerProperties containerProperties)
它在对象中接收ConsumerFactory
有关主题和分区以及其他配置的信息ContainerProperties
。
ContainerProperties
具有以下构造函数:
public ContainerProperties(TopicPartitionOffset... topicPartitions)
public ContainerProperties(String... topics)
public ContainerProperties(Pattern topicPattern)
第一个构造函数接受一个TopicPartitionOffset
参数数组来显式地指示容器使用哪些分区(使用消费者assign()
方法)以及一个可选的初始偏移量。默认情况下,正值是绝对偏移量。默认情况下,负值相对于分区内的当前最后一个偏移量。TopicPartitionOffset
提供了一个带有附加boolean
参数的构造函数。如果是true
,则初始偏移量(正或负)相对于该消费者的当前位置。启动容器时应用偏移量。第二个采用一组主题,Kafka 根据group.id
属性分配分区——在组中分布分区。第三个使用正则表达式Pattern
来选择主题。
要将 a 分配MessageListener
给容器,您可以ContainerProps.setMessageListener
在创建容器时使用该方法。以下示例显示了如何执行此操作:
ContainerProperties containerProps = new ContainerProperties("topic1", "topic2");
containerProps.setMessageListener(new MessageListener<Integer, String>() {
...
});
DefaultKafkaConsumerFactory<Integer, String> cf =
new DefaultKafkaConsumerFactory<>(consumerProps());
KafkaMessageListenerContainer<Integer, String> container =
new KafkaMessageListenerContainer<>(cf, containerProps);
return container;
请注意,在创建 时DefaultKafkaConsumerFactory
,使用仅接受上述属性的构造函数意味着Deserializer
从配置中获取键和值类。或者,Deserializer
可以将实例传递给DefaultKafkaConsumerFactory
键和/或值的构造函数,在这种情况下,所有消费者共享相同的实例。另一种选择是提供Supplier<Deserializer>
s(从 2.3 版开始),用于为每个 s 获取单独Deserializer
的实例Consumer
:
DefaultKafkaConsumerFactory<Integer, CustomValue> cf =
new DefaultKafkaConsumerFactory<>(consumerProps(), null, () -> new CustomValueDeserializer());
KafkaMessageListenerContainer<Integer, String> container =
new KafkaMessageListenerContainer<>(cf, containerProps);
return container;
有关可以设置的各种属性的更多信息,请参阅Javadoc 。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 起)。如果代理上不存在任何已配置的主题,这将阻止容器启动。如果容器配置为侦听主题模式(正则表达式),则不适用。以前,容器线程在consumer.poll()
方法中循环等待主题出现,同时记录许多消息。除了日志,没有迹象表明存在问题。
从 2.8 版authExceptionRetryInterval
开始,引入了一个新的容器属性。AuthenticationException
这会导致容器在AuthorizationException
从KafkaConsumer
. 例如,当配置的用户被拒绝访问某个主题或凭据不正确时,就会发生这种情况。定义authExceptionRetryInterval
允许容器在授予适当权限时恢复。
默认情况下,没有配置时间间隔 - 身份验证和授权错误被认为是致命的,这会导致容器停止。 |
从 2.8 版本开始,在创建消费者工厂时,如果您将反序列化器作为对象提供(在构造函数中或通过设置器),工厂将调用该configure()
方法以使用配置属性对其进行配置。
使用ConcurrentMessageListenerContainer
单构造函数类似于KafkaListenerContainer
构造函数。以下清单显示了构造函数的签名:
public ConcurrentMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,
ContainerProperties containerProperties)
它也concurrency
有财产。例如,container.setConcurrency(3)
创建三个KafkaMessageListenerContainer
实例。
对于第一个构造函数,Kafka 使用其组管理功能在消费者之间分配分区。
在监听多个主题时,默认的分区分布可能不是你所期望的。例如,如果您有 3 个主题,每个主题有 5 个分区,并且您想使用 使用 Spring Boot 时,可以按如下方式指定设置策略:
|
当容器属性配置为TopicPartitionOffset
s 时,ConcurrentMessageListenerContainer
将实例分布TopicPartitionOffset
在委托KafkaMessageListenerContainer
实例之间。
例如,如果提供了六个TopicPartitionOffset
实例并且concurrency
是3
; 每个容器有两个分区。对于五个TopicPartitionOffset
实例,两个容器获得两个分区,第三个获得一个。如果concurrency
大于 的数量TopicPartitions
,concurrency
则向下调整,使得每个容器获得一个分区。
client.id 属性(如果设置)附加在-n wheren 是对应于并发的消费者实例
。当启用 JMX 时,这是为 MBean 提供唯一名称所必需的。
|
从版本 1.3 开始,MessageListenerContainer
提供对底层KafkaConsumer
. 在 的情况下ConcurrentMessageListenerContainer
,该metrics()
方法返回所有目标KafkaMessageListenerContainer
实例的指标。指标按为Map<MetricName, ? extends Metric>
基础client-id
提供的分组为KafkaConsumer
。
从版本 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
除非在配置中明确设置。true
以前,如果未设置该属性,则使用Kafka 默认值 ( )。
消费者poll()
方法返回一个或多个ConsumerRecords
。为MessageListener
每条记录调用。AckMode
以下列表描述了容器对每个(未使用事务时)采取的操作:
-
RECORD
:在监听器处理完记录返回时提交偏移量。 -
BATCH
:当所有返回的记录poll()
都处理完后,提交偏移量。 -
TIME
: 当所有返回的记录都被处理完时,提交偏移量poll()
,只要ackTime
超过了自上次提交的时间。 -
COUNT
: 当所有返回的记录都被处理后,提交偏移量poll()
,只要ackCount
从上次提交后已经收到记录。 -
COUNT_TIME
: 与TIME
and类似COUNT
,但如果任一条件为 ,则执行提交true
。 -
MANUAL
: 消息监听器acknowledge()
对Acknowledgment
. 之后,应用相同的语义BATCH
。 -
MANUAL_IMMEDIATE
Acknowledgment.acknowledge()
:当监听器调用方法时立即提交偏移量。
当使用transactions时,偏移量被发送到 transaction 并且语义等同于RECORD
or BATCH
,具体取决于侦听器类型(记录或批处理)。
MANUAL ,并MANUAL_IMMEDIATE 要求听者是一个AcknowledgingMessageListener 或一个BatchAcknowledgingMessageListener 。请参阅消息侦听器。
|
根据syncCommits
容器属性,使用消费者上的commitSync()
orcommitAsync()
方法。
默认情况下syncCommits
;true
也见setSyncCommitTimeout
。查看setCommitCallback
获取异步提交的结果;默认回调是LoggingCommitCallback
记录错误(以及调试级别的成功)。
因为侦听器容器有自己的提交偏移量的机制,所以它更喜欢 KafkaConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG
为false
. 从 2.3 版开始,它无条件地将其设置为 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()
。
有关详细信息,请参阅容器错误处理程序。
消费者在睡眠期间暂停,以便我们继续轮询代理以保持消费者存活。实际的睡眠时间及其分辨率取决于pollTimeout 默认为 5 秒的容器。最小睡眠时间等于pollTimeout 并且所有睡眠时间都是它的倍数。对于较小的睡眠时间,或者为了提高其准确性,请考虑减少容器的pollTimeout .
|
手动提交偏移量
通常,使用AckMode.MANUAL
or时AckMode.MANUAL_IMMEDIATE
,必须按顺序确认确认,因为 Kafka 不维护每条记录的状态,只为每个组/分区维护一个已提交的偏移量。从 2.8 版开始,您现在可以设置 container 属性asyncAcks
,它允许以任何顺序确认对轮询返回的记录的确认。侦听器容器将推迟无序提交,直到收到丢失的确认。消费者将被暂停(不提供新记录),直到前一次轮询的所有偏移量都已提交。
虽然此功能允许应用程序异步处理记录,但应该理解它增加了失败后重复交付的可能性。 |
@KafkaListener
注解
@KafkaListener
注解用于指定一个 bean 方法作为侦听器容器的侦听器。bean 被包装在一个MessagingMessageListenerAdapter
配置有各种功能的组件中,例如转换器来转换数据,如果需要,匹配方法参数。
您可以使用#{…}
或属性占位符 ( ${…}
) 在带有 SpEL 的注释上配置大多数属性。有关更多信息,请参阅Javadoc。
记录听众
@KafkaListener
注释为简单的 POJO 侦听器提供了一种机制。以下示例显示了如何使用它:
public class Listener {
@KafkaListener(id = "foo", topics = "myTopic", clientIdPrefix = "myClientId")
public void listen(String data) {
...
}
}
此机制需要@EnableKafka
在您的一个类和一个侦听器容器工厂上添加注释@Configuration
,该工厂用于配置底层ConcurrentMessageListenerContainer
. 默认情况下,需要一个具有名称的 bean kafkaListenerContainerFactory
。下面的例子展示了如何使用ConcurrentMessageListenerContainer
:
@Configuration
@EnableKafka
public class KafkaConfig {
@Bean
KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(3);
factory.getContainerProperties().setPollTimeout(3000);
return factory;
}
@Bean
public ConsumerFactory<Integer, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}
@Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> 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
您可以在orpartitionOffsets
属性中指定每个分区,但不能同时指定两者。
与大多数注释属性一样,您可以使用 SpEL 表达式;有关如何生成大量分区列表的示例,请参阅手动分配所有分区。
从版本 2.5.5 开始,您可以将初始偏移量应用于所有分配的分区:
@KafkaListener(id = "thing3", topicPartitions =
{ @TopicPartition(topic = "topic1", partitions = { "0", "1" },
partitionOffsets = @PartitionOffset(partition = "*", initialOffset = "0"))
})
public void listen(ConsumerRecord<?, ?> record) {
...
}
*
通配符代表partitions
属性中的所有分区。每个 中只能有一个@PartitionOffset
带有通配符@TopicPartition
。
此外,即使在使用手动分配时,现在也调用侦听器实现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 个分区。
手动确认
使用手动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 版开始,如果传入记录有键,RECEIVED_MESSAGE_KEY
则不存在;null
以前,标题填充了一个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<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setBatchListener(true); // <<<<<<<<<<<<<<<<<<<<<<<<<
return factory;
}
从 2.8 版开始,您可以batchListener 使用注释batch 上的属性覆盖工厂的属性。这与对容器错误处理程序@KafkaListener 的更改一起允许将同一工厂用于记录和批处理侦听器。
|
以下示例显示了如何接收有效负载列表:
@KafkaListener(id = "list", topics = "myTopic", containerFactory = "batchFactory")
public void listen(List<String> list) {
...
}
主题、分区、偏移量等在与有效负载并行的标头中可用。以下示例显示了如何使用标头:
@KafkaListener(id = "list", topics = "myTopic", containerFactory = "batchFactory")
public void listen(List<String> list,
@Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) List<Integer> keys,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) List<Integer> partitions,
@Header(KafkaHeaders.RECEIVED_TOPIC) List<String> topics,
@Header(KafkaHeaders.OFFSET) List<Long> offsets) {
...
}
或者,您可以在每条消息中接收带有每个偏移量和其他详细信息List
的Message<?>
对象,但它必须是方法上定义的唯一参数(除了 optional Acknowledgment
,在使用手动提交时,和/或Consumer<?, ?>
参数)。以下示例显示了如何执行此操作:
@KafkaListener(id = "listMsg", topics = "myTopic", containerFactory = "batchFactory")
public void listen14(List<Message<?>> list) {
...
}
@KafkaListener(id = "listMsgAck", topics = "myTopic", containerFactory = "batchFactory")
public void listen15(List<Message<?>> list, Acknowledgment ack) {
...
}
@KafkaListener(id = "listMsgAckConsumer", topics = "myTopic", containerFactory = "batchFactory")
public void listen16(List<Message<?>> list, Acknowledgment ack, Consumer<?, ?> consumer) {
...
}
在这种情况下,不对有效负载执行任何转换。
如果BatchMessagingMessageConverter
配置了RecordMessageConverter
,您还可以将泛型类型添加到Message
参数并转换有效负载。有关详细信息,请参阅使用批处理侦听器进行有效负载转换。
您还可以接收ConsumerRecord<?, ?>
对象列表,但它必须是方法上定义的唯一参数(除了 optional Acknowledgment
,使用手动提交和Consumer<?, ?>
参数时)。以下示例显示了如何执行此操作:
@KafkaListener(id = "listCRs", topics = "myTopic", containerFactory = "batchFactory")
public void listen(List<ConsumerRecord<Integer, String>> list) {
...
}
@KafkaListener(id = "listCRsAck", topics = "myTopic", containerFactory = "batchFactory")
public void listen(List<ConsumerRecord<Integer, String>> list, Acknowledgment ack) {
...
}
从 2.2 版本开始,监听器可以接收方法ConsumerRecords<?, ?>
返回的完整对象poll()
,让监听器访问其他方法,例如partitions()
(返回TopicPartition
列表中的实例)和records(TopicPartition)
(获取选择性记录)。同样,这必须是方法上的唯一参数(除了 optional Acknowledgment
,当使用手动提交或Consumer<?, ?>
参数时)。以下示例显示了如何执行此操作:
@KafkaListener(id = "pollResults", topics = "myTopic", containerFactory = "batchFactory")
public void pollResults(ConsumerRecords<?, ?> records) {
...
}
如果容器工厂已RecordFilterStrategy 配置,则ConsumerRecords<?, ?> 侦听器将忽略它,并WARN 发出日志消息。<List<?>> 如果使用监听器的形式,则只能使用批处理监听器过滤记录。默认情况下,一次过滤一条记录;从 2.8 版开始,您可以重写filterBatch 以在一次调用中过滤整个批次。
|
注释属性
从 2.0 版开始,该id
属性(如果存在)用作 Kafka 消费者group.id
属性,覆盖消费者工厂中配置的属性(如果存在)。您还可以groupId
显式设置或设置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 消费者属性,这些将覆盖消费者工厂中配置的任何同名属性。您不能以这种方式指定group.id
andclient.id
属性;它们将被忽略;使用这些groupId
和clientIdPrefix
注释属性。
这些属性被指定为具有普通 JavaProperties
文件格式的单个字符串:foo:bar
、foo=bar
或foo bar
.
@KafkaListener(topics = "myTopic", groupId = "group", properties = {
"max.poll.interval.ms:60000",
ConsumerConfig.MAX_POLL_RECORDS_CONFIG + "=100"
})
以下是UsingRoutingKafkaTemplate
中示例的相应侦听器示例。
@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
属性来提供自定义执行器ContainerProperties
。使用池执行器时,请确保有足够的线程可用于处理使用它们的所有容器的并发性。使用 时ConcurrentMessageListenerContainer
,每个使用者 ( concurrency
) 使用每个线程。
如果您不提供消费者执行程序,SimpleAsyncTaskExecutor
则使用 a。此执行程序创建名称类似于<beanName>-C-1
(consumer thread) 的线程。对于ConcurrentMessageListenerContainer
,<beanName>
线程名称的部分变为<beanName>-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
, or中的至少一个起别名topicPartitions
(并且,通常,id
或者除非您在消费者工厂配置中groupId
指定了 a )。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
。有关详细信息,请参阅序列化、反序列化和消息转换。
由于 Spring 解析方法参数的方式存在一些限制,默认值@KafkaHandler 不能接收离散的 headers;它必须使用Consumer Record MetadataConsumerRecordMetadata 中讨论的。
|
例如:
@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();
...
}
@KafkaListener
属性修改
从版本 2.7.2 开始,您现在可以在创建容器之前以编程方式修改注释属性。为此,请将一个或多个添加KafkaListenerAnnotationBeanPostProcessor.AnnotationEnhancer
到应用程序上下文中。
AnnotationEnhancer
是一个BiFunction<Map<String, Object>, AnnotatedElement, Map<String, Object>
并且必须返回一个属性映射。属性值可以包含 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。相反,它们使用 类型的基础设施 bean 进行注册KafkaListenerEndpointRegistry
。该 bean 由框架自动声明并管理容器的生命周期;它将自动启动任何已autoStartup
设置为true
. 所有容器工厂创建的所有容器必须在同一个phase
. 有关详细信息,请参阅侦听器容器自动启动。您可以使用注册表以编程方式管理生命周期。启动或停止注册表将启动或停止所有已注册的容器。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 声明。
刷新应用程序上下文后注册的端点将立即启动,无论其autoStartup 属性如何,以遵守SmartLifecycle 合同,其中autoStartup 仅在应用程序上下文初始化期间考虑。延迟注册的一个示例是具有@KafkaListener 原型范围的 bean,其中在初始化上下文后创建实例。从版本 2.8.7 开始,您可以将注册表的alwaysStartAfterRefresh 属性设置为false ,然后容器的autoStartup 属性将定义容器是否启动。
|
@KafkaListener
@Payload
验证
从 2.2 版开始,现在可以更轻松地添加 aValidator
来验证@KafkaListener
@Payload
参数。以前,您必须配置自定义DefaultMessageHandlerMethodFactory
并将其添加到注册器。现在,您可以将验证器添加到注册商本身。以下代码显示了如何执行此操作:
@Configuration
@EnableKafka
public class Config implements KafkaListenerConfigurer {
...
@Override
public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
registrar.setValidator(new MyValidator());
}
}
当您将 Spring Boot 与验证启动器一起使用时,LocalValidatorFactoryBean 会自动配置 a,如以下示例所示:
|
@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 开始,验证现在适用于@KafkaHandler
类级侦听器中方法的有效负载。见@KafkaListener
上一课。
重新平衡侦听器
ContainerProperties
有一个名为 的属性consumerRebalanceListener
,它采用 Kafka 客户端ConsumerRebalanceListener
接口的实现。如果未提供此属性,则容器会配置一个日志记录侦听器,用于在INFO
级别记录重新平衡事件。该框架还添加了一个子接口ConsumerAwareRebalanceListener
。以下清单显示了ConsumerAwareRebalanceListener
接口定义:
public interface ConsumerAwareRebalanceListener extends ConsumerRebalanceListener {
void onPartitionsRevokedBeforeCommit(Consumer<?, ?> consumer, Collection<TopicPartition> partitions);
void onPartitionsRevokedAfterCommit(Consumer<?, ?> consumer, Collection<TopicPartition> partitions);
void onPartitionsAssigned(Consumer<?, ?> consumer, Collection<TopicPartition> partitions);
void onPartitionsLost(Consumer<?, ?> consumer, Collection<TopicPartition> partitions);
}
请注意,撤销分区时有两个回调。第一个被立即调用。第二个在提交任何待处理的偏移量后调用。如果您希望在某些外部存储库中维护偏移量,这将很有用,如以下示例所示:
containerProperties.setConsumerRebalanceListener(new ConsumerAwareRebalanceListener() {
@Override
public void onPartitionsRevokedBeforeCommit(Consumer<?, ?> consumer, Collection<TopicPartition> partitions) {
// acknowledge any pending Acknowledgments (if using manual acks)
}
@Override
public void onPartitionsRevokedAfterCommit(Consumer<?, ?> consumer, Collection<TopicPartition> partitions) {
// ...
store(consumer.position(partition));
// ...
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// ...
consumer.seek(partition, offsetTracker.getOffset() + 1);
// ...
}
});
从 2.4 版本开始,onPartitionsLost() 添加了一个新方法(类似于 中的同名方法ConsumerRebalanceLister )。ConsumerRebalanceLister 简单调用的默认实现onPartionsRevoked 。默认实现 onConsumerAwareRebalanceListener 什么都不做。在为侦听器容器提供自定义侦听器(任一类型)时,重要的是您的实现不要onPartitionsRevoked 从onPartitionsLost . 如果您实施ConsumerRebalanceListener ,您应该覆盖默认方法。这是因为监听器容器会在调用你的实现方法之后onPartitionsRevoked 从它的实现中调用它自己onPartitionsLost 的。如果您实现委托到默认行为,onPartitionsRevoked 则每次Consumer 在容器的侦听器上调用该方法时都会被调用两次。
|
使用转发侦听器结果@SendTo
从 2.0 版本开始,如果您还使用注解对@KafkaListener
a 进行@SendTo
注解,并且方法调用返回结果,则将结果转发到@SendTo
.
该@SendTo
值可以有多种形式:
-
@SendTo("someTopic")
到文字主题的路线 -
@SendTo("#{someExpression}")
通过在应用程序上下文初始化期间评估一次表达式确定的主题的路由。 -
@SendTo("!{someExpression}")
通过在运行时评估表达式确定的主题的路由。评估#root
对象具有三个属性:-
request
:入站ConsumerRecord
(或ConsumerRecords
批处理侦听器的对象)) -
source
:org.springframework.messaging.Message<?>
从request
. -
result
: 方法返回结果。
-
-
@SendTo
(无属性):这被视为!{source.headers['kafka_replyTopic']}
(从版本 2.1.3 开始)。
从版本 2.1.11 和 2.2.1 开始,属性占位符在@SendTo
值内解析。
表达式求值的结果必须是String
表示主题名称的 a。以下示例显示了各种使用方法@SendTo
:
@KafkaListener(topics = "annotated21")
@SendTo("!{request.value()}") // runtime SpEL
public String replyingListener(String in) {
...
}
@KafkaListener(topics = "${some.property:annotated22}")
@SendTo("#{myBean.replyTopic}") // config time SpEL
public Collection<String> replyingBatchListener(List<String> in) {
...
}
@KafkaListener(topics = "annotated23", errorHandler = "replyErrorHandler")
@SendTo("annotated23reply") // static reply topic definition
public String replyingListenerWithErrorHandler(String in) {
...
}
...
@KafkaListener(topics = "annotated25")
@SendTo("annotated25reply1")
public class MultiListenerSendTo {
@KafkaHandler
public String foo(String in) {
...
}
@KafkaHandler
@SendTo("!{'annotated25reply2'}")
public String bar(@Payload(required = false) KafkaNull nul,
@Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) int key) {
...
}
}
为了支持@SendTo ,侦听器容器工厂必须提供一个KafkaTemplate (在其replyTemplate 属性中),用于发送回复。这应该是 aKafkaTemplate 而不是 aReplyingKafkaTemplate 在客户端用于请求/回复处理。使用 Spring Boot 时,boot 会自动将模板配置到工厂中;在配置自己的工厂时,必须按照以下示例进行设置。
|
从版本 2.2 开始,您可以ReplyHeadersConfigurer
向侦听器容器工厂添加一个。参考此信息以确定您要在回复消息中设置哪些标头。以下示例显示了如何添加ReplyHeadersConfigurer
:
@Bean
public ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf());
factory.setReplyTemplate(template());
factory.setReplyHeadersConfigurer((k, v) -> k.equals("cat"));
return factory;
}
如果您愿意,还可以添加更多标题。以下示例显示了如何执行此操作:
@Bean
public ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(cf());
factory.setReplyTemplate(template());
factory.setReplyHeadersConfigurer(new ReplyHeadersConfigurer() {
@Override
public boolean shouldCopy(String headerName, Object headerValue) {
return false;
}
@Override
public Map<String, Object> additionalHeaders() {
return Collections.singletonMap("qux", "fiz");
}
});
return factory;
}
使用 时@SendTo
,必须在其属性中配置ConcurrentKafkaListenerContainerFactory
以执行发送。KafkaTemplate
replyTemplate
除非您使用请求/回复语义,否则仅使用简单send(topic, value) 方法,因此您可能希望创建一个子类来生成分区或键。以下示例显示了如何执行此操作:
|
@Bean
public KafkaTemplate<String, String> myReplyingTemplate() {
return new KafkaTemplate<Integer, String>(producerFactory()) {
@Override
public ListenableFuture<SendResult<String, String>> send(String topic, String data) {
return super.send(topic, partitionForData(data), keyForData(data), data);
}
...
};
}
如果侦听器方法返回
|
当使用请求/回复语义时,目标分区可以由发送者请求。
即使没有返回结果,您也可以注释
有关详细信息,请参阅处理异常。 |
如果侦听器方法返回一个Iterable ,则默认情况下,每个元素都会在发送值时记录一条记录。从版本 2.3.5 开始,将splitIterables 属性设置@KafkaListener 为false ,整个结果将作为单个ProducerRecord . 这需要在回复模板的生产者配置中使用合适的序列化程序。但是,如果回复是Iterable<Message<?>> ,则忽略该属性并单独发送每条消息。
|
过滤消息
在某些情况下,例如重新平衡,可能会重新传递已经处理的消息。框架无法知道这样的消息是否已被处理。那是一个应用程序级的功能。这被称为Idempotent Receiver模式,Spring Integration 提供了它的实现。
Spring for Apache Kafka 项目还通过FilteringMessageListenerAdapter
类提供了一些帮助,它可以包装你的MessageListener
. 此类采用一个实现RecordFilterStrategy
,您可以在其中实现该filter
方法来表示消息是重复的并且应该被丢弃。这有一个称为 的附加属性ackDiscarded
,它指示适配器是否应确认丢弃的记录。false
默认情况下。
使用 时@KafkaListener
,在容器工厂上设置RecordFilterStrategy
(和可选ackDiscarded
的),以便将侦听器包装在适当的过滤适配器中。
此外,FilteringBatchMessageListenerAdapter
还提供了 a ,供您使用批处理消息侦听器时使用。
FilteringBatchMessageListenerAdapter 如果您@KafkaListener 收到 a
而ConsumerRecords<?, ?> 不是 ,则忽略List<ConsumerRecord<?, ?>> ,因为ConsumerRecords 它是不可变的。
|
从版本 2.8.4 开始,您可以RecordFilterStrategy
使用filter
侦听器注释上的属性覆盖侦听器容器工厂的默认值。
@KafkaListener(id = "filtered", topics = "topic", filter = "differentFilter")
public void listen(Thing thing) {
...
}
重试交付
请参阅处理异常DefaultErrorHandler
中的。
@KafkaListener
按顺序开始
一个常见的用例是在另一个侦听器消耗了主题中的所有记录后启动侦听器。例如,您可能希望在处理来自其他主题的记录之前将一个或多个压缩主题的内容加载到内存中。从版本 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
.
在应用程序上下文初始化期间,定序autoStartup
器将提供的组中所有容器的属性设置为false
. 它还将idleEventInterval
任何容器(还没有一组)设置为提供的值(在本例中为 5000 毫秒)。然后,当应用程序上下文启动定序器时,第一组中的容器被启动。当ListenerContainerIdleEvent
接收到 s 时,每个容器中的每个单独的子容器都会停止。当 a 中的所有子容器ConcurrentMessageListenerContainer
都停止时,父容器也会停止。当一个组中的所有容器都已停止时,下一个组中的容器将启动。组中的组或容器的数量没有限制。
默认情况下,最后一组(g2
上面)中的容器在空闲时不会停止。要修改该行为,请在音序器上设置stopLastGroupWhenIdle
为。true
作为旁白; 以前,每个组中的容器都被添加到类型Collection<MessageListenerContainer>
为 bean 的 bean 中containerGroup
。这些集合现在已被弃用,取而代之的是具有 bean 名称的类型ContainerGroup
的 bean,该 bean 名称是组名称,后缀为.group
; 在上面的示例中,将有 2 个 beang1.group
和g2.group
. bean将Collection
在未来的版本中删除。
用于KafkaTemplate
接收
本节介绍如何使用KafkaTemplate
来接收消息。
从 2.8 版本开始,模板有四种receive()
方法:
ConsumerRecord<K, V> receive(String topic, int partition, long offset);
ConsumerRecord<K, V> receive(String topic, int partition, long offset, Duration pollTimeout);
ConsumerRecords<K, V> receive(Collection<TopicPartitionOffset> requested);
ConsumerRecords<K, V> receive(Collection<TopicPartitionOffset> requested, Duration pollTimeout);
如您所见,您需要知道需要检索的记录的分区和偏移量;Consumer
为每个操作创建(并关闭)一个新的。
使用最后两种方法,分别检索每条记录并将结果组合到一个ConsumerRecords
对象中。在为请求创建TopicPartitionOffset
s 时,仅支持正的绝对偏移量。
4.1.5。侦听器容器属性
财产 | 默认 | 描述 |
---|---|---|
1 |
当 |
|
|
包装消息侦听器的对象链 |
|
批 |
控制提交偏移量的频率 - 请参阅提交偏移量。 |
|
5000 |
|
|
LATEST_ONLY _NO_TX |
是否承诺在分配的初始位置;默认情况下,只有在存在时才会提交初始偏移量,即使存在事务管理器,它也不会在事务中运行 |
|
|
如果不为 null,则当Kafka 客户端抛出or |
|
(空字符串) |
|
|
错误的 |
设置为在收到a 时 |
|
错误的 |
设置为在收到a 时 |
|
|
如果存在并且 |
|
|
提供者 |
|
调试 |
与提交偏移量有关的日志的日志记录级别。 |
|
|
一个再平衡监听器;请参阅重新平衡侦听器。 |
|
30 多岁 |
在记录错误之前等待消费者启动的时间;例如,如果您使用线程不足的任务执行器,则可能会发生这种情况。 |
|
|
运行消费者线程的任务执行器。默认执行器创建名为 |
|
|
请参阅传递尝试标题。 |
|
|
Exactly Once 语义模式;请参阅Exactly Once 语义。 |
|
|
当使用事务生产者生成的记录,并且消费者位于分区的末尾时,延迟可能会错误地报告为大于零,这是由于用于指示事务提交/回滚的伪记录以及可能的存在的回滚记录。这在功能上不会影响消费者,但一些用户表示担心“滞后”不是零。将此属性设置为 |
|
|
覆盖消费者 |
|
5.0 |
|
|
0 |
用于通过在轮询之间休眠线程来减慢交付速度。处理一批记录的时间加上这个值必须小于 |
|
|
设置后,启用 |
|
|
设置后,启用 |
|
没有任何 |
用于覆盖在消费者工厂上配置的任意消费者属性。 |
|
|
设置为 true 以在 INFO 级别记录所有容器属性。 |
|
|
消息侦听器。 |
|
|
是否为消费者线程维护 Micrometer 计时器。 |
|
|
当为 true 时,如果代理上不存在已配置的主题,则阻止容器启动。 |
|
30 多岁 |
多久检查一次消费者线程的状态 |
|
3.0 |
乘以 |
|
|
设置为 false 以记录完整的消费者记录(错误、调试日志等),而不仅仅是 |
|
5000 |
超时传入 |
|
|
在其上运行消费者监控任务的调度程序。 |
|
10000 |
|
|
|
|
|
|
当容器停止时,在当前记录之后停止处理,而不是在处理完之前轮询的所有记录之后。 |
|
见描述。 |
使用批处理侦听器时,如果是 |
|
|
使用的超时时间 |
|
|
是否对偏移量使用同步或异步提交;见 |
|
不适用 |
配置的主题、主题模式或明确分配的主题/分区。互斥;必须提供至少一个;由 |
|
|
请参阅交易。 |
财产 | 默认 | 描述 |
---|---|---|
|
在 |
|
应用上下文 |
事件发布者。 |
|
见描述。 |
已弃用 - 请参阅 |
|
|
在调用批处理侦听器之前设置一个 |
|
豆名 |
容器的bean名称;后缀 |
|
见描述。 |
|
|
|
容器属性实例。 |
|
见描述。 |
已弃用 - 请参阅 |
|
见描述。 |
已弃用 - 请参阅 |
|
见描述。 |
, |
|
|
确定是 |
|
见描述。 |
用户配置容器的 bean 名称或 s 的 |
|
无效的 |
要填充在 |
|
(只读) |
如果已请求消费者暂停,则为真。 |
|
|
在调用记录监听器之前设置一个 |
|
30 多岁 |
当 |
财产 | 默认 | 描述 |
---|---|---|
(只读) |
当前分配给此容器的分区(显式或非显式)。 |
|
(只读) |
当前分配给此容器的分区(显式或非显式)。 |
|
|
由并发容器用来给每个子容器的消费者一个唯一的 |
|
不适用 |
如果已请求暂停并且消费者实际上已暂停,则为真。 |
财产 | 默认 | 描述 |
---|---|---|
|
设置为 false 以禁止向 |
|
(只读) |
当前分配给此容器的子 |
|
(只读) |
当前分配给此容器的子 |
|
1 |
|
|
不适用 |
如果已请求暂停并且所有子容器的使用者实际上已暂停,则为真。 |
|
不适用 |
对所有 child |
4.1.6。应用程序事件
以下 Spring 应用程序事件由侦听器容器及其使用者发布:
-
ConsumerStartingEvent
- 在消费者线程第一次启动时发布,在它开始轮询之前。 -
ConsumerStartedEvent
- 在消费者即将开始轮询时发布。 -
ConsumerFailedToStartEvent
- 如果没有ConsumerStartingEvent
在consumerStartTimeout
容器属性中发布,则发布。此事件可能表明配置的任务执行器没有足够的线程来支持它所使用的容器及其并发性。发生这种情况时,还会记录一条错误消息。 -
ListenerContainerIdleEvent
:在没有收到消息时发布idleInterval
(如果配置)。 -
ListenerContainerNoLongerIdleEvent
: 在先前发布 . 之后使用记录时发布ListenerContainerIdleEvent
。 -
ListenerContainerPartitionIdleEvent
:当没有从该分区收到消息时发布idlePartitionEventInterval
(如果已配置)。 -
ListenerContainerPartitionNoLongerIdleEvent
: 当一条记录从之前发布过的分区中消费时发布ListenerContainerPartitionIdleEvent
。 -
NonResponsiveConsumerEvent
:当消费者似乎在poll
方法中被阻止时发布。 -
ConsumerPartitionPausedEvent
: 由每个消费者在分区暂停时发布。 -
ConsumerPartitionResumedEvent
:恢复分区时由每个消费者发布。 -
ConsumerPausedEvent
:容器暂停时由每个消费者发布。 -
ConsumerResumedEvent
: 由每个消费者在容器恢复时发布。 -
ConsumerStoppingEvent
:由每个消费者在停止之前发布。 -
ConsumerStoppedEvent
: 消费者关闭后发布。请参阅线程安全。 -
ContainerStoppedEvent
:当所有消费者都停止时发布。
默认情况下,应用程序上下文的事件多播器在调用线程上调用事件侦听器。Consumer 如果您将多播器更改为使用异步执行器,则当事件包含对使用者的引用时
,您不得调用任何方法。 |
具有以下ListenerContainerIdleEvent
属性:
-
source
:发布事件的侦听器容器实例。 -
container
:侦听器容器或父侦听器容器,如果源容器是子容器。 -
id
:侦听器 ID(或容器 bean 名称)。 -
idleTime
:发布事件时容器空闲的时间。 -
topicPartitions
: 容器在事件生成时分配的主题和分区。 -
consumer
: 对 KafkaConsumer
对象的引用。例如,如果pause()
之前调用了消费者的方法,它可以resume()
在收到事件时调用。 -
paused
: 容器当前是否处于暂停状态。有关详细信息,请参阅暂停和恢复侦听器容器。
具有相同的ListenerContainerNoLongerIdleEvent
属性,除了idleTime
和paused
。
具有以下ListenerContainerPartitionIdleEvent
属性:
-
source
:发布事件的侦听器容器实例。 -
container
:侦听器容器或父侦听器容器,如果源容器是子容器。 -
id
:侦听器 ID(或容器 bean 名称)。 -
idleTime
: 发布事件时,时间分区消耗一直处于空闲状态。 -
topicPartition
:触发事件的主题和分区。 -
consumer
: 对 KafkaConsumer
对象的引用。例如,如果pause()
之前调用了消费者的方法,它可以resume()
在收到事件时调用。 -
paused
:该分区消费当前是否为该消费者暂停。有关详细信息,请参阅暂停和恢复侦听器容器。
具有相同的ListenerContainerPartitionNoLongerIdleEvent
属性,除了idleTime
和paused
。
具有以下NonResponsiveConsumerEvent
属性:
-
source
:发布事件的侦听器容器实例。 -
container
:侦听器容器或父侦听器容器,如果源容器是子容器。 -
id
:侦听器 ID(或容器 bean 名称)。 -
timeSinceLastPoll
: 容器最后一次调用之前的时间poll()
。 -
topicPartitions
: 容器在事件生成时分配的主题和分区。 -
consumer
: 对 KafkaConsumer
对象的引用。例如,如果pause()
之前调用了消费者的方法,它可以resume()
在收到事件时调用。 -
paused
: 容器当前是否处于暂停状态。有关详细信息,请参阅暂停和恢复侦听器容器。
、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
- 一个java.lang.Error
被抛出。 -
FENCED
- 交易生产者被围起来,stopContainerWhenFenced
容器财产是true
. -
AUTH
-AuthenticationException
orAuthorizationException
被抛出并且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<String, String> consumerFactory) {
ContainerProperties containerProps = new ContainerProperties("topic1", "topic2");
...
containerProps.setIdleEventInterval(60000L);
...
KafkaMessageListenerContainer<String, String> container = new KafKaMessageListenerContainer<>(...);
return container;
}
以下示例显示了如何设置idleEventInterval
for a @KafkaListener
:
@Bean
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
...
factory.getContainerProperties().setIdleEventInterval(60000L);
...
return factory;
}
在每种情况下,当容器空闲时,每分钟发布一次事件。
如果由于某种原因,消费者poll()
方法没有退出,则不会收到任何消息并且无法生成空闲事件(这kafka-clients
是无法访问代理的早期版本的问题)。NonResponsiveConsumerEvent
在这种情况下,如果民意调查未在属性内返回3x
,pollTimeout
则容器会发布 a 。默认情况下,此检查在每个容器中每 30 秒执行一次。您可以通过在配置侦听器容器时设置monitorInterval
(默认 30 秒)和noPollThreshold
(默认 3.0)属性来修改此行为。应该大于以避免由于竞争条件ContainerProperties
而出现虚假事件。接收到这样的事件可以让您停止容器,从而唤醒消费者以便它可以停止。noPollThreshold
1.0
从 2.6.2 版本开始,如果容器发布了 a ListenerContainerIdleEvent
,它将ListenerContainerNoLongerIdleEvent
在随后收到记录时发布 a 。
事件消费
您可以通过实现来捕获这些事件ApplicationListener
——一个通用的侦听器或一个缩小为仅接收此特定事件的侦听器。您也可以使用@EventListener
Spring Framework 4.2 中引入的 ,。
下一个示例将@KafkaListener
and组合@EventListener
成一个类。您应该了解应用程序侦听器获取所有容器的事件,因此如果您想根据哪个容器空闲采取特定操作,您可能需要检查侦听器 ID。您也可以将@EventListener
condition
用于此目的。
有关事件属性的信息,请参阅应用程序事件。
事件通常在消费者线程上发布,因此与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 在条件下使用的原因。
|
如果您希望使用空闲事件来停止侦听器容器,则不应调用container.stop() 调用侦听器的线程。这样做会导致延迟和不必要的日志消息。相反,您应该将事件交给另一个线程,然后该线程可以停止容器。stop() 此外,如果它是子容器,则不应使用容器实例。您应该改为停止并发容器。
|
空闲时的当前位置
请注意,您可以通过ConsumerSeekAware
在侦听器中实现检测到空闲时获取当前位置。请参阅寻求特定偏移onIdleContainer()
量。
4.1.7. 主题/分区初始偏移量
有几种方法可以设置分区的初始偏移量。
手动分配分区时,您可以在配置的TopicPartitionOffset
参数中设置初始偏移量(如果需要)(请参阅消息侦听器容器)。您也可以随时寻找特定的偏移量。
当您使用代理分配分区的组管理时:
-
对于一个新
group.id
的,初始偏移量由auto.offset.reset
消费者属性(earliest
或latest
)确定。 -
对于现有组 ID,初始偏移量是该组 ID 的当前偏移量。但是,您可以在初始化期间(或之后的任何时间)寻找特定的偏移量。
4.1.8。寻求特定的偏移量
为了寻找,你的监听器必须实现ConsumerSeekAware
,它有以下方法:
void registerSeekCallback(ConsumerSeekCallback callback);
void onPartitionsAssigned(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback);
void onPartitionsRevoked(Collection<TopicPartition> partitions)
void onIdleContainer(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback);
当registerSeekCallback
容器启动和分配分区时调用。在初始化后的任意时间进行搜索时,您应该使用此回调。您应该保存对回调的引用。如果您在多个容器(或 a ConcurrentMessageListenerContainer
)中使用相同的侦听器,则应将回调存储在 aThreadLocal
或其他由 listener 键入的结构中Thread
。
使用组管理时,onPartitionsAssigned
在分配分区时调用。您可以使用此方法,例如,通过调用回调设置分区的初始偏移量。您还可以使用此方法将此线程的回调与分配的分区相关联(参见下面的示例)。您必须使用回调参数,而不是传递给registerSeekCallback
. 从版本 2.5.5 开始,即使使用手动分区分配,也会调用此方法。
onPartitionsRevoked
当容器停止或 Kafka 撤销分配时调用。您应该丢弃此线程的回调并删除与已撤销分区的任何关联。
回调有以下方法:
void seek(String topic, int partition, long offset);
void seekToBeginning(String topic, int partition);
void seekToBeginning(Collection=<TopicPartitions> partitions);
void seekToEnd(String topic, int partition);
void seekToEnd(Collection=<TopicPartitions> partitions);
void seekRelative(String topic, int partition, long offset, boolean toCurrent);
void seekToTimestamp(String topic, int partition, long timestamp);
void seekToTimestamp(Collection<TopicPartition> topicPartitions, long timestamp);
seekRelative
在 2.3 版中添加,用于执行相对搜索。
-
offset
负数和toCurrent
false
- 相对于分区末尾的查找。 -
offset
正和toCurrent
false
- 相对于分区开头的查找。 -
offset
负和toCurrent
true
- 相对于当前位置搜索(倒带)。 -
offset
正和toCurrent
true
- 相对于当前位置搜索(快进)。
这些seekToTimestamp
方法也在 2.3 版中添加。
在onIdleContainer oronPartitionsAssigned 方法中为多个分区寻找相同的时间戳时,首选第二种方法,因为在对消费者offsetsForTimes 方法的一次调用中找到时间戳的偏移量更有效。当从其他位置调用时,容器将收集所有时间戳查找请求并调用offsetsForTimes .
|
您还可以onIdleContainer()
在检测到空闲容器时执行查找操作。有关如何启用空闲容器检测的信息,请参阅检测空闲和非响应消费者。
接受集合的seekToBeginning 方法很有用,例如,在处理压缩主题并且您希望每次启动应用程序时都从头开始:
|
public class MyListener implements ConsumerSeekAware {
...
@Override
public void onPartitionsAssigned(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
callback.seekToBeginning(assignments.keySet());
}
}
registerSeekCallback
要在运行时任意查找,请使用来自适当线程的回调引用。
这是一个简单的 Spring Boot 应用程序,演示了如何使用回调;它向主题发送 10 条记录;在控制台中点击<Enter>
会导致所有分区搜索到开头。
@SpringBootApplication
public class SeekExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SeekExampleApplication.class, args);
}
@Bean
public ApplicationRunner runner(Listener listener, KafkaTemplate<String, String> 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<ConsumerSeekCallback> callbackForThread = new ThreadLocal<>();
private final Map<TopicPartition, ConsumerSeekCallback> callbacks = new ConcurrentHashMap<>();
@Override
public void registerSeekCallback(ConsumerSeekCallback callback) {
this.callbackForThread.set(callback);
}
@Override
public void onPartitionsAssigned(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
assignments.keySet().forEach(tp -> this.callbacks.put(tp, this.callbackForThread.get()));
}
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
partitions.forEach(tp -> this.callbacks.remove(tp));
this.callbackForThread.remove();
}
@Override
public void onIdleContainer(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
}
@KafkaListener(id = "seekExample", topics = "seekExample", concurrency = "3")
public void listen(ConsumerRecord<String, String> 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<org.apache.kafka.common.TopicPartition, Long> 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
如Annotation中所讨论的, aConcurrentKafkaListenerContainerFactory
用于为带注释的方法创建容器。
从 2.2 版开始,您可以使用相同的工厂来创建任何ConcurrentMessageListenerContainer
. 如果您想创建多个具有相似属性的容器,或者您希望使用一些外部配置的工厂,例如 Spring Boot 自动配置提供的工厂,这可能会很有用。创建容器后,您可以进一步修改其属性,其中许多属性是使用container.getContainerProperties()
. 以下示例配置了一个ConcurrentMessageListenerContainer
:
@Bean
public ConcurrentMessageListenerContainer<String, String>(
ConcurrentKafkaListenerContainerFactory<String, String> factory) {
ConcurrentMessageListenerContainer<String, String> container =
factory.createContainer("topic1", "topic2");
container.setMessageListener(m -> { ... } );
return container;
}
以这种方式创建的容器不会添加到端点注册表中。它们应该作为@Bean 定义创建,以便在应用程序上下文中注册。
|
从版本 2.3.4 开始,您可以ContainerCustomizer
在工厂中添加一个以在每个容器创建和配置后进一步配置它。
@Bean
public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
...
factory.setContainerCustomizer(container -> { /* customize the container */ });
return factory;
}
4.1.10。线程安全
使用并发消息侦听器容器时,会在所有消费者线程上调用单个侦听器实例。因此,监听器需要是线程安全的,最好使用无状态监听器。如果无法使您的侦听器线程安全或添加同步会显着降低添加并发性的好处,您可以使用以下几种技术之一:
-
使用带有原型作用域bean 的
n
容器,以便每个容器都有自己的实例(使用 时这是不可能的)。concurrency=1
MessageListener
@KafkaListener
-
ThreadLocal<?>
在实例中保留状态。 -
将单例侦听器委托给在
SimpleThreadScope
(或类似范围)中声明的 bean。
为了便于清理线程状态(针对前面列表中的第二项和第三项),从 2.2 版本开始,监听器容器ConsumerStoppedEvent
会在每个线程退出时发布一个。您可以使用ApplicationListener
or@EventListener
方法使用这些事件,以从范围中删除ThreadLocal<?>
实例或remove()
线程范围的 bean。请注意,SimpleThreadScope
不会销毁具有销毁接口(例如DisposableBean
)的 bean,因此您应该destroy()
自己创建实例。
默认情况下,应用程序上下文的事件多播器在调用线程上调用事件侦听器。如果您将多播器更改为使用异步执行器,则线程清理无效。 |
4.1.11。监控
监控监听器性能
从 2.3 版开始Timer
,如果在类路径上检测到,并且在应用程序上下文中存在Micrometer
单个,则侦听器容器将自动为侦听器创建和更新 Micrometer。MeterRegistry
可以通过将 设置为ContainerProperty
micrometerEnabled
来禁用计时器false
。
维护了两个计时器 - 一个用于成功调用侦听器,一个用于失败。
计时器被命名spring.kafka.listener
并具有以下标签:
-
name
:(容器 bean 名称) -
result
:success
或failure
-
exception
:none
或ListenerExecutionFailedException
ContainerProperties
micrometerTags
您可以使用该属性添加其他标签。
使用并发容器,将为每个线程创建计时器,并且标签以where n is toname 为后缀。
-n 0 concurrency-1 |
监控 KafkaTemplate 性能
从版本 2.5 开始,如果在类路径上检测到,并且应用程序上下文中存在单个,则模板将自动创建和更新 MicrometerTimer
以进行发送操作。可以通过将模板的属性设置为 来禁用计时器。Micrometer
MeterRegistry
micrometerEnabled
false
维护了两个计时器 - 一个用于成功调用侦听器,一个用于失败。
计时器被命名spring.kafka.template
并具有以下标签:
-
name
: (模板 bean 名称) -
result
:success
或failure
-
exception
:none
或失败的异常类名
micrometerTags
您可以使用模板的属性添加其他标签。
千分尺原生指标
从 2.5 版开始,该框架提供Factory ListenersKafkaClientMetrics
来在创建和关闭生产者和消费者时管理 Micrometer实例。
要启用此功能,只需将侦听器添加到您的生产者和消费者工厂:
@Bean
public ConsumerFactory<String, String> myConsumerFactory() {
Map<String, Object> configs = consumerConfigs();
...
DefaultKafkaConsumerFactory<String, String> cf = new DefaultKafkaConsumerFactory<>(configs);
...
cf.addListener(new MicrometerConsumerListener<String, String>(meterRegistry(),
Collections.singletonList(new ImmutableTag("customTag", "customTagValue"))));
...
return cf;
}
@Bean
public ProducerFactory<String, String> myProducerFactory() {
Map<String, Object> configs = producerConfigs();
configs.put(ProducerConfig.CLIENT_ID_CONFIG, "myClientId");
...
DefaultKafkaProducerFactory<String, String> pf = new DefaultKafkaProducerFactory<>(configs);
...
pf.addListener(new MicrometerProducerListener<String, String>(meterRegistry(),
Collections.singletonList(new ImmutableTag("customTag", "customTagValue"))));
...
return pf;
}
传递给侦听器的消费者/生产者id
被添加到带有标签名称的计量器标签中spring.id
。
double count = this.meterRegistry.get("kafka.producer.node.incoming.byte.total")
.tag("customTag", "customTagValue")
.tag("spring.id", "myProducerFactory.myClientId-1")
.functionCounter()
.count()
StreamsBuilderFactoryBean
为- 参见KafkaStreams Micrometer Support提供了一个类似的侦听器。
4.1.12。交易
本节介绍 Spring for Apache Kafka 如何支持事务。
概述
0.11.0.0 客户端库增加了对事务的支持。Spring for Apache Kafka 通过以下方式添加支持:
-
KafkaTransactionManager
:与正常的 Spring 事务支持(@Transactional
等TransactionTemplate
)一起使用。 -
事务性的
KafkaMessageListenerContainer
-
与本地交易
KafkaTemplate
-
与其他事务管理器的事务同步
通过DefaultKafkaProducerFactory
提供transactionIdPrefix
. 在这种情况下,工厂不会管理单个 shared ,而是Producer
维护事务生产者的缓存。当用户调用close()
生产者时,它会返回到缓存中以供重用,而不是实际关闭。每个生产者的transactional.id
属性是transactionIdPrefix
+ n
,其中以每个新生产者n
开头0
并递增,除非事务由具有基于记录的侦听器的侦听器容器启动。在这种情况下,transactional.id
是<transactionIdPrefix>.<group.id>.<topic>.<partition>
。这是为了正确支持击剑僵尸,如此处所述. 此新行为是在版本 1.3.7、2.0.6、2.1.10 和 2.2.0 中添加的。如果您希望恢复到以前的行为,可以将producerPerConsumerPartition
属性设置DefaultKafkaProducerFactory
为false
。
虽然批处理侦听器支持事务,但默认情况下不支持僵尸防护,因为批处理可能包含来自多个主题或分区的记录。但是,从版本 2.3.2 开始,如果将容器属性设置subBatchPerPartition 为 true,则支持僵尸防护。在这种情况下,从上次轮询接收到的每个分区都会调用一次批处理侦听器,就好像每个轮询仅返回单个分区的记录一样。这是true 从 2.5 版开始的默认情况下,使用EOSMode.ALPHA ;启用事务时。false 如果您正在使用事务但不关心僵尸围栏,请将其设置为。另请参阅Exactly Once Semantics。
|
另请参阅transactionIdPrefix
。
使用 Spring Boot,只需要设置spring.kafka.producer.transaction-id-prefix
属性 - Boot 会自动配置一个KafkaTransactionManager
bean 并将其连接到侦听器容器中。
从版本 2.5.8 开始,您现在可以maxAge 在生产者工厂上配置属性。这在使用可能为代理闲置的事务生产者时很有用transactional.id.expiration.ms 。使用 current kafka-clients ,这可能会导致 aProducerFencedException 没有重新平衡。通过将 设置maxAge 为 less than transactional.id.expiration.ms ,如果生产者超过了它的最大年龄,工厂将刷新它。
|
使用KafkaTransactionManager
这KafkaTransactionManager
是 Spring Framework 的PlatformTransactionManager
. 它在其构造函数中提供了对生产者工厂的引用。如果您提供自定义生产者工厂,它必须支持事务。见ProducerFactory.transactionCapable()
。
您可以将KafkaTransactionManager
与正常的 Spring 事务支持一起使用(@Transactional
、TransactionTemplate
和其他)。如果事务处于活动状态,KafkaTemplate
则在事务范围内执行的任何操作都使用事务的Producer
. 管理器根据成功或失败提交或回滚事务。您必须将其配置KafkaTemplate
为使用与ProducerFactory
事务管理器相同的内容。
事务同步
本节涉及仅生产者事务(不是由侦听器容器启动的事务);有关在容器启动事务时链接事务的信息,请参阅使用消费者发起的事务。
如果你想将记录发送到 kafka 并执行一些数据库更新,你可以使用普通的 Spring 事务管理,比如DataSourceTransactionManager
.
@Transactional
public void process(List<Thing> things) {
things.forEach(thing -> this.kafkaTemplate.send("topic", thing));
updateDb(things);
}
@Transactional
注释的拦截器启动事务,KafkaTemplate
并将事务与该事务管理器同步;每个发送都将参与该事务。当方法退出时,数据库事务将提交,然后是 Kafka 事务。如果您希望以相反的顺序执行提交(首先是 Kafka),请使用嵌套@Transactional
方法,外部方法配置为使用DataSourceTransactionManager
.,内部方法配置为使用KafkaTransactionManager
.
有关在 Kafka-first 或 DB-first 配置中同步 JDBC 和 Kafka 事务的应用程序示例,请参阅Kafka Transactions 与其他事务管理器的示例。
从版本 2.5.17、2.6.12、2.7.9 和 2.8.0 开始,如果同步事务的提交失败(在主事务提交之后),将向调用者抛出异常。以前,这被默默地忽略(在调试时记录)。如有必要,应用程序应采取补救措施,以补偿已提交的主要事务。 |
使用消费者发起的交易
从ChainedKafkaTransactionManager
2.7 版开始,现在已弃用;ChainedTransactionManager
有关更多信息,请参阅其超类的 javadocs 。相反,KafkaTransactionManager
在容器中使用 a 来启动 Kafka 事务,并用注释监听器方法@Transactional
来启动另一个事务。
有关链接 JDBC 和 Kafka 事务的示例应用程序,请参阅使用其他事务管理器的 Kafka 事务示例。
KafkaTemplate
本地交易
您可以使用KafkaTemplate
来在本地事务中执行一系列操作。以下示例显示了如何执行此操作:
boolean result = template.executeInTransaction(t -> {
t.sendDefault("thing1", "thing2");
t.sendDefault("cat", "hat");
return true;
});
回调中的参数是模板本身 ( this
)。如果回调正常退出,则事务被提交。如果抛出异常,事务将回滚。
如果正在KafkaTransactionManager 处理(或同步)事务,则不使用它。相反,使用了一个新的“嵌套”事务。
|
transactionIdPrefix
如概述中所述,生产者工厂配置了此属性以构建生产者transactional.id
属性。指定此属性时存在二分法,即当使用 运行应用程序的多个实例时,EOSMode.ALPHA
在侦听器容器线程上生成记录时,所有实例上必须相同以满足围栏僵尸(也在概述中提到)。但是,当使用不是由侦听器容器启动的事务生成记录时,每个实例的前缀必须不同。2.3 版使配置更简单,尤其是在 Spring Boot 应用程序中。在以前的版本中,您必须创建两个生产者工厂和KafkaTemplate
kafkaTemplate.executeInTransaction()
s - 一种用于在侦听器容器线程上生成记录,另一种用于由方法上的事务拦截器启动或由其启动的独立事务@Transactional
。
现在,您可以覆盖工厂transactionalIdPrefix
的KafkaTemplate
和KafkaTransactionManager
。
当为侦听器容器使用事务管理器和模板时,您通常会将其保留为默认的生产者工厂的属性。在使用EOSMode.ALPHA
. EOSMode.BETA
不再需要使用相同的,transactional.id
即使是消费者发起的交易;事实上,它在每个实例上都必须是唯一的,就像生产者发起的事务一样。对于由模板(或 的事务管理器@Transaction
)启动的事务,您应该分别在模板和事务管理器上设置属性。此属性在每个应用程序实例上必须具有不同的值。
使用时(代理版本> = 2.5)transactional.id
已消除此问题(不同的规则);EOSMode.BETA
请参阅Exactly Once 语义。
KafkaTemplate
事务性和非事务性发布
通常,当 aKafkaTemplate
是事务性的(配置了具有事务能力的生产者工厂)时,需要事务。当配置了. TransactionTemplate
_ 任何在事务范围之外使用模板的尝试都会导致模板抛出. 从版本 2.4.3 开始,您可以将模板的属性设置为. 在这种情况下,模板将允许操作在没有事务的情况下运行,方法是调用's方法;生产者将像往常一样被缓存或线程绑定以供重用。请参阅使用。@Transactional
executeInTransaction
KafkaTransactionManager
IllegalStateException
allowNonTransactional
true
ProducerFactory
createNonTransactionalProducer()
DefaultKafkaProducerFactory
与批处理侦听器的事务
当正在使用事务时侦听器失败时,AfterRollbackProcessor
会在回滚发生后调用 以采取一些行动。当使用AfterRollbackProcessor
记录侦听器的默认值时,将执行搜索以便重新传递失败的记录。但是,使用批处理侦听器,将重新传递整个批处理,因为框架不知道批处理中的哪条记录失败。有关详细信息,请参阅回滚后处理器。
使用批处理侦听器时,2.4.2 版引入了一种替代机制来处理批处理时的故障;BatchToRecordAdapter
. _ 当batchListener
设置为 true 的容器工厂配置为 时BatchToRecordAdapter
,一次调用一个记录的侦听器。这可以在批处理中进行错误处理,同时仍然可以停止处理整个批处理,具体取决于异常类型。提供了一个默认值BatchToRecordAdapter
,可以使用标准配置,ConsumerRecordRecoverer
例如DeadLetterPublishingRecoverer
. 以下测试用例配置片段说明了如何使用此功能:
public static class TestListener {
final List<String> 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<String, String> 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
操作都参与事务。如果侦听器成功处理了记录(或多个记录,使用 a 时),则容器在事务管理器提交事务之前BatchMessageListener
使用 ) 将偏移量发送到事务。producer.sendOffsetsToTransaction()
如果侦听器抛出异常,事务将回滚并重新定位消费者,以便可以在下一次轮询时检索回滚记录。有关更多信息和处理反复失败的记录,请参阅回滚后处理器。
使用事务启用 Exactly Once Semantics (EOS)。
这意味着,对于一个read→process-write
序列,可以保证该序列恰好完成一次。(读取和处理至少有一次语义)。
Spring for Apache Kafka 2.5 及更高版本支持两种 EOS 模式:
-
ALPHA
- 别名V1
(已弃用) -
BETA
- 别名V2
(已弃用) -
V1
- 又名transactional.id
围栏(自版本 0.11.0.0 起) -
V2
- 又名 fetch-offset-request fencing (从 2.5 版开始)
使用 mode V1
,如果启动另一个具有相同的实例,则生产者将被“隔离” transactional.id
。Spring 通过使用Producer
for each来管理它group.id/topic/partition
;当发生重新平衡时,新实例将使用相同transactional.id
的实例,并且旧的生产者被隔离。
使用 mode V2
,不必为每个生产者指定一个生产者,group.id/topic/partition
因为消费者元数据与事务的偏移量一起发送,并且代理可以确定生产者是否使用该信息被隔离。
从 2.6 版开始,默认EOSMode
值为V2
.
要将容器配置为使用 mode ALPHA
,请将容器属性设置EOSMode
为ALPHA
, 以恢复之前的行为。
使用V2 (默认),您的代理必须是 2.5 或更高版本;kafka-clients 3.0 版,生产者将不再回退到V1 ; 如果代理不支持V2 ,则抛出异常。如果您的代理早于 2.5,则必须设置EOSMode 为V1 ,将DefaultKafkaProducerFactory producerPerConsumerPartition 设置保留为true ,如果您使用的是批处理侦听器,则应设置subBatchPerPartition 为true 。
|
当你的 broker 升级到 2.5 或更高版本时,你应该将模式切换为V2
,但生产者的数量将保持不变。然后,您可以使用producerPerConsumerPartition
set 对应用程序进行滚动升级false
以减少生产者的数量;您也不应该再设置subBatchPerPartition
容器属性。
如果您的经纪人已经是 2.5 或更高版本,您应该将该DefaultKafkaProducerFactory
producerPerConsumerPartition
属性设置为false
,以减少所需的生产者数量。
与 一起使用EOSMode.V2 时producerPerConsumerPartition=false ,transactional.id 必须在所有应用程序实例中都是唯一的。
|
使用V2
模式时,不再需要设置subBatchPerPartition
为true
; 它将默认false
为EOSMode
is V2
。
有关详细信息,请参阅KIP-447。
V1
和V2
以前是ALPHA
和BETA
; 它们已被更改以使框架与KIP-732保持一致。
4.1.14。将 Spring Bean 连接到生产者/消费者拦截器中
Apache Kafka 提供了一种向生产者和消费者添加拦截器的机制。这些对象由 Kafka 管理,而不是 Spring,因此正常的 Spring 依赖注入对于在依赖 Spring Bean 中的连接不起作用。config()
但是,您可以使用拦截器方法手动连接这些依赖项。下面的 Spring Boot 应用程序展示了如何通过覆盖 boot 的默认工厂来将一些依赖 bean 添加到配置属性中来做到这一点。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public ConsumerFactory<?, ?> kafkaConsumerFactory(SomeBean someBean) {
Map<String, Object> 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<String, Object> producerProperties = new HashMap<>();
// producerProperties.put(..., ...)
// ...
Map<String, Object> 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<String, String> 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<String, String> {
private SomeBean bean;
@Override
public void configure(Map<String, ?> configs) {
this.bean = (SomeBean) configs.get("some.bean");
}
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> 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<String, String> {
private SomeBean bean;
@Override
public void configure(Map<String, ?> configs) {
this.bean = (SomeBean) configs.get("some.bean");
}
@Override
public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
this.bean.someMethod("consumer interceptor");
return records;
}
@Override
public void onCommit(Map<TopicPartition, OffsetAndMetadata> 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()
以前,您可以在 a 中暂停消费者ConsumerAwareMessageListener
并通过侦听 a 来恢复它ListenerContainerIdleEvent
,这提供了对Consumer
对象的访问。虽然您可以使用事件侦听器在空闲容器中暂停消费者,但在某些情况下,这不是线程安全的,因为不能保证在消费者线程上调用事件侦听器。为了安全地暂停和恢复消费者,您应该在侦听器容器上使用pause
和方法。resume
Apause()
在下一个之前生效poll()
;a在当前返回resume()
后立即生效。poll()
当一个容器暂停时,它会继续poll()
消费者,如果正在使用组管理,则避免重新平衡,但它不会检索任何记录。有关更多信息,请参阅 Kafka 文档。
从 2.1.5 版本开始,可以调用isPauseRequested()
查看是否pause()
已调用。但是,消费者可能还没有真正暂停。
如果所有实例实际上都已暂停,isConsumerPaused()
则返回 true 。Consumer
此外(也是从 2.1.5 开始),ConsumerPausedEvent
并且ConsumerResumedEvent
实例以容器作为source
属性发布,并且TopicPartition
实例包含在该partitions
属性中。
以下简单的 Spring Boot 应用程序演示了使用容器注册表获取对@KafkaListener
方法容器的引用并暂停或恢复其使用者以及接收相应的事件:
@SpringBootApplication
public class Application implements ApplicationListener<KafkaEvent> {
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<String, String> 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)
and方法暂停和恢复分配给该使用者的特定分区的使用。暂停和恢复分别发生在与和方法类似的resumePartition(TopicPartition topicPartition)
前后。如果已请求该分区的暂停,则该方法返回 true。如果该分区已有效暂停,则该方法返回 true。poll()
pause()
resume()
isPartitionPauseRequested()
isPartitionPaused()
此外,从 2.7 版开始ConsumerPartitionPausedEvent
,ConsumerPartitionResumedEvent
实例与容器一起作为source
属性和TopicPartition
实例发布。
4.1.17。序列化、反序列化和消息转换
概述
Apache Kafka 提供了用于序列化和反序列化记录值及其键的高级 API。它与一些内置实现的org.apache.kafka.common.serialization.Serializer<T>
和
抽象一起出现。org.apache.kafka.common.serialization.Deserializer<T>
同时,我们可以通过使用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
(and, 因此, ) 提供了重载的构造函数来分别KafkaProducer
接受Serializer
和Deserializer
实例。keys
values
当您使用此 API 时,DefaultKafkaProducerFactory
andDefaultKafkaConsumerFactory
还提供属性(通过构造函数或 setter 方法)将自定义Serializer
和Deserializer
实例注入目标Producer
或Consumer
. 此外,您可以通过构造函数传入Supplier<Serializer>
orSupplier<Deserializer>
实例 - 这些Supplier
s 在创建每个Producer
or时被调用Consumer
。
字符串序列化
从 2.5 版开始,Spring for Apache Kafka 提供了使用字符串表示实体的类ToStringSerializer
。ParseStringDeserializer
它们依赖方法toString
和一些Function<String>
或BiFunction<String, Headers>
解析字符串并填充实例的属性。通常,这会调用类上的一些静态方法,例如parse
:
ToStringSerializer<Thing> thingSerializer = new ToStringSerializer<>();
//...
ParseStringDeserializer<Thing> deserializer = new ParseStringDeserializer<>(Thing::parse);
默认情况下,ToStringSerializer
配置为在记录中传达有关序列化实体的类型信息Headers
。addTypeInfo
您可以通过将该属性设置为 false来禁用此功能。ParseStringDeserializer
接收方可以使用此信息。
-
ToStringSerializer.ADD_TYPE_INFO_HEADERS
(默认true
):您可以将其设置false
为禁用此功能ToStringSerializer
(设置addTypeInfo
属性)。
ParseStringDeserializer<Object> 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
}
});
您可以配置Charset
用于转换String
为/从byte[]
默认为UTF-8
.
ConsumerConfig
您可以使用属性使用解析器方法的名称配置反序列化器:
-
ParseStringDeserializer.KEY_PARSER
-
ParseStringDeserializer.VALUE_PARSER
属性必须包含类的完全限定名称,后跟方法名称,用句点分隔.
。该方法必须是静态的,并且具有一个(String, Headers)
或的签名(String)
。
还提供了A ToFromStringSerde
,用于 Kafka Streams。
JSON
Spring for Apache Kafka 还提供了基于 Jackson JSON 对象映射器的实现JsonSerializer
。JsonDeserializer
允许将JsonSerializer
任何 Java 对象编写为 JSON byte[]
。这JsonDeserializer
需要一个额外的Class<?> targetType
参数来允许将消费对象反序列化为byte[]
正确的目标对象。下面的例子展示了如何创建一个JsonDeserializer
:
JsonDeserializer<Thing> thingDeserializer = new JsonDeserializer<>(Thing.class);
您可以自定义JsonSerializer
和JsonDeserializer
使用ObjectMapper
. 您还可以扩展它们以在configure(Map<String, ?> configs, boolean isKey)
方法中实现一些特定的配置逻辑。
从 2.3 版开始,所有支持 JSON 的组件都默认配置有一个JacksonUtils.enhancedObjectMapper()
实例,该实例禁用了MapperFeature.DEFAULT_VIEW_INCLUSION
和功能。DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
此外,此类实例还提供了用于自定义数据类型的众所周知的模块,例如 Java 时间和 Kotlin 支持。有关更多信息,请参阅JacksonUtils.enhancedObjectMapper()
JavaDocs。此方法还将对象序列化注册org.springframework.kafka.support.JacksonMimeTypeModule
到org.springframework.util.MimeType
纯字符串中,以便通过网络实现跨平台兼容性。AJacksonMimeTypeModule
可以在应用程序上下文中注册为 bean,并将自动配置到Spring BootObjectMapper
实例中。
同样从 2.3 版开始,JsonDeserializer
提供了TypeReference
基于构造函数的构造函数,用于更好地处理目标通用容器类型。
从 2.1 版本开始,您可以在记录中传达类型信息Headers
,从而允许处理多种类型。此外,您可以使用以下 Kafka 属性配置序列化器和反序列化器。如果您分别为和提供Serializer
和Deserializer
实例,它们将无效。KafkaConsumer
KafkaProducer
配置属性
-
JsonSerializer.ADD_TYPE_INFO_HEADERS
(默认true
):您可以将其设置false
为禁用此功能JsonSerializer
(设置addTypeInfo
属性)。 -
JsonSerializer.TYPE_MAPPINGS
(默认empty
):见映射类型。 -
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
):见映射类型。 -
JsonDeserializer.KEY_TYPE_METHOD
(默认empty
):请参阅使用方法确定类型。 -
JsonDeserializer.VALUE_TYPE_METHOD
(默认empty
):请参阅使用方法确定类型。
从 2.2 版开始,类型信息标头(如果由序列化程序添加)被反序列化程序删除。您可以通过直接在反序列化器上或使用前面描述的配置属性将removeTypeHeaders
属性设置为 来恢复到以前的行为。false
从 2.8 版本开始,如果您以编程方式构建序列化器或反序列化器,如Programmatic Construction所示,只要您没有显式设置任何属性(使用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 Bootapplication.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
您只能使用属性执行简单的配置。对于更高级的配置(例如
还提供了 Setter,作为使用这些构造函数的替代方法。 |
useHeadersIfPresent
从版本 2.2 开始,您可以使用具有布尔值(true
默认情况下)的重载构造函数之一显式配置反序列化器以使用提供的目标类型并忽略标头中的类型信息。以下示例显示了如何执行此操作:
DefaultKafkaConsumerFactory<Integer, Cat1> 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<Object> deser = new JsonDeserializer<>()
.trustedPackages("*")
.typeResolver(SomeClass::thing1Thing2JavaTypeForTopic);
...
public static JavaType thing1Thing2JavaTypeForTopic(String topic, byte[] data, Headers headers) {
...
}
程序化建设
在以编程方式构建序列化器/反序列化器以供生产者/消费者工厂使用时,从 2.3 版本开始,您可以使用 fluent API,它简化了配置。
@Bean
public ProducerFactory<MyKeyType, MyValueType> pf() {
Map<String, Object> props = new HashMap<>();
// props.put(..., ...)
// ...
DefaultKafkaProducerFactory<MyKeyType, MyValueType> pf = new DefaultKafkaProducerFactory<>(props,
new JsonSerializer<MyKeyType>()
.forKeys()
.noTypeInfo(),
new JsonSerializer<MyValueType>()
.noTypeInfo());
return pf;
}
@Bean
public ConsumerFactory<MyKeyType, MyValueType> cf() {
Map<String, Object> props = new HashMap<>();
// props.put(..., ...)
// ...
DefaultKafkaConsumerFactory<MyKeyType, MyValueType> cf = new DefaultKafkaConsumerFactory<>(props,
new JsonDeserializer<>(MyKeyType.class)
.forKeys()
.ignoreTypeHeaders(),
new JsonDeserializer<>(MyValueType.class)
.ignoreTypeHeaders());
return cf;
}
要以编程方式提供类型映射,类似于使用方法确定类型,请使用该typeFunction
属性。
JsonDeserializer<Object> deser = new JsonDeserializer<>()
.trustedPackages("*")
.typeFunction(MyUtils::thingOneOrThingTwo);
或者,只要你不使用 fluent API 来配置属性,或者使用set*()
方法设置它们,工厂将使用配置属性来配置序列化器/反序列化器;请参阅配置属性。
委派序列化器和反序列化器
使用标题
2.3 版引入了DelegatingSerializer
and DelegatingDeserializer
,它允许生成和使用具有不同键和/或值类型的记录。生产者必须将一个标头设置DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR
为一个选择器值,用于选择哪个序列化器用于值和DelegatingSerializer.KEY_SERIALIZATION_SELECTOR
键;如果未找到匹配项,IllegalStateException
则抛出 an。
对于传入的记录,反序列化器使用相同的标头来选择要使用的反序列化器;如果未找到匹配项或标头不存在,byte[]
则返回原始数据。
您可以通过构造函数将选择器映射配置为Serializer
/ Deserializer
,也可以通过 Kafka 生产者/消费者属性使用键DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG
和DelegatingSerializer.KEY_SERIALIZATION_SELECTOR_CONFIG
. 对于序列化程序,生产者属性可以是一个Map<String, Object>
,其中键是选择器,值是Serializer
实例、序列化程序Class
或类名。该属性也可以是逗号分隔的映射条目字符串,如下所示。
对于反序列化器,消费者属性可以是一个Map<String, Object>
,其中键是选择器,值是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
。
该技术支持向同一主题(或不同主题)发送不同类型。
Serdes 从版本 2.5.1 开始,如果类型(键或值)是( Long ,Integer 等)
支持的标准类型之一,则无需设置选择器标头。相反,序列化程序会将标头设置为类型的类名。不需要为这些类型配置序列化器或反序列化器,它们将被动态创建(一次)。
|
有关将不同类型发送到不同主题的另一种技术,请参阅使用RoutingKafkaTemplate
.
按类型
2.8 版引入了DelegatingByTypeSerializer
.
@Bean
public ProducerFactory<Integer, Object> producerFactory(Map<String, Object> 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
,例如 a 。LinkedHashMap
按主题
从 2.8 版开始,DelegatingByTopicSerializer
允许DelegatingByTopicDeserializer
根据主题名称选择序列化器/反序列化器。RegexPattern
用于查找要使用的实例。可以使用构造函数或通过属性(逗号分隔的列表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<Integer, Object> producerFactory(Map<String, Object> 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<Object>()); // default
}
DelegatingByTopicSerialization.KEY_SERIALIZATION_TOPIC_DEFAULT
您可以使用and指定在没有模式匹配时使用的默认序列化器/反序列化器DelegatingByTopicSerialization.VALUE_SERIALIZATION_TOPIC_DEFAULT
。
一个附加属性DelegatingByTopicSerialization.CASE_SENSITIVE
(默认true
),当设置为false
使主题查找不区分大小写时。
重试反序列化器
当委托在反序列化过程中可能出现瞬时错误(例如网络问题)时,RetryingDeserializer
使用委托Deserializer
并重RetryTemplate
试反序列化。
ConsumerFactory cf = new DefaultKafkaConsumerFactory(myConsumerConfigs,
new RetryingDeserializer(myUnreliableKeyDeserializer, retryTemplate),
new RetryingDeserializer(myUnreliableValueDeserializer, retryTemplate));
重试策略、回退策略等的配置请参考spring-retry项目。RetryTemplate
Spring Messaging 消息转换
尽管从低级 Kafka和角度来看, Serializer
and API 非常简单和灵活,但在使用Spring Integration 的 Apache Kafka Support时,您可能需要在 Spring Messaging 级别具有更大的灵活性。为了让您轻松地与 Kafka 相互转换,Spring for Apache Kafka 提供了具有实现及其(和子类)定制的抽象。您可以直接将其注入实例,也可以使用属性的bean 定义。以下示例显示了如何执行此操作:Deserializer
Consumer
Producer
@KafkaListener
org.springframework.messaging.Message
MessageConverter
MessagingMessageConverter
JsonMessageConverter
MessageConverter
KafkaTemplate
AbstractKafkaListenerContainerFactory
@KafkaListener.containerFactory()
@Bean
public KafkaListenerContainerFactory<?> kafkaJsonListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setMessageConverter(new JsonMessageConverter());
return factory;
}
...
@KafkaListener(topics = "jsonData",
containerFactory = "kafkaJsonListenerContainerFactory")
public void jsonListener(Cat cat) {
...
}
使用 Spring Boot 时,只需将转换器定义为 a @Bean
,Spring Boot 自动配置会将其连接到自动配置的模板和容器工厂。
当您使用 a@KafkaListener
时,参数类型被提供给消息转换器以协助转换。
|
在消费者方面,您可以配置一个 在生产者端,当您使用 Spring Integration 或
同样,使用 为方便起见,从 2.3 版开始,该框架还提供了一个 |
从版本 2.7.1 开始,可以将消息负载转换委托给spring-messaging
SmartMessageConverter
; 例如,这使得转换可以基于MessageHeaders.CONTENT_TYPE
标题。
调用该KafkaMessageConverter.fromMessage() 方法以使用属性ProducerRecord 中的消息有效负载进行出站转换ProducerRecord.value() 。调用该KafkaMessageConverter.toMessage() 方法进行入站转换,ConsumerRecord 有效负载是ConsumerRecord.value() 属性。调用该SmartMessageConverter.toMessage() 方法以Message<?> 从Message 传递的 to`fromMessage()`(通常通过KafkaTemplate.send(Message<?> msg) )创建一个新的出站。类似地,在该KafkaMessageConverter.toMessage() 方法中,转换器从 中创建一个新Message<?> 的之后ConsumerRecord ,调用该SmartMessageConverter.fromMessage() 方法,然后使用新转换的有效负载创建最终的入站消息。在任何一种情况下,如果SmartMessageConverter 返回null ,则使用原始消息。
|
当在KafkaTemplate
监听器容器工厂中使用默认转换器时,您可以SmartMessageConverter
通过调用setMessagingConverter()
模板和方法上的contentMessageConverter
属性来配置@KafkaListener
。
例子:
template.setMessagingConverter(mySmartConverter);
@KafkaListener(id = "withSmartConverter", topics = "someTopic",
contentTypeConverter = "mySmartConverter")
public void smart(Thing thing) {
...
}
使用 Spring 数据投影接口
从版本 2.1.1 开始,您可以将 JSON 转换为 Spring Data Projection 接口而不是具体类型。这允许对数据进行非常选择性和低耦合的绑定,包括从 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 文档中将属性名称作为字段查找。该@JsonPath
表达式允许自定义值查找,甚至可以定义多个 JSON 路径表达式,从多个位置查找值,直到表达式返回实际值。
要启用此功能,请使用ProjectingMessageConverter
配置了适当的委托转换器(用于出站转换和转换非投影接口)。您还必须将spring-data:spring-data-commons
和添加com.jayway.jsonpath:json-path
到类路径。
当用作方法的参数时@KafkaListener
,接口类型会像往常一样自动传递给转换器。
使用ErrorHandlingDeserializer
当反序列化器无法反序列化消息时,Spring 无法处理该问题,因为它发生在poll()
返回之前。为了解决这个问题,ErrorHandlingDeserializer
引入了。这个反序列化器委托给一个真正的反序列化器(键或值)。如果委托未能反序列化记录内容,则在包含原因和原始字节的标头中ErrorHandlingDeserializer
返回一个null
值和 a 。DeserializationException
当您使用记录级别MessageListener
时,如果ConsumerRecord
包含DeserializationException
键或值的标头,ErrorHandler
则使用 failed 调用容器ConsumerRecord
。记录不会传递给侦听器。
或者,您可以配置ErrorHandlingDeserializer
以通过提供 a 来创建自定义值failedDeserializationFunction
,即Function<FailedDeserializationInfo, T>
. 调用此函数以创建 的实例T
,该实例以通常的方式传递给侦听器。FailedDeserializationInfo
一个包含所有上下文信息的类型的对象被提供给函数。您可以DeserializationException
在标头中找到(作为序列化的 Java 对象)。有关更多信息,请参阅Javadoc 。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<FailedDeserializationInfo, Foo> {
@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 该序列化程序可以处理正常对象以及byte[] 由反序列化异常导致的原始值。模板的通用值类型应该是Object . 一种技术是使用DelegatingByTypeSerializer ; 一个例子如下:
|
@Bean
public ProducerFactory<String, Object> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(),
new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(),
MyNormalObject.class, new JsonSerializer<Object>())));
}
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
使用ErrorHandlingDeserializer
批处理侦听器时,您必须检查消息头中的反序列化异常。当与 a 一起使用时DefaultBatchErrorHandler
,您可以使用该标头来确定异常失败的记录,并通过 a 与错误处理程序通信BatchListenerFailedException
。
@KafkaListener(id = "test", topics = "test")
void listen(List<Thing> in, @Header(KafkaHeaders.BATCH_CONVERTED_HEADERS) List<Map<String, Object>> 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<ConsumerRecord<?, ?>
,ListenerUtils.getExceptionFromHeader()
使用 , 代替:
@KafkaListener(id = "kgh2036", topics = "kgh2036")
void listen(List<ConsumerRecord<String, Thing>> in) {
for (int i = 0; i < in.size(); i++) {
ConsumerRecord<String, Thing> 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());
}
}
使用批处理侦听器进行有效负载转换
当您使用批处理侦听器容器工厂时,您还可以使用 a JsonMessageConverter
within a来转换批处理消息。BatchMessagingMessageConverter
有关更多信息,请参阅序列化、反序列化和消息转换和Spring Messaging 消息转换。
默认情况下,转换的类型是从侦听器参数中推断出来的。如果您使用设置为(而不是默认值)的JsonMessageConverter
aDefaultJackson2TypeMapper
进行配置,则转换器将使用标头中的类型信息(如果存在)。例如,这允许使用接口而不是具体类来声明侦听器方法。此外,类型转换器支持映射,因此反序列化可以是与源不同的类型(只要数据兼容)。当您使用必须已经转换有效负载以确定调用哪个方法的类级实例时,这也很有用。以下示例创建使用此方法的 bean:TypePrecedence
TYPE_ID
INFERRED
@KafkaListener
@Bean
public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> 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<Foo> foos, @Header(KafkaHeaders.OFFSET) List<Long> offsets) {
...
}
请注意,您仍然可以访问批次标题。
如果批处理转换器有支持它的记录转换器,您还可以收到消息列表,其中根据泛型类型转换有效负载。以下示例显示了如何执行此操作:
@KafkaListener(topics = "blc3", groupId = "blc3")
public void listen1(List<Message<Foo>> fooMessages) {
...
}
ConversionService
定制
从版本 2.1.1 开始,org.springframework.core.convert.ConversionService
默认情况下o.s.messaging.handler.annotation.support.MessageHandlerMethodFactory
用于解析调用侦听器方法的参数的所有 bean 都提供了实现以下任何接口的所有 bean:
-
org.springframework.core.convert.converter.Converter
-
org.springframework.core.convert.converter.GenericConverter
-
org.springframework.format.Formatter
这使您可以进一步自定义侦听器反序列化,而无需更改 和 的默认ConsumerFactory
配置KafkaListenerContainerFactory
。
通过bean
设置自定义MessageHandlerMethodFactory 会禁用此功能。KafkaListenerEndpointRegistrar KafkaListenerConfigurer |
添加自定义HandlerMethodArgumentResolver
到@KafkaListener
从版本 2.4.2 开始,您可以添加自己的HandlerMethodArgumentResolver
和解析自定义方法参数。您所需要的只是实现KafkaListenerConfigurer
和使用setCustomMethodArgumentResolvers()
类中的方法KafkaListenerEndpointRegistrar
。
@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)
);
}
}
);
}
}
您还可以通过向bean添加自定义MessageHandlerMethodFactory
来完全替换框架的参数解析。如果您这样做,并且您的应用程序需要使用 a (例如来自压缩主题)KafkaListenerEndpointRegistrar
来处理墓碑记录,您应该将 a 添加到工厂;它必须是最后一个解析器,因为它支持所有类型并且可以在没有注释的情况下匹配参数。如果您使用的是,请将此解析器设置为最后一个自定义解析器;工厂将确保在标准之前使用此解析器,标准不了解有效载荷。null
value()
KafkaNullAwarePayloadArgumentResolver
@Payload
DefaultMessageHandlerMethodFactory
PayloadMethodArgumentResolver
KafkaNull
4.1.18。消息头
0.11.0.0 客户端引入了对消息头的支持。从 2.0 版开始,Spring for Apache Kafka 现在支持将这些标头映射到spring-messaging
MessageHeaders
.
以前的版本映射ConsumerRecord 和ProducerRecord 到 spring-messaging Message<?> ,其中 value 属性映射到和从 thepayload 和其他属性(topic ,partition ,等等)被映射到标题。情况仍然如此,但现在可以映射附加(任意)标头。
|
Apache Kafka 标头有一个简单的 API,如以下接口定义所示:
public interface Header {
String key();
byte[] value();
}
该KafkaHeaderMapper
策略用于在 KafkaHeaders
和MessageHeaders
. 其接口定义如下:
public interface KafkaHeaderMapper {
void fromHeaders(MessageHeaders headers, Headers target);
void toHeaders(Headers source, Map<String, Object> target);
}
将DefaultKafkaHeaderMapper
键映射到MessageHeaders
标头名称,并且为了支持出站消息的丰富标头类型,执行 JSON 转换。“特殊”标头(带有 键spring_json_header_types
)包含 的 JSON 映射<key>:<type>
。此标头用于入站端,以将每个标头值适当地转换为原始类型。
在入站端,所有 KafkaHeader
实例都映射到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 | 使用默认 JacksonObjectMapper 并映射大多数标头,如示例前所述。 |
2 | 使用提供的 JacksonObjectMapper 并映射大多数标头,如示例前所述。 |
3 | 使用默认 JacksonObjectMapper 并根据提供的模式映射标题。 |
4 | 使用提供的 JacksonObjectMapper 并根据提供的模式映射标题。 |
模式相当简单,可以包含前导通配符 ( )、尾随通配符或两者兼有(例如,
.cat.*
)。您可以否定带有前导的模式!
。第一个匹配标头名称(无论是正数还是负数)的模式获胜。
当您提供自己的模式时,我们建议包含!id
and !timestamp
,因为这些标头在入站端是只读的。
默认情况下,映射器仅反序列化java.lang 和中的类java.util 。addTrustedPackages 您可以通过使用该方法添加受信任的包来信任其他(或所有)包。如果您从不受信任的来源收到消息,您可能希望仅添加您信任的那些包。要信任所有包,您可以使用mapper.addTrustedPackages("*") .
|
在与不知道映射器 JSON 格式的系统通信时,以原始形式映射String 标头值很有用。
|
从版本 2.2.5 开始,您可以指定某些字符串值标头不应使用 JSON 映射,而是映射到/来自 raw byte[]
。有新的AbstractKafkaHeaderMapper
属性;mapAllStringsOut
设置为 true 时,所有字符串值的标头都将转换为byte[]
使用该charset
属性(默认UTF-8
)。此外,还有一个属性rawMappedHeaders
,它是 ; 的映射header name : boolean
。如果映射包含标头名称,并且标头包含值,则将使用字符集String
将其映射为原始数据。当且仅当映射值中的布尔值为 时,byte[]
此映射还用于将原始传入byte[]
标头映射到使用字符集。如果布尔值是,或者标题名称不在地图中String
true
false
true
值,传入的标头被简单地映射为原始未映射的标头。
下面的测试用例说明了这种机制。
@Test
public void testSpecificStringConvert() {
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
Map<String, Boolean> rawMappedHeaders = new HashMap<>();
rawMappedHeaders.put("thisOnesAString", true);
rawMappedHeaders.put("thisOnesBytes", false);
mapper.setRawMappedHeaders(rawMappedHeaders);
Map<String, Object> 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()));
}
默认情况下,只要 Jackson 在类路径上,DefaultKafkaHeaderMapper
就会在MessagingMessageConverter
and中使用。BatchMessagingMessageConverter
使用批量转换器,转换后的标头可在列表中的KafkaHeaders.BATCH_CONVERTED_HEADERS
某个List<Map<String, Object>>
位置的映射对应于有效负载中的数据位置。
如果没有转换器(因为 Jackson 不存在或明确设置为null
),则来自消费者记录的标头在KafkaHeaders.NATIVE_HEADERS
标头中提供未转换的。此标头是一个Headers
对象(或List<Headers>
在批处理转换器的情况下为 a),其中列表中的位置对应于有效负载中的数据位置)。
某些类型不适合 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 Boot,它将自动将此转换器 bean 配置为 auto-configured KafkaTemplate
;否则您应该将此转换器添加到模板中。
4.1.19。'Tombstone' 记录的空负载和日志压缩
使用Log Compaction时,您可以发送和接收带有null
有效负载的消息,以识别密钥的删除。
您还可以null
出于其他原因接收值,例如在无法反序列化值时Deserializer
可能返回的值。null
要使用 发送null
有效负载KafkaTemplate
,您可以将 null 传递给send()
方法的 value 参数。一个例外是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
}
@KafkaListener
当您使用具有多个方法的类级别时@KafkaHandler
,需要进行一些额外的配置。具体来说,您需要一个@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
。
请参阅手动分配所有分区。 |
此功能需要使用KafkaNullAwarePayloadArgumentResolver 框架将在使用默认配置时配置的MessageHandlerMethodFactory . 使用自定义MessageHandlerMethodFactory 时,请参阅将自定义添加HandlerMethodArgumentResolver 到@KafkaListener .
|
4.1.20。处理异常
本节介绍如何处理使用 Spring for Apache Kafka 时可能出现的各种异常。
侦听器错误处理程序
从 2.0 版开始,@KafkaListener
注解有一个新属性:errorHandler
.
您可以使用errorHandler
来提供实现的 bean 名称KafkaListenerErrorHandler
。这个功能接口有一个方法,如下面的清单所示:
@FunctionalInterface
public interface KafkaListenerErrorHandler {
Object handleError(Message<?> message, ListenerExecutionFailedException exception) throws Exception;
}
您可以访问Message<?>
消息转换器生成的 spring-messaging 对象以及侦听器引发的异常,该异常被包装在ListenerExecutionFailedException
. 错误处理程序可以抛出原始异常或新异常,该异常被抛出到容器中。错误处理程序返回的任何内容都将被忽略。
从 2.7 版开始,您可以在 and 上设置属性,rawRecordHeader
这会导致将原始数据添加到标头中的转换后。这很有用,例如,如果您希望在侦听器错误处理程序中使用 a。它可能用于请求/回复场景中,您希望在重试几次后,在死信主题中捕获失败记录后,将失败结果发送给发件人。MessagingMessageConverter
BatchMessagingMessageConverter
ConsumerRecord
Message<?>
KafkaHeaders.RAW_DATA
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<String> topics = headers.get(KafkaHeaders.RECEIVED_TOPIC, List.class);
List<Integer> partitions = headers.get(KafkaHeaders.RECEIVED_PARTITION_ID, List.class);
List<Long> offsets = headers.get(KafkaHeaders.OFFSET, List.class);
Map<TopicPartition, Long> 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
提供了替换大多数遗留框架错误处理程序实现的实现,并且不推荐使用遗留错误处理程序。监听器容器和监听器容器工厂仍然支持遗留接口;它们将在未来的版本中被弃用。
有关将自定义错误处理程序迁移CommonErrorHandler
到CommonErrorHandler
.
使用事务时,默认情况下不配置错误处理程序,因此异常将回滚事务。事务容器的错误处理由AfterRollbackProcessor
. 如果您在使用事务时提供自定义错误处理程序,如果您希望事务回滚,它必须抛出异常。
该接口有一个默认方法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<ConcurrentMessageListenerContainer<Integer, String>>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
...
factory.setCommonErrorHandler(myErrorHandler);
...
return factory;
}
默认情况下,如果带注解的监听方法抛出异常,会抛出到容器中,消息按照容器配置进行处理。
容器在调用错误处理程序之前提交任何未决的偏移提交。
如果您使用的是 Spring Boot,您只需将错误处理程序添加为 a @Bean
,Boot 就会将其添加到自动配置的工厂中。
默认错误处理程序
这个新的错误处理程序取代了SeekToCurrentErrorHandler
and RecoveringBatchErrorHandler
,它们现在已经是多个版本的默认错误处理程序。一个区别是批处理侦听器的回退行为(当BatchListenerFailedException
抛出 a 以外的异常时)等效于Retrying Complete Batches。
错误处理程序可以恢复(跳过)一直失败的记录。默认情况下,十次失败后,记录失败的记录(在ERROR
级别)。BiConsumer
您可以使用自定义恢复器 ( ) 和BackOff
控制交付尝试和每个之间延迟的a 来配置处理程序。使用FixedBackOff
withFixedBackOff.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<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(AckMode.RECORD);
factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 2L)));
return factory;
}
对于记录侦听器,这将重试传送最多 2 次(3 次传送尝试),后退 1 秒,而不是默认配置 ( FixedBackOff(0L, 9)
)。在重试用尽后,仅记录失败。
举个例子; 如果poll
返回 6 条记录(每个分区 0、1、2 中的两条)并且侦听器在第四条记录上抛出异常,则容器通过提交它们的偏移量来确认前三条消息。DefaultErrorHandler
寻求分区 1 的偏移量 1 和分区 2 的偏移量 0。下一个返回poll()
三个未处理的记录。
如果AckMode
是BATCH
,容器在调用错误处理程序之前提交前两个分区的偏移量。
对于批处理侦听器,侦听器必须抛出一个BatchListenerFailedException
指示批处理中的哪些记录失败。
事件的顺序是:
-
提交索引前记录的偏移量。
-
如果重试未用尽,则执行搜索,以便重新传递所有剩余的记录(包括失败的记录)。
-
如果重试次数用尽,则尝试恢复失败的记录(仅默认日志)并执行搜索,以便重新传递剩余的记录(不包括失败的记录)。已恢复记录的偏移量已提交
-
如果重试已用尽并且恢复失败,则执行搜索,就好像重试未用尽一样。
默认恢复器在重试用尽后记录失败的记录。您可以使用自定义恢复器,也可以使用框架提供的恢复器,例如DeadLetterPublishingRecoverer
.
当使用 POJO 批处理侦听器(例如List<Thing>
),并且您没有完整的消费者记录要添加到异常中时,您可以只添加失败记录的索引:
@KafkaListener(id = "recovering", topics = "someTopic")
public void listen(List<Thing> 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
。
另请参阅发布死信记录。
使用事务时,类似的功能由DefaultAfterRollbackProcessor
. 请参阅回滚后处理器。
认为某些异常是致命的DefaultErrorHandler
,并且对此类异常跳过重试;在第一次失败时调用恢复器。默认情况下,被认为是致命的异常是:
-
DeserializationException
-
MessageConversionException
-
ConversionException
-
MethodArgumentResolutionException
-
NoSuchMethodException
-
ClassCastException
因为这些异常不太可能在重试交付时得到解决。
您可以在不可重试类别中添加更多异常类型,或者完全替换分类异常的映射。有关更多信息,请参阅 Javadocs,DefaultErrorHandler.addNotRetryableException()
以及.DefaultErrorHandler.setClassifications()
spring-retry
BinaryExceptionClassifier
这是一个添加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。
如果恢复器失败(抛出异常),失败的记录将包含在搜索中。如果恢复器失败,则BackOff 默认情况下将重置,并且在再次尝试恢复之前,重新交付将再次经历回退。要在恢复失败后跳过重试,请将错误处理程序设置resetStateOnRecoveryFailure 为false .
|
您可以根据失败的记录和/或异常为错误处理程序提供一个BiFunction<ConsumerRecord<?, ?>, Exception, BackOff>
来确定要使用的:BackOff
handler.setBackOffFunction((record, ex) -> { ... });
如果函数返回null
,将使用处理程序的默认值BackOff
。
如果异常类型在两次失败之间发生变化,则设置resetStateOnExceptionChange
为true
并重新启动重试序列(包括选择新的BackOff
,如果已配置)。默认情况下,不考虑异常类型。
另请参阅交付尝试标题。
批处理错误处理程序的转换错误
MessageConverter
从 2.8 版开始,批处理侦听器现在可以在将 a与 a ByteArrayDeserializer
、 aBytesDeserializer
或 aStringDeserializer
以及a 一起使用时正确处理转换错误DefaultErrorHandler
。当发生转换错误时,payload 被设置为 null 并且反序列化异常被添加到记录头中,类似于ErrorHandlingDeserializer
. 侦听器中提供了一个 s 列表,ConversionException
因此侦听器可以抛出一个BatchListenerFailedException
指示发生转换异常的第一个索引。
例子:
@KafkaListener(id = "test", topics = "topic")
void listen(List<Thing> in, @Header(KafkaHeaders.CONVERSION_FAILURES) List<ConversionException> 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
批处理侦听器的后备行为,其中侦听器抛出除 a 之外的异常BatchListenerFailedException
。
无法保证在重新交付批次时,该批次具有相同数量的记录和/或重新交付的记录的顺序相同。因此,不可能轻松地维护批处理的重试状态。采用FallbackBatchErrorHandler
以下方法。如果批处理侦听器抛出不是 a 的异常,BatchListenerFailedException
则从内存中的记录批处理执行重试。为了避免在扩展重试序列期间重新平衡,错误处理程序暂停消费者,在休眠之前轮询它以等待每次重试,然后再次调用侦听器。如果/当重试用尽时,ConsumerRecordRecoverer
为批处理中的每条记录调用。如果恢复器抛出异常,或者线程在睡眠期间被中断,那么这批记录将在下一次轮询时重新传递。在退出之前,无论结果如何,消费者都会被恢复。
此机制不能与事务一起使用。 |
在等待一个BackOff
间隔时,错误处理程序将循环短暂睡眠,直到达到所需的延迟,同时检查容器是否已停止,允许睡眠在 之后很快退出,stop()
而不是导致延迟。
容器停止错误处理程序
CommonContainerStoppingErrorHandler
如果侦听器抛出异常,则停止容器。对于记录侦听器,当AckMode
is时RECORD
,已提交已处理记录的偏移量。对于记录侦听器,当AckMode
是任何手动值时,已确认记录的偏移量将被提交。对于记录侦听器, wWhen AckMode
is BATCH
,或者对于批处理侦听器,在容器重新启动时重播整个批处理。
容器停止后,ListenerExecutionFailedException
会抛出一个包装容器的异常。这是为了使事务回滚(如果启用了事务)。
委派错误处理程序
可以委托给不同的CommonDelegatingErrorHandler
错误处理程序,具体取决于异常类型。例如,您可能希望DefaultErrorHandler
为大多数异常调用 a,或CommonContainerStoppingErrorHandler
为其他异常调用 a。
为记录和批处理侦听器使用不同的常见错误处理程序
如果您希望对记录和批处理侦听器使用不同的错误处理策略,CommonMixedErrorHandler
则提供了允许为每种侦听器类型配置特定错误处理程序的方法。
常见错误处理程序总结
-
DefaultErrorHandler
-
CommonContainerStoppingErrorHandler
-
CommonDelegatingErrorHandler
-
CommonLoggingErrorHandler
-
CommonMixedErrorHandler
旧版错误处理程序及其替换
旧版错误处理程序 | 替代品 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
没有替代品, |
|
|
|
没有替换 - 使用 |
将自定义遗留错误处理程序实现迁移到CommonErrorHandler
请参阅 中的 javadocs CommonErrorHandler
。
要替换ErrorHandler
orConsumerAwareErrorHandler
实现,您应该实现handleRecord()
并离开remainingRecords()
返回false
(默认)。您还应该实现handleOtherException()
- 以处理记录处理范围之外发生的异常(例如,消费者错误)。
要替换RemainingRecordsErrorHandler
实现,您应该实现handleRemaining()
并覆盖remainingRecords()
返回true
。您还应该实现handleOtherException()
- 以处理记录处理范围之外发生的异常(例如,消费者错误)。
要替换任何BatchErrorHandler
实现,您应该实现handleBatch()
您还应该实现handleOtherException()
- 以处理记录处理范围之外发生的异常(例如消费者错误)。
回滚后处理器
使用事务时,如果侦听器抛出异常(并且错误处理程序,如果存在,则抛出异常),事务将回滚。默认情况下,任何未处理的记录(包括失败的记录)都会在下一次轮询时重新获取。这是通过seek
在DefaultAfterRollbackProcessor
. 使用批处理侦听器,将重新处理整批记录(容器不知道批处理中的哪条记录失败)。要修改此行为,您可以使用自定义配置侦听器容器AfterRollbackProcessor
。例如,对于基于记录的侦听器,您可能希望跟踪失败的记录并在尝试多次后放弃,可能通过将其发布到死信主题。
从 2.2 版开始,DefaultAfterRollbackProcessor
现在可以恢复(跳过)一直失败的记录。默认情况下,十次失败后,记录失败的记录(在ERROR
级别)。BiConsumer
您可以使用自定义恢复器 ( ) 和最大故障数来配置处理器。将该maxFailures
属性设置为负数会导致无限次重试。以下示例配置了三次尝试后的恢复:
AfterRollbackProcessor<String, String> 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
. 请参阅容器错误处理程序。
使用批处理侦听器无法进行恢复,因为框架不知道批处理中的哪条记录一直失败。在这种情况下,应用程序侦听器必须处理不断失败的记录。 |
另请参阅发布死信记录。
从版本 2.2.5 开始,DefaultAfterRollbackProcessor
可以在新事务中调用(在失败的事务回滚后开始)。然后,如果您使用DeadLetterPublishingRecoverer
发布失败的记录,处理器会将恢复的记录在原始主题/分区中的偏移量发送到事务。要启用此功能,commitRecovered
请kafkaTemplate
在DefaultAfterRollbackProcessor
.
如果恢复器失败(抛出异常),失败的记录将包含在搜索中。从版本 2.5.5 开始,如果恢复器失败,BackOff 将默认重置,并且在再次尝试恢复之前重新交付将再次经历回退。对于早期版本,BackOff 没有重置,并且在下一次失败时重新尝试恢复。要恢复到以前的行为,请将处理器的resetStateOnRecoveryFailure 属性设置为false 。
|
从 2.6 版开始,您现在可以根据失败记录和/或异常为处理器提供一个BiFunction<ConsumerRecord<?, ?>, Exception, BackOff>
来确定要使用的处理器:BackOff
handler.setBackOffFunction((record, ex) -> { ... });
如果函数返回null
,将使用处理器的默认值BackOff
。
从版本 2.6.3 开始,如果异常类型在失败之间发生变化,则设置resetStateOnExceptionChange
为true
并且重试序列将重新启动(包括选择新的BackOff
,如果已配置)。默认情况下,不考虑异常类型。
从 2.3.1 版本开始,类似于DefaultErrorHandler
,DefaultAfterRollbackProcessor
认为某些异常是致命的,并且对此类异常跳过重试;在第一次失败时调用恢复器。默认情况下,被认为是致命的异常是:
-
DeserializationException
-
MessageConversionException
-
ConversionException
-
MethodArgumentResolutionException
-
NoSuchMethodException
-
ClassCastException
因为这些异常不太可能在重试交付时得到解决。
您可以在不可重试类别中添加更多异常类型,或者完全替换分类异常的映射。有关更多信息,请参阅 Javadocs DefaultAfterRollbackProcessor.setClassifications()
,以及spring-retry
BinaryExceptionClassifier
.
这是一个添加IllegalArgumentException
到不可重试异常的示例:
@Bean
public DefaultAfterRollbackProcessor errorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
DefaultAfterRollbackProcessor processor = new DefaultAfterRollbackProcessor(recoverer);
processor.addNotRetryableException(IllegalArgumentException.class);
return processor;
}
另请参阅交付尝试标题。
使用 current kafka-clients ,容器无法检测 a 是否ProducerFencedException 是由重新平衡引起的,或者生产者transactional.id 是否由于超时或到期而被撤销。因为,在大多数情况下,它是由重新平衡引起的,容器不会调用AfterRollbackProcessor (因为不适合寻找分区,因为我们不再被分配它们)。如果您确保超时足够大以处理每个事务并定期执行“空”事务(例如通过 a ListenerContainerIdleEvent ),您可以避免由于超时和到期而导致的围栏。或者,您可以将stopContainerWhenFenced 容器属性设置为true ,容器将停止,避免记录丢失。您可以使用 aConsumerStoppedEvent 并检查Reason 属性FENCED 来检测这种情况。由于该事件还具有对容器的引用,因此您可以使用此事件重新启动容器。
|
从 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 版开始,当使用实现的ErrorHandler
or时,可以将标头 ( ) 添加到记录中。此标头的值是一个从 1 开始递增的整数。接收原始数据时,该整数位于.AfterRollbackProcessor
DeliveryAttemptAware
KafkaHeaders.DELIVERY_ATTEMPT
kafka_deliveryAttempt
ConsumerRecord<?, ?>
byte[4]
int delivery = ByteBuffer.wrap(record.headers()
.lastHeader(KafkaHeaders.DELIVERY_ATTEMPT).value())
.getInt()
@KafkaListener
与DefaultKafkaHeaderMapper
or一起使用时SimpleKafkaHeaderMapper
,可以通过将@Header(KafkaHeaders.DELIVERY_ATTEMPT) int delivery
其作为参数添加到侦听器方法中来获得。
要启用此标头的填充,请将容器属性设置deliveryAttemptHeader
为true
。默认情况下禁用它以避免查找每条记录的状态和添加标题的(小)开销。
并DefaultErrorHandler
支持DefaultAfterRollbackProcessor
此功能。
侦听器信息标题
在某些情况下,能够知道侦听器在哪个容器中运行很有用。
从版本 2.8.4 开始,您现在可以listenerInfo
在侦听器容器上设置info
属性,或在@KafkaListener
注释上设置属性。然后,容器会将这个添加KafkaListener.LISTENER_INFO
到所有传入消息的标头中;然后,它可以用于记录拦截器、过滤器等,或者用于侦听器本身。
@KafkaListener(id = "something", topic = "topic", filter = "someFilter",
info = "this is the something listener")
public void listen2(@Payload Thing thing,
@Header(KafkaHeaders.LISTENER_INFO) String listenerInfo) {
...
}
在 a RecordInterceptor
orRecordFilterStrategy
实现中使用时,标头在消费者记录中作为字节数组,使用KafkaListenerAnnotationBeanPostProcessor
'charSet
属性进行转换。
当从消费者记录String
创建时,标头映射器也会转换为,并且永远不会将此标头映射到出站记录上。MessageHeaders
对于 POJO 批处理侦听器,从版本 2.8.6 开始,标头被复制到批处理的每个成员中,并且在转换后也可作为单个String
参数使用。
@KafkaListener(id = "list2", topics = "someTopic", containerFactory = "batchFactory",
info = "info for batch")
public void listen(List<Thing> list,
@Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) List<Integer> keys,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) List<Integer> partitions,
@Header(KafkaHeaders.RECEIVED_TOPIC) List<String> topics,
@Header(KafkaHeaders.OFFSET) List<Long> offsets,
@Header(KafkaHeaders.LISTENER_INFO) String info) {
...
}
如果批处理侦听器具有过滤器并且过滤器导致空批处理,则您需要添加required = false 到@Header 参数,因为该信息不适用于空批处理。
|
如果您收到List<Message<Thing>>
信息,则在KafkaHeaders.LISTENER_INFO
每个Message<?>
.
有关使用批处理的更多信息,请参阅批处理侦听器。
发布死信记录
当达到记录的最大失败次数时,您可以使用记录恢复器配置DefaultErrorHandler
and 。DefaultAfterRollbackProcessor
框架提供了DeadLetterPublishingRecoverer
,它将失败的消息发布到另一个主题。恢复器需要一个KafkaTemplate<Object, Object>
,用于发送记录。您还可以选择使用 配置它,BiFunction<ConsumerRecord<?, ?>, Exception, TopicPartition>
调用它来解析目标主题和分区。
默认情况下,死信记录被发送到一个名为<originalTopic>.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());
}
});
CommonErrorHandler errorHandler = new DefaultErrorHandler(recoverer, new FixedBackOff(0L, 2L));
发送到死信主题的记录通过以下标头进行了增强:
-
KafkaHeaders.DLT_EXCEPTION_FQCN
: Exception 类名(通常是ListenerExecutionFailedException
,但也可以是其他的)。 -
KafkaHeaders.DLT_EXCEPTION_CAUSE_FQCN
: 异常原因类名,如果存在(从 2.8 版开始)。 -
KafkaHeaders.DLT_EXCEPTION_STACKTRACE
:异常堆栈跟踪。 -
KafkaHeaders.DLT_EXCEPTION_MESSAGE
:异常消息。 -
KafkaHeaders.DLT_KEY_EXCEPTION_FQCN
: Exception 类名(仅限键反序列化错误)。 -
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
.
有两种机制可以添加更多标头。
-
子类化恢复器并覆盖
createProducerRecord()
- 调用super.createProducerRecord()
并添加更多标头。 -
提供一个
BiFunction
接收消费者记录和异常,返回一个Headers
对象;那里的标头将被复制到最终的生产者记录中;另请参阅管理死信记录标头。用于setHeadersFunction()
设置BiFunction
.
第二个实现起来更简单,但第一个有更多可用信息,包括已经组装的标准头文件。
从 2.3 版本开始,当与 an 一起使用时ErrorHandlingDeserializer
,发布者会将value()
死信生产者记录中的记录恢复为无法反序列化的原始值。以前,value()
为 null 并且用户代码必须DeserializationException
从消息头中解码 。此外,您可以向KafkaTemplate
发布者提供多个 s;这可能是需要的,例如,如果您想发布byte[]
from aDeserializationException
以及使用与成功反序列化的记录不同的序列化程序的值。下面是一个使用KafkaTemplate
sString
和byte[]
序列化器配置发布者的示例:
@Bean
public DeadLetterPublishingRecoverer publisher(KafkaTemplate<?, ?> stringTemplate,
KafkaTemplate<?, ?> bytesTemplate) {
Map<Class<?>, KafkaTemplate<?, ?>> templates = new LinkedHashMap<>();
templates.put(String.class, stringTemplate);
templates.put(byte[].class, bytesTemplate);
return new DeadLetterPublishingRecoverer(templates);
}
发布者使用映射键来定位适合value()
即将发布的模板。建议使用A LinkedHashMap
,以便按顺序检查键。
发布值时,当有多个模板时,recoverer会为该类null
寻找一个模板;Void
如果不存在,values().iterator()
将使用来自的第一个模板。
从 2.7 开始,您可以使用该setFailIfSendResultIsError
方法,以便在消息发布失败时引发异常。您还可以使用 设置验证发件人成功的超时时间setWaitForSendResultTimeout
。
如果恢复器失败(抛出异常),失败的记录将包含在搜索中。从版本 2.5.5 开始,如果恢复器失败,BackOff 将默认重置,并且在再次尝试恢复之前重新交付将再次经历回退。对于早期版本,BackOff 没有重置,并且在下一次失败时重新尝试恢复。要恢复到以前的行为,请将错误处理程序的resetStateOnRecoveryFailure 属性设置为false 。
|
从版本 2.6.3 开始,如果异常类型在失败之间发生变化,则设置resetStateOnExceptionChange
为true
并且重试序列将重新启动(包括选择新的BackOff
,如果已配置)。默认情况下,不考虑异常类型。
从 2.3 版开始,恢复器也可以与 Kafka Streams 一起使用 -有关更多信息,请参阅从反序列化异常中恢复。
在标头中ErrorHandlingDeserializer
添加反序列化异常ErrorHandlingDeserializer.VALUE_DESERIALIZER_EXCEPTION_HEADER
和ErrorHandlingDeserializer.KEY_DESERIALIZER_EXCEPTION_HEADER
(使用 java 序列化)。默认情况下,这些标头不会保留在发布到死信主题的消息中。从版本 2.7 开始,如果键和值都未能反序列化,则两者的原始值都会填充到发送到 DLT 的记录中。
如果传入记录相互依赖,但可能无序到达,则将失败的记录重新发布到原始主题的尾部(一定次数)可能很有用,而不是将其直接发送到死信主题. 有关示例,请参阅此堆栈溢出问题。
以下错误处理程序配置将完全做到这一点:
@Bean
public ErrorHandler eh(KafkaOperations<String, String> 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
。
管理死信记录头
参考上面的Publishing Dead-letter RecordsDeadLetterPublishingRecoverer
,当这些标头已经存在时(例如在重新处理失败的死信记录时,包括使用Non-Blocking Retries时),有两个属性用于管理标头。
-
appendOriginalHeaders
(默认true
) -
stripPreviousExceptionHeaders
true
(自 2.8 版起默认)
Apache Kafka 支持多个同名标头;要获得“最新”值,您可以使用headers.lastHeader(headerName)
; 要获取多个标头的迭代器,请使用headers.headers(headerName).iterator()
.
当反复重新发布失败的记录时,这些标头可能会增长(并最终导致发布失败,因为 a RecordTooLargeException
);对于异常标头尤其是堆栈跟踪标头尤其如此。
使用这两个属性的原因是,虽然您可能只想保留最后的异常信息,但您可能希望保留每次失败时记录通过的主题的历史记录。
appendOriginalHeaders
应用于所有名为的头文件ORIGINAL
,而stripPreviousExceptionHeaders
应用于所有名为的头文件EXCEPTION
。
从版本 2.8.4 开始,您现在可以控制将哪些标准标头添加到输出记录中。请参阅enum HeadersToAdd
默认添加的(当前)10 个标准头文件的通用名称(这些不是实际的头文件名称,只是一个抽象;实际的头文件名称由getHeaderNames()
子类可以覆盖的方法设置。
要排除标题,请使用excludeHeaders()
方法;例如,要禁止在标头中添加异常堆栈跟踪,请使用:
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
recoverer.excludeHeaders(HeaderNames.HeadersToAdd.EX_STACKTRACE);
此外,您可以通过添加 ; 来完全自定义异常头的添加ExceptionHeadersCreator
。这也会禁用所有标准异常标头。
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
recoverer.setExceptionHeadersCreator((kafkaHeaders, exception, isKey, headerNames) -> {
kafkaHeaders.add(new RecordHeader(..., ...));
});
同样从 2.8.4 版本开始,您现在可以通过该addHeadersFunction
方法提供多个标头功能。这允许应用附加功能,即使已经注册了另一个功能,例如,在使用Non-Blocking Retries时。
ExponentialBackOffWithMaxRetries
执行
Spring Framework 提供了许多BackOff
实现。默认情况下,ExponentialBackOff
将无限期重试;在多次重试尝试后放弃需要计算maxElapsedTime
. 从 2.7.3 版本开始,Spring for Apache Kafka 提供了ExponentialBackOffWithMaxRetries
which 是一个子类,它接收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
几秒钟后重试,然后再调用恢复器。
4.1.21。JAAS 和 Kerberos
从 2.0 版开始,KafkaJaasLoginModuleInitializer
添加了一个类来协助 Kerberos 配置。您可以使用所需的配置将此 bean 添加到应用程序上下文中。以下示例配置了这样一个 bean:
@Bean
public KafkaJaasLoginModuleInitializer jaasConfig() throws IOException {
KafkaJaasLoginModuleInitializer jaasConfig = new KafkaJaasLoginModuleInitializer();
jaasConfig.setControlFlag("REQUIRED");
Map<String, String> options = new HashMap<>();
options.put("useKeyTab", "true");
options.put("storeKey", "true");
options.put("keyTab", "/etc/security/keytabs/kafka_client.keytab");
options.put("principal", "kafka-client-1@EXAMPLE.COM");
jaasConfig.setOptions(options);
return jaasConfig;
}
4.2. Apache Kafka 流支持
从 1.1.4 版开始,Spring for Apache Kafka 为Kafka Streams提供一流的支持。要从 Spring 应用程序中使用它,kafka-streams
jar 必须存在于类路径中。它是 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
:管理这些实例的生命周期。
单个KStream 暴露给一个实例的
所有实例同时启动和停止,即使它们具有不同的逻辑。换句话说,由 a 定义的所有流都与单个生命周期控制相关联。一旦实例被 关闭,它就无法重新启动。相反,必须创建一个新实例来重新启动流处理。
KafkaStreams StreamsBuilder StreamsBuilder KafkaStreams streams.close() KafkaStreams |
4.2.2. 弹簧管理
为了从 Spring 应用程序上下文的角度简化 Kafka Streams 的使用并通过容器使用生命周期管理,Spring for Apache Kafka 引入了StreamsBuilderFactoryBean
. 这是一个AbstractFactoryBean
将StreamsBuilder
单例实例公开为 bean 的实现。以下示例创建了这样一个 bean:
@Bean
public FactoryBean<StreamsBuilder> myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) {
return new StreamsBuilderFactoryBean(streamsConfig);
}
从 2.2 版开始,流配置现在作为KafkaStreamsConfiguration 对象而不是StreamsConfig .
|
StreamsBuilderFactoryBean
还实现SmartLifecycle
了管理内部KafkaStreams
实例的生命周期。与 Kafka Streams API 类似,您必须KStream
在启动KafkaStreams
. 这也适用于 Kafka Streams 的 Spring API。因此,当您在 上使用默认值autoStartup = true
时StreamsBuilderFactoryBean
,您必须在刷新应用程序上下文之前在KStream
上声明实例。StreamsBuilder
例如,KStream
可以是常规的 bean 定义,而使用 Kafka Streams API 时没有任何影响。以下示例显示了如何执行此操作:
@Bean
public KStream<?, ?> kStream(StreamsBuilder kStreamBuilder) {
KStream<Integer, String> stream = kStreamBuilder.stream(STREAMING_TOPIC1);
// Fluent KStream API
return stream;
}
如果您想手动控制生命周期(例如,根据某些条件停止和启动),您可以StreamsBuilderFactoryBean
使用工厂 bean ( &
)前缀直接引用 bean 。由于StreamsBuilderFactoryBean
使用其内部KafkaStreams
实例,因此可以安全地停止并重新启动它。在KafkaStreams
每个start()
. StreamsBuilderFactoryBean
如果您想分别控制实例的生命周期,您也可以考虑使用不同的实例KStream
。
您还可以在 上指定KafkaStreams.StateListener
、Thread.UncaughtExceptionHandler
和StateRestoreListener
选项StreamsBuilderFactoryBean
,它们被委派给内部KafkaStreams
实例。此外,除了在 2.1.5 上间接设置这些选项外StreamsBuilderFactoryBean
,您还可以使用KafkaStreamsCustomizer
回调接口来配置内部KafkaStreams
实例。请注意,它KafkaStreamsCustomizer
会覆盖StreamsBuilderFactoryBean
. 如果您需要KafkaStreams
直接执行某些操作,您可以KafkaStreams
使用StreamsBuilderFactoryBean.getKafkaStreams()
. 您可以按类型自动装配StreamsBuilderFactoryBean
bean,但应确保在 bean 定义中使用完整类型,如以下示例所示:
@Bean
public StreamsBuilderFactoryBean myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) {
return new StreamsBuilderFactoryBean(streamsConfig);
}
...
@Autowired
private StreamsBuilderFactoryBean myKStreamBuilderFactoryBean;
或者,@Qualifier
如果您使用接口 bean 定义,则可以按名称添加注入。以下示例显示了如何执行此操作:
@Bean
public FactoryBean<StreamsBuilder> myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) {
return new StreamsBuilderFactoryBean(streamsConfig);
}
...
@Autowired
@Qualifier("&myKStreamBuilder")
private StreamsBuilderFactoryBean myKStreamBuilderFactoryBean;
从版本 2.4.1 开始,工厂 bean 有一个新infrastructureCustomizer
的 type属性KafkaStreamsInfrastructureCustomizer
;这允许在创建流之前自定义StreamsBuilder
(例如添加状态存储)和/或。Topology
public interface KafkaStreamsInfrastructureCustomizer {
void configureBuilder(StreamsBuilder builder);
void configureTopology(Topology topology);
}
提供了默认的无操作实现以避免在不需要时必须实现这两种方法。
提供了一个CompositeKafkaStreamsInfrastructureCustomizer
,用于当您需要应用多个定制器时。
4.2.3。KafkaStreams 千分尺支持
在 2.5.3 版本中引入,您可以配置一个为工厂 bean 管理KafkaStreamsMicrometerListener
的对象自动注册千分尺: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
中的描述。该实现通过其构造函数(目标类型或)提供相同的配置选项。在下面的示例中,我们使用来序列化和反序列化Kafka 流的有效负载(可以在需要实例的地方以类似的方式使用):JsonSerde
ObjectMapper
JsonSerde
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<String, String>[] 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<String, String>()
.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文档。
从 2.2 版开始,流配置现在作为KafkaStreamsConfiguration 对象提供,而不是作为StreamsConfig .
|
为了避免大多数情况下的样板代码,尤其是在您开发微服务时,Spring for Apache Kafka 提供了@EnableKafkaStreams
注释,您应该将其放在@Configuration
类中。您只需要声明一个KafkaStreamsConfiguration
名为defaultKafkaStreamsConfig
. 一个StreamsBuilderFactoryBean
名为 的 beandefaultKafkaStreamsBuilder
会在应用程序上下文中自动声明。您也可以声明和使用任何其他StreamsBuilderFactoryBean
bean。您可以通过提供一个实现StreamsBuilderFactoryBeanConfigurer
. 如果有多个这样的bean,它们将根据它们的Ordered.order
属性应用。
默认情况下,当工厂 bean 停止时,KafkaStreams.cleanUp()
会调用该方法。从版本 2.1.2 开始,工厂 bean 具有额外的构造函数,采用CleanupConfig
具有属性的对象,让您控制是否在期间调用方法或不cleanUp()
调用方法。从 2.7 版开始,默认是从不清理本地状态。start()
stop()
4.2.7。标题丰富器
2.3 版添加HeaderEnricher
了Transformer
. 这可用于在流处理中添加标头;标头值是 SpEL 表达式;表达式求值的根对象有 3 个属性:
-
context
- theProcessorContext
,允许访问当前记录元数据 -
key
- 当前记录的键 -
value
- 当前记录的值
表达式必须返回 abyte[]
或 a String
(将转换为byte[]
using UTF-8
)。
要在流中使用扩充器:
.transform(() -> enricher)
变压器不改变key
或value
;它只是添加标题。
如果您的流是多线程的,则每条记录都需要一个新实例。 |
.transform(() -> new HeaderEnricher<..., ...>(expressionMap))
这是一个简单的示例,添加一个文字标题和一个变量:
Map<String, Expression> headers = new HashMap<>();
headers.put("header1", new LiteralExpression("value1"));
SpelExpressionParser parser = new SpelExpressionParser();
headers.put("header2", parser.parseExpression("context.timestamp() + ' @' + context.offset()"));
HeaderEnricher<String, String> enricher = new HeaderEnricher<>(headers);
KStream<String, String> 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 Integration 使用它的GatewayProxyFactoryBean
. 它还需要MessagingMessageConverter
将键、值和元数据(包括标头)与 Spring Messaging 相互转换Message<?>
。有关更多信息,请参阅[从 a 调用 Spring 集成流KStream
]。
4.2.9. 从反序列化异常中恢复
2.3 版引入了RecoveringDeserializationExceptionHandler
当反序列化异常发生时可以采取一些措施。请参阅 Kafka 文档DeserializationExceptionHandler
,其中RecoveringDeserializationExceptionHandler
是一个实现。配置RecoveringDeserializationExceptionHandler
了一个ConsumerRecordRecoverer
实现。该框架提供了DeadLetterPublishingRecoverer
将失败记录发送到死信主题的方法。有关此恢复器的更多信息,请参阅发布死信记录。
要配置恢复器,请将以下属性添加到您的流配置中:
@Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME)
public KafkaStreamsConfiguration kStreamsConfigs() {
Map<String, Object> 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 流示例
以下示例结合了我们在本章中介绍的所有主题:
@Configuration
@EnableKafka
@EnableKafkaStreams
public static class KafkaStreamsConfig {
@Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME)
public KafkaStreamsConfiguration kStreamsConfigs() {
Map<String, Object> 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<Integer, String> kStream(StreamsBuilder kStreamBuilder) {
KStream<Integer, String> stream = kStreamBuilder.stream("streamingTopic1");
stream
.mapValues((ValueMapper<String, String>) 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。
4.3.2. JUnit
o.s.kafka.test.utils.KafkaTestUtils
还提供了一些静态方法来设置生产者和消费者属性。以下清单显示了这些方法签名:
/**
* Set up test properties for an {@code <Integer, String>} consumer.
* @param group the group id.
* @param autoCommit the auto commit.
* @param embeddedKafka a {@link EmbeddedKafkaBroker} instance.
* @return the properties.
*/
public static Map<String, Object> consumerProps(String group, String autoCommit,
EmbeddedKafkaBroker embeddedKafka) { ... }
/**
* Set up test properties for an {@code <Integer, String>} producer.
* @param embeddedKafka a {@link EmbeddedKafkaBroker} instance.
* @return the properties.
*/
public static Map<String, Object> producerProps(EmbeddedKafkaBroker embeddedKafka) { ... }
从 2.5 版开始,该 使用嵌入式代理时,通常最好的做法是为每个测试使用不同的主题,以防止串扰。如果由于某种原因无法做到这一点,请注意该 |
提供了一个 JUnit 4@Rule
包装器EmbeddedKafkaBroker
来创建嵌入式 Kafka 和嵌入式 Zookeeper 服务器。(有关使用JUnit 5的信息,请参阅@EmbeddedKafka 注释)。@EmbeddedKafka
以下清单显示了这些方法的签名:
/**
* 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<String, Object> consumerProps = KafkaTestUtils.consumerProps("testT", "false", embeddedKafka);
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<Integer, String>(
consumerProps);
Consumer<Integer, String> 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 <K, V> ConsumerRecord<K, V> getSingleRecord(Consumer<K, V> consumer, String topic) { ... }
/**
* Poll the consumer for records.
* @param consumer the consumer.
* @return the records.
*/
public static <K, V> ConsumerRecords<K, V> getRecords(Consumer<K, V> consumer) { ... }
下面的例子展示了如何使用KafkaTestUtils
:
...
template.sendDefault(0, 2, "bar");
ConsumerRecord<Integer, String> received = KafkaTestUtils.getSingleRecord(consumer, "topic");
...
当嵌入式 Kafka 和嵌入式 Zookeeper 服务器由 启动时EmbeddedKafkaBroker
,名为的系统属性spring.embedded.kafka.brokers
设置为 Kafka 代理的地址,并且名为的系统属性spring.embedded.zookeeper.connect
设置为 Zookeeper 的地址。为此属性提供了方便的常量 (EmbeddedKafkaBroker.SPRING_EMBEDDED_KAFKA_BROKERS
和)。EmbeddedKafkaBroker.SPRING_EMBEDDED_ZOOKEEPER_CONNECT
使用EmbeddedKafkaBroker.brokerProperties(Map<String, String>)
,您可以为 Kafka 服务器提供额外的属性。有关可能的代理属性的更多信息,请参阅Kafka 配置。
4.3.3. 配置主题
以下示例配置创建名为cat
和的主题,hat
具有五个分区,一个名为thing1
10 个分区的主题,一个名为thing2
15 个分区的主题:
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<String, Exception>
; 键是主题名称,值是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 Boot 环境,并且嵌入式代理替换了引导服务器属性。
然后,在每个测试类中,您可以使用类似于以下内容的内容:
static {
EmbeddedKafkaHolder.getEmbeddedKafka().addTopics("topic1", "topic2");
}
private static final EmbeddedKafkaBroker broker = EmbeddedKafkaHolder.getEmbeddedKafka();
如果您不使用 Spring Boot,则可以使用broker.getBrokersAsString()
.
前面的示例没有提供在所有测试完成时关闭代理的机制。如果您在 Gradle 守护程序中运行测试,这可能是一个问题。在这种情况下,您不应该使用此技术,或者您应该在测试完成时使用某些东西来destroy() 调用EmbeddedKafkaBroker 。
|
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<String, Object> consumerProps = KafkaTestUtils.consumerProps("testGroup", "true", this.embeddedKafka);
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
ConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(consumerProps);
Consumer<Integer, String> consumer = cf.createConsumer();
this.embeddedKafka.consumeFromAnEmbeddedTopic(consumer, KafkaStreamsTests.STREAMING_TOPIC2);
ConsumerRecords<Integer, String> 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<String, Object> 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 端口属性。
以下示例设置支持属性占位符解析的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
从brokerPropertiesLocation
. brokerPropertiesLocation
为URL 和资源中找到的任何属性占位符解析属性占位符。brokerProperties
由在 中找到的覆盖属性定义的属性brokerPropertiesLocation
。
您可以将@EmbeddedKafka
注释与 JUnit 4 或 JUnit 5 一起使用。
4.3.6. 使用 JUnit5 进行 @EmbeddedKafka 注释
从 2.3 版开始,有两种方法可以将@EmbeddedKafka
注解与 JUnit5 一起使用。与@SpringJunitConfig
注释一起使用时,嵌入式代理将添加到测试应用程序上下文中。您可以在类或方法级别将代理自动连接到您的测试中,以获取代理地址列表。
当不使用 spring 测试上下文时,EmbdeddedKafkaCondition
创建一个代理;该条件包括一个参数解析器,因此您可以在您的测试方法中访问代理……
@EmbeddedKafka
public class EmbeddedKafkaConditionTests {
@Test
public void test(EmbeddedKafkaBroker broker) {
String brokerList = broker.getBrokersAsString();
...
}
}
@EmbeddedBroker
如果用 注释的类也没有用 注释(或元注释),则将创建一个独立(不是 Spring 测试上下文)代理ExtendedWith(SpringExtension.class)
。
@SpringJunitConfig
并且@SpringBootTest
被元注释,并且当这些注释中的任何一个也存在时,将使用基于上下文的代理。
当有可用的 Spring 测试应用程序上下文时,主题和代理属性可以包含属性占位符,只要在某处定义了属性,就会解析这些占位符。如果没有可用的 Spring 上下文,则不会解析这些占位符。 |
4.3.7. @SpringBootTest
注释中的嵌入式代理
Spring Initializr现在自动将spring-kafka-test
测试范围内的依赖添加到项目配置中。
如果您的应用程序使用 Kafka binder
|
在 Spring Boot 应用程序测试中使用嵌入式代理有多种方法。
他们包括:
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<String, String> template;
@Test
public void test() {
...
}
}
请注意,由于这是一个 Spring Boot 应用程序,我们覆盖了代理列表属性来设置 Boot 的属性。
@EmbeddedKafka
注释或EmbeddedKafkaBroker
Bean
以下示例展示了如何使用@EmbeddedKafka
Annotation 创建嵌入式代理:
@RunWith(SpringRunner.class)
@EmbeddedKafka(topics = "someTopic",
bootstrapServersProperty = "spring.kafka.bootstrap-servers")
public class MyApplicationTests {
@Autowired
private KafkaTemplate<String, String> template;
@Test
public void test() {
...
}
}
4.3.8。Hamcrest 火柴人
提供以下o.s.kafka.test.hamcrest.KafkaMatchers
匹配器:
/**
* @param key the key
* @param <K> the type.
* @return a Matcher that matches the key in a consumer record.
*/
public static <K> Matcher<ConsumerRecord<K, ?>> hasKey(K key) { ... }
/**
* @param value the value.
* @param <V> the type.
* @return a Matcher that matches the value in a consumer record.
*/
public static <V> Matcher<ConsumerRecord<?, V>> hasValue(V value) { ... }
/**
* @param partition the partition.
* @return a Matcher that matches the partition in a consumer record.
*/
public static Matcher<ConsumerRecord<?, ?>> 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<ConsumerRecord<?, ?>> 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<ConsumerRecord<?, ?>> hasTimestamp(TimestampType type, long ts) {
return new ConsumerRecordTimestampMatcher(type, ts);
}
4.3.9。断言条件
您可以使用以下 AssertJ 条件:
/**
* @param key the key
* @param <K> the type.
* @return a Condition that matches the key in a consumer record.
*/
public static <K> Condition<ConsumerRecord<K, ?>> key(K key) { ... }
/**
* @param value the value.
* @param <V> the type.
* @return a Condition that matches the value in a consumer record.
*/
public static <V> Condition<ConsumerRecord<?, V>> value(V value) { ... }
/**
* @param key the key.
* @param value the value.
* @param <K> the key type.
* @param <V> the value type.
* @return a Condition that matches the key in a consumer record.
* @since 2.2.12
*/
public static <K, V> Condition<ConsumerRecord<K, V>> keyValue(K key, V value) { ... }
/**
* @param partition the partition.
* @return a Condition that matches the partition in a consumer record.
*/
public static Condition<ConsumerRecord<?, ?>> partition(int partition) { ... }
/**
* @param value the timestamp.
* @return a Condition that matches the timestamp value in a consumer record.
*/
public static Condition<ConsumerRecord<?, ?>> 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<ConsumerRecord<?, ?>> 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<String, Object> consumerProps = KafkaTestUtils.consumerProps("testT", "false",
embeddedKafka.getEmbeddedKafka());
DefaultKafkaConsumerFactory<Integer, String> cf =
new DefaultKafkaConsumerFactory<Integer, String>(consumerProps);
ContainerProperties containerProperties = new ContainerProperties(TEMPLATE_TOPIC);
KafkaMessageListenerContainer<Integer, String> container =
new KafkaMessageListenerContainer<>(cf, containerProperties);
final BlockingQueue<ConsumerRecord<Integer, String>> records = new LinkedBlockingQueue<>();
container.setupMessageListener(new MessageListener<Integer, String>() {
@Override
public void onMessage(ConsumerRecord<Integer, String> record) {
System.out.println(record);
records.add(record);
}
});
container.setBeanName("templateTests");
container.start();
ContainerTestUtils.waitForAssignment(container,
embeddedKafka.getEmbeddedKafka().getPartitionsPerTopic());
Map<String, Object> producerProps =
KafkaTestUtils.producerProps(embeddedKafka.getEmbeddedKafka());
ProducerFactory<Integer, String> pf =
new DefaultKafkaProducerFactory<Integer, String>(producerProps);
KafkaTemplate<Integer, String> 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<Integer, String> 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 匹配器。使用AssertJ
,最后一部分看起来像下面的代码:
assertThat(records.poll(10, TimeUnit.SECONDS)).has(value("foo"));
template.sendDefault(0, 2, "bar");
ConsumerRecord<Integer, String> 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 功能通常需要设置额外的主题并创建和配置相应的侦听器。@RetryableTopic
从 2.7 Spring for Apache Kafka 开始,通过注解和类提供支持,RetryTopicConfiguration
以简化引导。
4.4.1。模式是如何工作的
如果消息处理失败,则将消息转发到带有退避时间戳的重试主题。重试主题消费者然后检查时间戳,如果它没有到期,它会暂停该主题分区的消费。到期时恢复分区消费,再次消费消息。如果消息处理再次失败,消息将被转发到下一个重试主题,并重复该模式,直到成功处理,或者尝试用尽,然后将消息发送到死信主题(如果已配置)。
为了说明,如果您有一个“主主题”主题,并且想要设置非阻塞重试,指数退避为 1000 毫秒,最大尝试次数为 2 次和 4 次,它将创建主主题重试 1000, main-topic-retry-2000、main-topic-retry-4000 和 main-topic-dlt 主题并配置各自的消费者。该框架还负责创建主题以及设置和配置侦听器。
通过使用此策略,您将失去 Kafka 对该主题的排序保证。 |
您可以设置AckMode 您喜欢的模式,但RECORD 建议使用。
|
目前此功能不支持类级别@KafkaListener 注释
|
4.4.2. 回退延迟精度
概述和保证
所有消息处理和回退都由消费者线程处理,因此,延迟精度在最大努力的基础上得到保证。如果一条消息的处理时间长于该消费者的下一条消息的回退期,则下一条消息的延迟将高于预期。此外,对于短暂的延迟(大约 1 秒或更短),线程必须做的维护工作,例如提交偏移量,可能会延迟消息处理的执行。如果重试主题的消费者处理多个分区,精度也会受到影响,因为我们依靠从轮询中唤醒消费者并拥有完整的 pollTimeouts 来进行时间调整。
话虽如此,对于处理单个分区的消费者,消息的处理应该在大多数情况下的确切到期时间之后的 100 毫秒内发生。
保证在到期时间之前永远不会处理消息。 |
调整延迟精度
消息的处理延迟精度取决于两个ContainerProperties
:ContainerProperties.pollTimeout
和ContainerProperties.idlePartitionEventInterval
。这两个属性都会在重试主题和 dlt 中自动设置为该主题ListenerContainerFactory
最小延迟值的四分之一,最小值为 250 毫秒,最大值为 5000 毫秒。仅当属性具有其默认值时才会设置这些值 - 如果您自己更改任何一个值,您的更改将不会被覆盖。这样,您可以根据需要调整重试主题的精度和性能。
您可以ListenerContainerFactory 为主要主题和重试主题设置单独的实例 - 这样您可以拥有不同的设置以更好地满足您的需求,例如为主要主题设置较高的轮询超时设置,为重试主题设置较低的设置。
|
4.4.3. 配置
使用@RetryableTopic
注释
要为带@KafkaListener
注释的方法配置重试主题和 dlt,您只需向其中添加@RetryableTopic
注释,Spring for Apache Kafka 将使用默认配置引导所有必要的主题和使用者。
@RetryableTopic(kafkaTemplate = "myRetryableTopicKafkaTemplate")
@KafkaListener(topics = "my-annotated-topic", groupId = "myGroupId")
public void processMessage(MyPojo message) {
// ... message processing
}
您可以在同一个类中指定一个方法来处理 dlt 消息,方法是使用注解对其进行@DltHandler
注解。如果没有提供 DltHandler 方法,则会创建一个仅记录消费的默认消费者。
@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}
如果您不指定 kafkaTemplate 名称,retryTopicDefaultKafkaTemplate 则会查找具有名称的 bean。如果没有找到 bean,则抛出异常。
|
使用RetryTopicConfiguration
bean
您还可以通过在带注释的类中创建RetryTopicConfiguration
bean 来配置非阻塞重试支持。@Configuration
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, Object> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.create(template);
}
这将使用默认配置为使用“@KafkaListener”注释的方法中的所有主题创建重试主题和 dlt,以及相应的消费者。KafkaTemplate
消息转发需要实例。
为了对如何处理每个主题的非阻塞重试实现更细粒度的控制,RetryTopicConfiguration
可以提供多个 bean。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.fixedBackoff(3000)
.maxAttempts(5)
.includeTopics("my-topic", "my-other-topic")
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<String, MyOtherPojo> 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 主题后缀的组合。如果您不提供任何内容,它们都将属于同一组,并且重试主题上的重新平衡将导致主主题上不必要的重新平衡。
|
如果消费者配置了ErrorHandlingDeserializer , 来处理反序列化异常,那么为消费者KafkaTemplate 及其生产者配置一个序列化程序非常重要,该序列化程序可以处理正常对象以及byte[] 由反序列化异常产生的原始值。模板的通用值类型应该是Object . 一种技术是使用DelegatingByTypeSerializer ; 一个例子如下:
|
@Bean
public ProducerFactory<String, Object> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(),
new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(),
MyNormalObject.class, new JsonSerializer<Object>())));
}
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
多个@KafkaListener 注释可以用于同一个主题,有或没有手动分区分配以及非阻塞重试,但只有一个配置将用于给定主题。最好使用单个RetryTopicConfiguration bean 来配置此类主题;如果多个@RetryableTopic 注解用于同一主题,则它们都应具有相同的值,否则其中一个将应用于该主题的所有侦听器,而其他注解的值将被忽略。
|
4.4.4. 特征
大多数功能都可用于@RetryableTopic
注释和RetryTopicConfiguration
bean。
退避配置
BackOff 配置依赖于项目中的BackOffPolicy
接口Spring Retry
。
这包括:
-
固定后退
-
指数回退
-
随机指数回退
-
均匀随机退避
-
无后退
-
自定义后退
@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<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.fixedBackoff(3000)
.maxAttempts(4)
.build();
}
您还可以提供 Spring RetrySleepingBackOffPolicy
接口的自定义实现:
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.customBackOff(new MyCustomBackOffPolicy())
.maxAttempts(5)
.build();
}
默认退避策略FixedBackOffPolicy 最多尝试 3 次,间隔为 1000 毫秒。
|
有 30 秒的默认最大延迟ExponentialBackOffPolicy 。如果您的退避策略需要大于该值的延迟,请相应地调整 maxDelay 属性。
|
第一次尝试计入maxAttempts ,因此如果您提供maxAttempts 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<String, MyPojo> 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<String, MyPojo> 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<String, MyOtherPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.notRetryOn(MyDontRetryException.class)
.create(template);
}
默认行为是重试所有异常而不是遍历原因。 |
从 2.8.3 开始,有一个致命异常的全局列表,这将导致记录被发送到 DLT 而无需任何重试。有关致命异常的默认列表,请参阅DefaultErrorHandler。您可以使用以下方法在此列表中添加或删除例外:
@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 。
|
包括和排除主题
RetryTopicConfiguration
您可以通过 .includeTopic(String topic)、.includeTopics(Collection<String> topics)、.excludeTopic(String topic) 和 .excludeTopics(Collection<String> topics) 方法决定 bean 将处理和不处理哪些主题。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.includeTopics(List.of("my-included-topic", "my-other-included-topic"))
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.excludeTopic("my-excluded-topic")
.create(template);
}
默认行为是包含所有主题。 |
主题自动创建
除非另有说明,否则框架将使用NewTopic
bean 使用的 bean自动创建所需的主题KafkaAdmin
。您可以指定将创建主题的分区数和复制因子,并且可以关闭此功能。
请注意,如果您不使用 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<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.autoCreateTopicsWith(2, 3)
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.doNotAutoCreateRetryTopics()
.create(template);
}
默认情况下,主题是使用一个分区和一个复制因子自动创建的。 |
失败标头管理
在考虑如何管理失败标头(原始标头和异常标头)时,框架委托给DeadLetterPublishingRecover
决定是追加还是替换标头。
默认情况下,它显式设置appendOriginalHeaders
并false
保留.stripPreviousExceptionHeaders
DeadLetterPublishingRecover
这意味着只有第一个“原始”和最后一个异常标头保留在默认配置中。这是为了避免在涉及许多重试步骤时创建过大的消息(例如,由于堆栈跟踪标头)。
有关详细信息,请参阅管理死信记录标头。
要重新配置框架以对这些属性使用不同的设置,请DeadLetterPublishingRecovererFactory
通过添加以下内容替换标准 bean recovererCustomizer
:
@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;
}
从 2.8.4 版本开始,如果你想添加自定义 headers(除了工厂添加的重试信息 headers,你可以添加一个headersFunction
到工厂 -factory.setHeadersFunction((rec, ex) → { … })
4.4.5。结合阻塞和非阻塞重试
从 2.8.4 开始,您可以将框架配置为同时使用阻塞和非阻塞重试。例如,您可以有一组异常,这些异常也可能会在下一条记录上触发错误,例如DatabaseAccessException
,因此您可以在将同一记录发送到重试主题之前重试几次,或直接发送到 DLT。
addRetryableExceptions
要配置阻塞重试,您只需要通过 bean 中的方法添加要重试的异常ListenerContainerFactoryConfigurer
,如下所示。默认策略是FixedBackOff
,重试九次并且它们之间没有延迟。或者,您可以提供自己的退避政策。
@Bean(name = RetryTopicInternalBeanNames.LISTENER_CONTAINER_FACTORY_CONFIGURER_NAME)
public ListenerContainerFactoryConfigurer lcfc(KafkaConsumerBackoffManager kafkaConsumerBackoffManager,
DeadLetterPublishingRecovererFactory deadLetterPublishingRecovererFactory,
@Qualifier(RetryTopicInternalBeanNames
.INTERNAL_BACKOFF_CLOCK_BEAN_NAME) Clock clock) {
ListenerContainerFactoryConfigurer lcfc = new ListenerContainerFactoryConfigurer(kafkaConsumerBackoffManager, deadLetterPublishingRecovererFactory, clock);
lcfc.setBlockingRetryableExceptions(MyBlockingRetryException.class, MyOtherBlockingRetryException.class);
lcfc.setBlockingRetriesBackOff(new FixedBackOff(500, 5)); // Optional
return lcfc;
}
如果需要进一步调优异常分类,可以Map
通过方法设置自己的分类ListenerContainerFactoryConfigurer.setErrorHandlerCustomizer()
,如:
lcfc.setErrorHandlerCustomizer(ceh -> ((DefaultErrorHandler) ceh).setClassifications(myClassificationsMap, myDefaultValue));
结合全局可重试主题的致命异常分类,您可以为您想要的任何行为配置框架,例如让某些异常同时触发阻塞和非阻塞重试,仅触发一种或另一种,或者直接进入DLT 无需任何形式的重试。 |
这是两个配置一起工作的示例:
@Bean(name = RetryTopicInternalBeanNames.LISTENER_CONTAINER_FACTORY_CONFIGURER_NAME)
public ListenerContainerFactoryConfigurer lcfc(KafkaConsumerBackoffManager kafkaConsumerBackoffManager,
DeadLetterPublishingRecovererFactory deadLetterPublishingRecovererFactory,
@Qualifier(RetryTopicInternalBeanNames
.INTERNAL_BACKOFF_CLOCK_BEAN_NAME) Clock clock) {
ListenerContainerFactoryConfigurer lcfc = new ListenerContainerFactoryConfigurer(kafkaConsumerBackoffManager, deadLetterPublishingRecovererFactory, clock);
lcfc.setBlockingRetryableExceptions(ShouldRetryOnlyBlockingException.class, ShouldRetryViaBothException.class);
return lcfc;
}
@Bean(name = RetryTopicInternalBeanNames.DESTINATION_TOPIC_CONTAINER_NAME)
public DefaultDestinationTopicResolver ddtr(ApplicationContext applicationContext,
@Qualifier(RetryTopicInternalBeanNames
.INTERNAL_BACKOFF_CLOCK_BEAN_NAME) Clock clock) {
DefaultDestinationTopicResolver ddtr = new DefaultDestinationTopicResolver(clock, applicationContext);
ddtr.addNotRetryableExceptions(ShouldRetryOnlyBlockingException.class, ShouldSkipBothRetriesException.class);
return ddtr;
}
在这个例子中:
-
ShouldRetryOnlyBlockingException.class
只会通过阻塞重试,如果所有重试都失败,将直接进入 DLT。 -
ShouldRetryViaBothException.class
将通过阻塞重试,如果所有阻塞重试失败将被转发到下一个重试主题以进行另一组尝试。 -
ShouldSkipBothRetriesException.class
永远不会以任何方式重试,如果第一次处理尝试失败,将直接进入 DLT。
请注意,阻止重试行为是允许列表 - 您添加您确实希望以这种方式重试的异常;而非阻塞重试分类是针对 FATAL 异常的,因此是拒绝列表 - 您添加了您不想进行非阻塞重试的异常,而是直接发送到 DLT。 |
非阻塞异常分类行为还取决于特定主题的配置。 |
4.4.6。主题命名
重试主题和 DLT 通过使用提供的或默认值作为主主题的后缀来命名,并附加该主题的延迟或索引。
例子:
“我的主题”→“我的主题重试-0”,“我的主题重试-1”,...,“我的主题-dlt”
“我的其他主题”→“我的主题-myRetrySuffix-1000”,“我的主题-myRetrySuffix-2000”,...,“我的主题-myDltSuffix”。
重试主题和 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<String, MyOtherPojo> 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<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.suffixTopicsWithIndexValues()
.create(template);
}
默认行为是以延迟值作为后缀,但具有多个主题的固定延迟配置除外,在这种情况下,主题以主题的索引为后缀。 |
自定义命名策略
更复杂的命名策略可以通过注册一个实现RetryTopicNamesProviderFactory
. 默认实现是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.7. DLT 策略
该框架提供了一些使用 DLT 的策略。您可以提供一种 DLT 处理方法,使用默认的日志记录方法,或者根本没有 DLT。您还可以选择如果 DLT 处理失败会发生什么。
Dlt处理方法
您可以指定用于处理该主题的 Dlt 的方法,以及该处理失败时的行为。
为此,您可以@DltHandler
在带有注释的类的方法中使用@RetryableTopic
注释。@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(String, String) 方法提供,将应处理 DLT 消息的 bean 名称和方法名称作为参数传递。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> 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 处理失败,有两种可能的行为可用: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<Integer, MyPojo> 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上的方法向该列表中添加和删除异常。
有关详细信息,请参阅异常分类器。
配置无 DLT
该框架还提供了不为主题配置 DLT 的可能性。在这种情况下,在重试用尽之后,处理简单地结束。
@RetryableTopic(dltProcessingFailureStrategy =
DltStrategy.NO_DLT)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.doNotConfigureDlt()
.create(template);
}
4.4.8。指定 ListenerContainerFactory
默认情况下,RetryTopic 配置将使用@KafkaListener
注解中提供的工厂,但您可以指定一个不同的工厂用于创建重试主题和 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<Integer, MyPojo> template,
ConcurrentKafkaListenerContainerFactory<Integer, MyPojo> factory) {
return RetryTopicConfigurationBuilder
.newInstance()
.listenerFactory(factory)
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.listenerFactory("my-retry-topic-factory")
.create(template);
}
从 2.8.3 开始,您可以对可重试和不可重试主题使用相同的工厂。 |
如果需要将出厂配置行为恢复到 2.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;
}
4.4.9。更改 KafkaBackOffException 日志记录级别
当重试主题中的消息未到期时,KafkaBackOffException
会抛出 a。此类异常默认记录在级别,但您可以通过在类DEBUG
中设置错误处理程序定制器来更改此行为。ListenerContainerFactoryConfigurer
@Configuration
例如,要将日志记录级别更改为 WARN,您可以添加:
@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;
}
5. 提示、技巧和示例
5.1。手动分配所有分区
假设您希望始终从所有分区读取所有记录(例如使用压缩主题加载分布式缓存时),手动分配分区而不使用 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<String, String> consumerFactory) {
return new PartitionFinder(consumerFactory);
}
public static class PartitionFinder {
private final ConsumerFactory<String, String> consumerFactory;
public PartitionFinder(ConsumerFactory<String, String> consumerFactory) {
this.consumerFactory = consumerFactory;
}
public String[] partitions(String topic) {
try (Consumer<String, String> 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 版本开始,如上所示,您可以将初始偏移量应用于所有分区;有关详细信息,请参阅显式分区分配。
5.2. Kafka 事务与其他事务管理器的示例
以下 Spring Boot 应用程序是链接数据库和 Kafka 事务的示例。listener 容器启动 Kafka 事务,@Transactional
注解启动 DB 事务。DB事务首先提交;如果 Kafka 事务提交失败,记录将被重新传递,因此数据库更新应该是幂等的。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public ApplicationRunner runner(KafkaTemplate<String, String> 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<String, String> kafkaTemplate;
public Listener(JdbcTemplate jdbcTemplate, KafkaTemplate<String, String> 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
其事务与数据库事务同步,并且提交/回滚发生在数据库之后。
如果您希望先提交 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());
}
5.3. 自定义 JsonSerializer 和 JsonDeserializer
序列化器和反序列化器支持许多使用属性的自定义,有关更多信息,请参阅JSON。代码而kafka-clients
不是 Spring 实例化这些对象,除非您将它们直接注入消费者和生产者工厂。如果您希望使用属性配置(反)序列化器,但希望使用 custom ObjectMapper
,只需创建一个子类并将自定义映射器传递给super
构造函数。例如:
public class CustomJsonSerializer extends JsonSerializer<Object> {
public CustomJsonSerializer() {
super(customizedObjectMapper());
}
private static ObjectMapper customizedObjectMapper() {
ObjectMapper mapper = JacksonUtils.enhancedObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
附录 A:覆盖 Spring Boot 依赖项
在 Spring Boot 应用程序中使用 Spring for Apache Kafka 时,Apache Kafka 依赖项版本由 Spring Boot 的依赖项管理确定。如果您希望使用不同版本的kafka-clients
orkafka-streams
并使用嵌入式 kafka 代理进行测试,则需要覆盖 Spring Boot 依赖管理使用的版本并test
为 Apache Kafka 添加两个工件。
<properties>
<kafka.version>3.1.0</kafka.version>
</properties>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- optional - only needed when using kafka-streams -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<classifier>test</classifier>
<scope>test</scope>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.13</artifactId>
<classifier>test</classifier>
<scope>test</scope>
<version>${kafka.version}</version>
</dependency>
仅当您在测试中使用嵌入式 Kafka 代理时才需要测试范围依赖项。
附录 B:变更历史
B.1。2.6 和 2.7 之间的变化
B.1.1。卡夫卡客户端版本
此版本需要 2.7.0 kafka-clients
。从 2.7.1 版开始,它也与 2.8.0 客户端兼容;请参阅覆盖 Spring Boot 依赖项。
B.1.2。使用主题的非阻塞延迟重试
此版本中添加了这一重要的新功能。当严格排序不重要时,可以将失败的交付发送到另一个主题以供以后使用。可以配置一系列这样的重试主题,并增加延迟。有关详细信息,请参阅非阻塞重试。
B.1.3。侦听器容器更改
onlyLogRecordMetadata
容器属性现在true
是默认的。
一个新的容器属性stopImmediate
现在可用。
有关详细信息,请参阅侦听器容器属性。
使用BackOff
between 传递尝试(例如SeekToCurrentErrorHandler
和DefaultAfterRollbackProcessor
)的错误处理程序现在将在容器停止后立即退出回退间隔,而不是延迟停止。有关详细信息,请参阅回滚后处理器和[seek-to-current]。
错误处理程序和扩展后的回滚处理器FailedRecordProcessor
现在可以配置一个或多个RetryListener
s 以接收有关重试和恢复进度的信息。
有关详细信息,请参阅回滚后处理器、[seek-to-current]和[recovering-batch-eh]。
now在RecordInterceptor
侦听器返回后(通常或通过抛出异常)调用了其他方法。它还有一个子界面ConsumerAwareRecordInterceptor
。此外,现在还有一个BatchInterceptor
用于批处理侦听器。有关详细信息,请参阅消息侦听器容器。
B.1.4。@KafkaListener
变化
您现在可以验证@KafkaHandler
方法(类级侦听器)的有效负载参数。有关详细信息,请参阅@KafkaListener
@Payload
验证。
您现在可以在 and 上设置属性,rawRecordHeader
这会导致将 raw添加到转换后的. 这很有用,例如,如果您希望在侦听器错误处理程序中使用 a。有关详细信息,请参阅侦听器错误处理程序。MessagingMessageConverter
BatchMessagingMessageConverter
ConsumerRecord
Message<?>
DeadLetterPublishingRecoverer
您现在可以@KafkaListener
在应用程序初始化期间修改注释。有关详细信息,请参阅@KafkaListener
属性修改。
B.1.5。DeadLetterPublishingRecover
变化
现在,如果键和值都未能反序列化,则原始值将发布到 DLT。以前,值已填充,但键DeserializationException
仍保留在标题中。如果您将恢复器子类化并覆盖该createProducerRecord
方法,则会发生重大的 API 更改。
此外,recoverer 会在发布到它之前验证目标解析器选择的分区是否确实存在。
有关详细信息,请参阅发布死信记录。
B.1.6。ChainedKafkaTransactionManager
已弃用
有关更多信息,请参阅事务。
B.1.7。ReplyingKafkaTemplate
变化
现在有一种机制可以检查回复并在某些条件存在时异常地使未来失败。
添加了对发送和接收spring-messaging
Message<?>
的支持。
有关详细信息,请参阅使用ReplyingKafkaTemplate
。
B.1.8。卡夫卡流变化
默认情况下,StreamsBuilderFactoryBean
现在配置为不清理本地状态。有关详细信息,请参阅配置。
B.1.9。KafkaAdmin
变化
新方法createOrModifyTopics
并describeTopics
已添加。
KafkaAdmin.NewTopics
已添加以方便在单个 bean 中配置多个主题。有关更多信息,请参阅配置主题。
B.1.10。MessageConverter
变化
现在可以在 中添加一个spring-messaging
SmartMessageConverter
,MessagingMessageConverter
允许基于contentType
标头进行内容协商。有关更多信息,请参阅Spring Messaging 消息转换。
B.1.11。测序@KafkaListener
_
有关详细信息,请参阅按顺序启动s。@KafkaListener
B.1.12。ExponentialBackOffWithMaxRetries
提供了一个新的BackOff
实现,可以更方便地配置最大重试次数。有关详细信息,请参阅ExponentialBackOffWithMaxRetries
实施。
B.1.13。条件委托错误处理程序
这些新的错误处理程序可以配置为委托给不同的错误处理程序,具体取决于异常类型。有关详细信息,请参阅委派错误处理程序。
B.2。2.5 和 2.6 之间的变化
B.2.2。侦听器容器更改
默认EOSMode
值为现在BETA
。有关更多信息,请参阅Exactly Once 语义。
各种错误处理程序(扩展FailedRecordProcessor
)和DefaultAfterRollbackProcessor
现在重置BackOff
如果恢复失败。此外,您现在可以BackOff
根据失败的记录和/或异常选择要使用的。有关详细信息,请参阅[seek-to-current]、[recovering-batch-eh]、发布死信记录和回滚后处理器。
您现在可以adviceChain
在容器属性中配置一个。有关详细信息,请参阅侦听器容器属性。
当容器配置为发布时ListenerContainerIdleEvent
,它现在ListenerContainerNoLongerIdleEvent
在发布空闲事件后收到记录时发布。有关更多信息,请参阅应用程序事件和检测空闲和非响应消费者。
B.2.3。@KafkaListener 更改
使用手动分区分配时,您现在可以指定通配符来确定哪些分区应重置为初始偏移量。另外,如果监听器实现了ConsumerSeekAware
,onPartitionsAssigned()
在手动赋值之后调用。(也在 2.5.5 版中添加)。有关详细信息,请参阅显式分区分配。
添加了方便的方法AbstractConsumerSeekAware
以使搜索更容易。有关详细信息,请参阅寻求特定偏移量。
B.2.4。错误处理程序更改
如果异常的类型与先前使用此记录发生的异常类型不同,则(例如 、 )的子类现在可以配置为重置重FailedRecordProcessor
试SeekToCurrentErrorHandler
状态。有关详细信息,请参阅[seek-to-current]、After-rollback Processor、[recovering-batch-eh]。DefaultAfterRollbackProcessor
RecoveringBatchErrorHandler
B.2.5。生产者工厂变更
您现在可以为生产者设置最大年龄,在此之后它们将被关闭并重新创建。有关更多信息,请参阅事务。
您现在可以DefaultKafkaProducerFactory
在创建后更新配置映射。这可能很有用,例如,如果您必须在凭据更改后更新 SSL 密钥/信任存储位置。有关详细信息,请参阅使用DefaultKafkaProducerFactory
。
B.3。2.4 和 2.5 之间的变化
本节介绍从 2.4 版到 2.5 版所做的更改。有关早期版本的更改,请参阅更改历史记录。
B.3.1。消费者/生产者工厂变更
默认的消费者和生产者工厂现在可以在创建或关闭消费者或生产者时调用回调。提供了原生千分尺度量的实现。有关详细信息,请参阅工厂侦听器。
您现在可以在运行时更改引导服务器属性,从而启用故障转移到另一个 Kafka 集群。有关更多信息,请参阅连接到 Kafka。
B.3.2。StreamsBuilderFactoryBean
变化
KafkaStreams
工厂 bean 现在可以在创建或销毁时调用回调。提供了原生千分尺度量的实现。有关更多信息,请参阅KafkaStreams Micrometer 支持。
B.3.5。传递尝试标题
现在可以选择添加一个标头,该标头在使用某些错误处理程序时和回滚处理器后跟踪传递尝试。有关详细信息,请参阅传递尝试标头。
B.3.6。@KafkaListener 更改
@KafkaListener
当返回类型为时,如果需要,默认回复标头现在将自动填充Message<?>
。有关详细信息,请参阅回复类型消息<?>。
当传入记录有键时,KafkaHeaders.RECEIVED_MESSAGE_KEY
不再填充值;标题被完全省略。null
null
@KafkaListener
方法现在可以指定一个ConsumerRecordMetadata
参数,而不是对元数据(如主题、分区等)使用离散的标头。有关更多信息,请参阅消费者记录元数据。
B.3.7。侦听器容器更改
assignmentCommitOption
容器属性现在LATEST_ONLY_NO_TX
是默认的。有关详细信息,请参阅侦听器容器属性。
使用事务时,默认情况下subBatchPerPartition
容器属性true
。有关更多信息,请参阅事务。
RecoveringBatchErrorHandler
现在提供一个新的。有关详细信息,请参阅[recovering-batch-eh]。
现在支持静态组成员身份。有关详细信息,请参阅消息侦听器容器。
当配置了增量/协作重新平衡时,如果偏移量以 non-fatal 提交失败RebalanceInProgressException
,容器将尝试在重新平衡完成后重新提交仍分配给该实例的分区的偏移量。
默认错误处理程序现在是SeekToCurrentErrorHandler
记录侦听RecoveringBatchErrorHandler
器和批处理侦听器。有关详细信息,请参阅容器错误处理程序。
您现在可以控制记录标准错误处理程序有意抛出的异常的级别。有关详细信息,请参阅容器错误处理程序。
添加了该getAssignmentsByClientId()
方法,可以更轻松地确定并发容器中的哪些消费者被分配了哪些分区。有关详细信息,请参阅侦听器容器属性。
您现在可以禁止ConsumerRecord
在错误、调试日志等onlyLogRecordMetadata
中记录整个 s。请参阅Listener Container Properties。
B.3.8。Kafka模板更改
现在KafkaTemplate
可以维护千分尺计时器。有关详细信息,请参阅监控。
现在KafkaTemplate
可以配置ProducerConfig
属性以覆盖生产者工厂中的属性。有关详细信息,请参阅使用KafkaTemplate
。
现在RoutingKafkaTemplate
已经提供了一个。有关详细信息,请参阅使用RoutingKafkaTemplate
。
您现在可以使用KafkaSendCallback
而不是ListenerFutureCallback
获得更窄的异常,从而更容易提取失败的ProducerRecord
. 有关详细信息,请参阅使用KafkaTemplate
。
B.3.9。Kafka 字符串序列化器/反序列化器
现在提供了新的ToStringSerializer
/ StringDeserializer
s 以及关联的。SerDe
有关详细信息,请参阅字符串序列化。
B.3.10。JsonDeserializer
现在JsonDeserializer
可以更灵活地确定反序列化类型。有关详细信息,请参阅使用方法确定类型。
B.3.11。委派串行器/解串器
当DelegatingSerializer
出站记录没有标题时,现在可以处理“标准”类型。有关更多信息,请参阅委托序列化器和反序列化器。
B.3.12。测试更改
KafkaTestUtils.consumerProps()
助手记录现在默认设置ConsumerConfig.AUTO_OFFSET_RESET_CONFIG
为earliest
。有关更多信息,请参阅JUnit。
B.4。2.3 和 2.4 之间的变化
B.4.2。ConsumerAwareRebalanceListener
就像ConsumerRebalanceListener
,这个接口现在多了一个方法onPartitionsLost
。有关更多信息,请参阅 Apache Kafka 文档。
与 不同的是ConsumerRebalanceListener
,默认实现不调用onPartitionsRevoked
. 相反,侦听器容器将在调用该方法后调用该方法onPartitionsLost
;因此,在实施ConsumerAwareRebalanceListener
.
有关详细信息,请参阅重新平衡侦听器末尾的重要说明。
B.4.4。卡夫卡模板
现在KafkaTemplate
支持非事务性发布和事务性发布。有关详细信息,请参阅KafkaTemplate
事务性和非事务性发布。
B.4.5。聚合回复Kafka模板
现在releaseStrategy
是一个BiConsumer
. 现在在超时后(以及记录到达时)调用它;第二个参数是true
在超时后调用的情况。
有关详细信息,请参阅聚合多个回复。
B.4.6。侦听器容器
提供ContainerProperties
了一个authorizationExceptionRetryInterval
选项,让侦听器容器AuthorizationException
在KafkaConsumer
. 有关更多信息,请参阅其 JavaDocs 和UsingKafkaMessageListenerContainer
。
B.4.7. @KafkaListener
@KafkaListener
注释有一个新属性splitIterables
;默认为真。当回复侦听器返回Iterable
this 属性时,控制返回结果是作为单个记录发送还是作为每个元素的记录发送。有关更多信息,请参阅使用转发侦听器结果@SendTo
现在可以使用BatchToRecordAdapter
;配置批处理侦听器 例如,这允许在事务中处理批处理,而侦听器一次获取一条记录。使用默认实现, aConsumerRecordRecoverer
可用于处理批处理中的错误,而无需停止整个批处理的处理 - 这在使用事务时可能很有用。有关更多信息,请参阅使用批处理侦听器的事务。
B.4.8. 卡夫卡流
StreamsBuilderFactoryBean
接受一个新属性KafkaStreamsInfrastructureCustomizer
。这允许在创建流之前配置构建器和/或拓扑。有关更多信息,请参阅Spring 管理。
B.5。2.2 和 2.3 之间的变化
本节介绍从 2.2 版到 2.3 版所做的更改。
B.5.1。提示、技巧和示例
添加了新章节提示、技巧和示例。请提交 GitHub 问题和/或拉取请求以获取该章中的其他条目。
B.5.4. 配置更改
从版本 2.3.4 开始,missingTopicsFatal
容器属性默认为 false。如果是这样,如果代理关闭,应用程序将无法启动;许多用户受到此更改的影响;鉴于 Kafka 是一个高可用性平台,我们没有预料到启动一个没有活动代理的应用程序会是一个常见的用例。
B.5.5。生产者和消费者工厂的变化
现在DefaultKafkaProducerFactory
可以配置为每个线程创建一个生产者。您还可以Supplier<Serializer>
在构造函数中提供实例作为配置类(需要无参数构造函数)或使用Serializer
实例构造的替代方案,然后在所有生产者之间共享。有关详细信息,请参阅使用DefaultKafkaProducerFactory
。
相同的选项可Supplier<Deserializer>
用于DefaultKafkaConsumerFactory
. 有关详细信息,请参阅使用KafkaMessageListenerContainer
。
B.5.6. 侦听器容器更改
以前,当使用侦听器适配器(例如s)调用侦听器时,会收到错误处理程序ListenerExecutionFailedException
(实际侦听器异常为)。native 抛出的异常被原封不动地传递给错误处理程序。现在 a始终是参数(实际的侦听器异常为),它提供对容器属性的访问。cause
@KafkaListener
GenericMessageListener
ListenerExecutionFailedException
cause
group.id
因为侦听器容器有自己的提交偏移量的机制,所以它更喜欢 KafkaConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG
为false
. 它现在自动将其设置为 false ,除非在消费者工厂中特别设置或容器的消费者属性覆盖。
该ackOnError
属性现在false
是默认的。有关详细信息,请参阅[寻求当前]。
现在可以group.id
在 listener 方法中获取消费者的属性。有关更多信息,请参阅获取消费者group.id
。
容器有一个新属性recordInterceptor
,允许在调用侦听器之前检查或修改记录。还提供了ACompositeRecordInterceptor
以防您需要调用多个拦截器。有关详细信息,请参阅消息侦听器容器。
有新的ConsumerSeekAware
方法允许您相对于开始、结束或当前位置执行查找,并查找大于或等于时间戳的第一个偏移量。有关详细信息,请参阅寻求特定偏移量。
AbstractConsumerSeekAware
现在提供了一个便利类来简化搜索。有关详细信息,请参阅寻求特定偏移量。
提供ContainerProperties
了一个idleBetweenPolls
选项,让侦听器容器中的主循环在KafkaConsumer.poll()
调用之间休眠。有关更多信息,请参阅其 JavaDocs 和UsingKafkaMessageListenerContainer
。
使用AckMode.MANUAL
(或)时,MANUAL_IMMEDIATE
您现在可以通过调用nack
. 有关更多信息,Acknowledgment
请参阅提交偏移量。
现在可以使用 Micrometer 监控监听器性能Timer
。有关详细信息,请参阅监控。
容器现在发布与启动相关的其他消费者生命周期事件。有关详细信息,请参阅应用程序事件。
事务批处理侦听器现在可以支持僵尸防护。有关更多信息,请参阅事务。
侦听器容器工厂现在可以配置为ContainerCustomizer
在创建和配置每个容器后进一步配置它。有关详细信息,请参阅容器工厂。
B.5.7. 错误处理程序更改
现在SeekToCurrentErrorHandler
将某些异常视为致命异常并禁用这些异常的重试,在第一次失败时调用恢复器。
SeekToCurrentErrorHandler
和现在SeekToCurrentBatchErrorHandler
可以配置为BackOff
在传递尝试之间应用(线程睡眠)。
从版本 2.3.2 开始,当错误处理程序在恢复失败记录后返回时,将提交恢复记录的偏移量。
有关详细信息,请参阅[寻求当前]。
DeadLetterPublishingRecoverer
与 结合使用时,ErrorHandlingDeserializer
现在将发送到死信主题的消息的有效负载设置为无法反序列化的原始值。以前,它是null
和用户代码需要DeserializationException
从消息头中提取的。有关详细信息,请参阅发布死信记录。
B.5.8. 主题生成器
提供了一个新类TopicBuilder
,以便更方便地创建NewTopic
@Bean
用于自动主题供应的 s。有关更多信息,请参阅配置主题。
B.5.9. 卡夫卡流变化
您现在可以执行StreamsBuilderFactoryBean
创建者的附加配置@EnableKafkaStreams
。有关详细信息,请参阅流配置。
现在提供了A RecoveringDeserializationExceptionHandler
,它允许恢复具有反序列化错误的记录。它可以与 a 结合使用DeadLetterPublishingRecoverer
,将这些记录发送到死信主题。有关详细信息,请参阅从反序列化异常中恢复。
已HeaderEnricher
提供转换器,使用 SpEL 生成标头值。有关详细信息,请参阅Header Enricher。
已MessagingTransformer
提供。这允许 Kafka 流拓扑与 spring-messaging 组件交互,例如 Spring Integration 流。有关更多信息MessagingTransformer
,请参阅[Calling a Spring Integration Flow from aKStream
]。
B.5.10。JSON 组件更改
现在所有支持 JSON 的组件都默认配置ObjectMapper
为由JacksonUtils.enhancedObjectMapper()
. 现在JsonDeserializer
提供TypeReference
基于 - 的构造函数以更好地处理目标通用容器类型。还JacksonMimeTypeModule
引入了 a 用于org.springframework.util.MimeType
将纯字符串序列化。有关更多信息,请参阅其 JavaDocs 和序列化、反序列化和消息转换。
已经为所有 Json 转换器提供了AByteArrayJsonMessageConverter
以及一个新的超类,JsonMessageConverter
. 此外,aStringOrBytesSerializer
现在可用;它可以序列化byte[]
,Bytes
和sString
中的值ProducerRecord
。有关更多信息,请参阅Spring Messaging 消息转换。
JsonSerializer
和JsonDeserializer
现在具有流畅的JsonSerde
API 以使编程配置更简单。有关更多信息,请参阅 javadocs、序列化、反序列化和消息转换,以及Streams JSON 序列化和反序列化。
B.5.11。回复Kafka模板
当回复超时时,future 会异常完成,用 aKafkaReplyTimeoutException
而不是 a KafkaException
。
此外,现在提供了一种重载sendAndReceive
方法,允许基于每条消息指定回复超时。
B.5.12。聚合回复Kafka模板
ReplyingKafkaTemplate
通过聚合来自多个接收者的回复来扩展。有关详细信息,请参阅聚合多个回复。
B.5.13。交易变更
您现在可以覆盖 和 上的生产者transactionIdPrefix
工厂。有关更多信息,请参阅。KafkaTemplate
KafkaTransactionManager
transactionIdPrefix
B.5.14。新的委托序列化器/反序列化器
该框架现在提供了一个委托Serializer
和Deserializer
,利用标头来生成和使用具有多种键/值类型的记录。有关更多信息,请参阅委托序列化器和反序列化器。
B.5.15。新的重试反序列化器
该框架现在提供了一个委托RetryingDeserializer
,以在可能发生网络问题等暂时错误时重试序列化。有关详细信息,请参阅重试反序列化程序。
B.6。2.1 和 2.2 之间的变化
B.6.2. 类和包更改
课程ContainerProperties
已从org.springframework.kafka.listener.config
移至。org.springframework.kafka.listener
枚举AckMode
已从AbstractMessageListenerContainer
移至。ContainerProperties
和方法已从两者移至setBatchErrorHandler()
and 。setErrorHandler()
ContainerProperties
AbstractMessageListenerContainer
AbstractKafkaListenerContainerFactory
B.6.3. 回滚处理后
提供了一种新的AfterRollbackProcessor
策略。有关详细信息,请参阅回滚后处理器。
B.6.4. ConcurrentKafkaListenerContainerFactory
变化
您现在可以使用ConcurrentKafkaListenerContainerFactory
来创建和配置任何ConcurrentMessageListenerContainer
,而不仅仅是那些用于@KafkaListener
注释的。有关详细信息,请参阅容器工厂。
B.6.5。侦听器容器更改
添加了一个新的容器属性 ( missingTopicsFatal
)。有关详细信息,请参阅使用KafkaMessageListenerContainer
。
AConsumerStoppedEvent
现在在消费者停止时发出。有关详细信息,请参阅线程安全。
批处理侦听器可以选择接收完整ConsumerRecords<?, ?>
对象而不是List<ConsumerRecord<?, ?>
. 有关详细信息,请参阅批处理侦听器。
DefaultAfterRollbackProcessor
和现在SeekToCurrentErrorHandler
可以恢复(跳过)一直失败的记录,并且默认情况下,在 10 次失败后恢复。它们可以配置为将失败的记录发布到死信主题。
从 2.2.4 版本开始,可以在选择死信主题名称时使用消费者的组 ID。
有关详细信息,请参阅回滚后处理器、[seek-to-current]和发布死信记录。
已ConsumerStoppingEvent
添加。有关详细信息,请参阅应用程序事件。
现在SeekToCurrentErrorHandler
可以将其配置为在配置容器时提交已恢复记录的偏移量AckMode.MANUAL_IMMEDIATE
(自 2.2.4 起)。有关详细信息,请参阅[寻求当前]。
B.6.6。@KafkaListener 更改
您现在可以通过在注解上设置属性来覆盖侦听器容器工厂的concurrency
和属性。autoStartup
您现在可以添加配置以确定将哪些标头(如果有)复制到回复消息。有关详细信息,请参阅@KafkaListener
注解。
您现在可以@KafkaListener
将自己的注释用作元注释。有关更多信息,请参阅@KafkaListener
元注释。
现在更容易配置Validator
验证@Payload
。有关详细信息,请参阅@KafkaListener
@Payload
验证。
您现在可以直接在注解上指定 kafka 消费者属性;这些将覆盖消费者工厂中定义的任何具有相同名称的属性(从版本 2.2.4 开始)。有关详细信息,请参阅注解属性。
B.6.7. 标头映射更改
类型MimeType
和MediaType
的标头现在映射为RecordHeader
值中的简单字符串。以前,它们被映射为 JSON 并且仅MimeType
被解码。
MediaType
无法解码。它们现在是用于互操作性的简单字符串。
此外,DefaultKafkaHeaderMapper
还有一个新方法,允许使用而不是 JSONaddToStringClasses
来指定应该映射的类型。toString()
有关详细信息,请参阅消息标头。
B.6.8. 嵌入式 Kafka 更改
该类KafkaEmbedded
及其KafkaRule
接口已被弃用,取而代之的是JUnit 4 包装器EmbeddedKafkaBroker
及其 JUnit 4EmbeddedKafkaRule
包装器。注释现在@EmbeddedKafka
填充一个EmbeddedKafkaBroker
bean 而不是 deprecated KafkaEmbedded
。此更改允许@EmbeddedKafka
在 JUnit 5 中使用测试。注释现在@EmbeddedKafka
具有ports
指定填充EmbeddedKafkaBroker
. 有关详细信息,请参阅测试应用程序。
B.6.9。JsonSerializer/Deserializer 增强
您现在可以使用生产者和消费者属性提供类型映射信息。
反序列化器上提供了新的构造函数,以允许使用提供的目标类型覆盖类型标头信息。
现在JsonDeserializer
默认删除任何类型信息标题。
您现在可以JsonDeserializer
使用 Kafka 属性(自 2.2.3 起)将其配置为忽略类型信息标头。
有关详细信息,请参阅序列化、反序列化和消息转换。
B.6.10。卡夫卡流变化
流配置 bean 现在必须是KafkaStreamsConfiguration
对象而不是StreamsConfig
对象。
StreamsBuilderFactoryBean
已从package…core
移至.…config
当条件分支建立在实例KafkaStreamBrancher
之上时,已经引入了更好的最终用户体验。KStream
有关更多信息,请参阅Apache Kafka Streams 支持和配置。
B.6.11。事务 ID
当侦听器容器启动事务时,transactional.id
现在transactionIdPrefix
附加<group.id>.<topic>.<partition>
. 如此处所述,此更改允许对僵尸进行适当的防护。
B.7. 2.0 和 2.1 之间的变化
B.7.2. JSON 改进
现在添加类型信息StringJsonMessageConverter
,让转换器在接收时根据消息本身而不是固定配置类型创建特定类型。有关详细信息,请参阅序列化、反序列化和消息转换。JsonSerializer
Headers
JsonDeserializer
B.7.3. 容器停止错误处理程序
现在为记录和批处理侦听器提供容器错误处理程序,将侦听器抛出的任何异常视为致命/它们停止容器。有关详细信息,请参阅处理异常。
B.7.4. 暂停和恢复容器
侦听器容器现在具有pause()
和resume()
方法(从 2.1.3 版开始)。有关详细信息,请参阅暂停和恢复侦听器容器。
B.7.5。有状态重试
从版本 2.1.3 开始,您可以配置有状态重试。有关详细信息,请参阅有状态重试。
B.7.6. 客户编号
从版本 2.1.1 开始,您现在可以将client.id
前缀设置为@KafkaListener
. 以前,要自定义客户端 ID,您需要每个侦听器有一个单独的消费者工厂(和容器工厂)。当您使用并发时,前缀带有后缀-n
以提供唯一的客户端 ID。
B.7.7. 记录偏移提交
默认情况下,主题偏移提交的日志记录是使用DEBUG
日志记录级别执行的。从版本 2.1.2 开始,ContainerProperties
调用的新属性commitLogLevel
允许您指定这些消息的日志级别。有关详细信息,请参阅使用KafkaMessageListenerContainer
。
B.7.8. 默认@KafkaHandler
从版本 2.1.3 开始,您可以将@KafkaHandler
类级别的注释之一指定@KafkaListener
为默认值。有关更多信息,请参阅@KafkaListener
类。
B.7.9. 回复Kafka模板
从版本 2.1.3 开始,KafkaTemplate
提供了一个子类来支持请求/回复语义。有关详细信息,请参阅使用ReplyingKafkaTemplate
。
B.7.11。从 2.0 迁移指南
请参阅2.0 到 2.1 迁移指南。
B.8. 1.3 和 2.0 之间的变化
B.8.3. 消息监听器
消息侦听器现在可以知道该Consumer
对象。有关详细信息,请参阅消息侦听器。
B.8.4. 使用ConsumerAwareRebalanceListener
重新平衡侦听器现在可以Consumer
在重新平衡通知期间访问该对象。有关详细信息,请参阅重新平衡侦听器。
B.9。1.2 和 1.3 之间的变化
B.9.1。交易支持
0.11.0.0 客户端库增加了对事务的支持。添加了对事务的KafkaTransactionManager
支持和其他支持。有关更多信息,请参阅事务。
B.9.2. 对标头的支持
0.11.0.0 客户端库增加了对消息头的支持。这些现在可以映射到和从spring-messaging
MessageHeaders
. 有关详细信息,请参阅消息标头。
B.9.4。支持 Kafka 时间戳
KafkaTemplate
现在支持 API 添加带有时间戳的记录。KafkaHeaders
已引入有关timestamp
支持的新内容。此外,还添加了新的KafkaConditions.timestamp()
和KafkaMatchers.hasTimestamp()
测试实用程序。有关更多详细信息,请参阅使用KafkaTemplate
、@KafkaListener
注释和测试应用程序。
B.9.5。@KafkaListener
变化
您现在可以配置 aKafkaListenerErrorHandler
来处理异常。有关详细信息,请参阅处理异常。
默认情况下,该@KafkaListener
id
属性现在用作group.id
属性,覆盖消费者工厂中配置的属性(如果存在)。此外,您可以显式配置groupId
注释上的。以前,您需要一个单独的容器工厂(和消费者工厂)来group.id
为侦听器使用不同的值。要恢复以前使用出厂配置的行为,请将注释上group.id
的属性设置为。idIsGroup
false
B.9.6。@EmbeddedKafka
注解
为方便起见,提供了一个测试类级别的@EmbeddedKafka
注释,用于注册KafkaEmbedded
为 bean。有关详细信息,请参阅测试应用程序。
B.9.7. Kerberos 配置
现在提供了对配置 Kerberos 的支持。有关详细信息,请参阅JAAS 和 Kerberos。
B.11。1.0 和 1.1 之间的变化
B.11.5。寻找
您现在可以查找每个主题或分区的位置。当使用组管理并且Kafka分配分区时,您可以使用它来设置初始化期间的初始位置。您还可以在检测到空闲容器时或在应用程序执行中的任意点进行查找。有关详细信息,请参阅寻求特定偏移量。