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-clients3.0.0 及更高版本不再支持EOSMode.V2(aka BETA) (并自动回退到V1- aka ALPHA)与早于 2.5 的经纪人;因此,如果您的代理较旧(或升级您的代理) ,您必须覆盖默认值EOSMode( )。V2V1

有关详细信息,请参阅Exactly Once SemanticsKIP-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

@KafkaListener注释现在具有属性info;这用于填充新的侦听器容器属性listenerInfo。然后,这用于填充KafkaHeaders.LISTENER_INFO每个记录中的标题,该标题可用于RecordInterceptorRecordFilterStrategy或侦听器本身。有关详细信息,请参阅侦听器信息标头抽象侦听器容器属性

2.1.5。KafkaTemplate变化

给定主题、分区和偏移量,您现在可以接收一条记录。有关详细信息,请参阅用于KafkaTemplate接收

2.1.6。CommonErrorHandler添加

用于记录批处理侦听器的遗留GenericErrorHandler及其子接口层次结构已被新的单一接口所取代,其CommonErrorHandler实现对应于GenericErrorHandler. 有关详细信息,请参阅容器错误处理程序迁移自定义旧错误处理程序实现CommonErrorHandler

2.1.7。侦听器容器更改

interceptBeforeTx容器属性现在true是默认的。

除了以前的 s 之外,该authorizationExceptionRetryInterval属性已重命名为authExceptionRetryInterval并现在适用于s 。这两个异常都被认为是致命的,并且容器将默认停止,除非设置了此属性。AuthenticationExceptionAuthorizationException

有关详细信息,请参阅使用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-kafkajar 声明为项目中的依赖项。

Maven
Gradle
<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
  <version>2.8.7</version>
</dependency>
使用 Spring Boot 时(并且您还没有使用 start.spring.io 创建项目),省略版本,Boot 将自动引入与您的 Boot 版本兼容的正确版本:
Maven
Gradle
<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.1。兼容性

此快速浏览适用于以下版本:

  • Apache Kafka 客户端 3.0.x

  • Spring 框架 5.3.x

  • 最低 Java 版本:8

3.1.2。入门

最简单的入门方法是使用start.spring.io(或 Spring Tool Suits 和 Intellij IDEA 中的向导)并创建一个项目,选择“Spring for Apache Kafka”作为依赖项。有关其自以为是的基础设施 bean 自动配置的更多信息,请参阅Spring Boot 文档

这是一个最小的消费者应用程序。

Spring Boot 消费者应用程序
示例 1. 应用程序
java
科特林
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("topic1")
                .partitions(10)
                .replicas(1)
                .build();
    }

    @KafkaListener(id = "myId", topics = "topic1")
    public void listen(String in) {
        System.out.println(in);
    }

}
示例 2.application.properties
spring.kafka.consumer.auto-offset-reset=earliest

NewTopicbean 导致在代理上创建主题;如果主题已经存在,则不需要。

Spring Boot 生产者应用程序
示例 3. 应用程序
java
科特林
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("topic1")
                .partitions(10)
                .replicas(1)
                .build();
    }

    @Bean
    public ApplicationRunner runner(KafkaTemplate<String, String> template) {
        return args -> {
            template.send("topic1", "test");
        };
    }

}
使用 Java 配置(无 Spring Boot)
Spring for Apache Kafka 旨在用于 Spring 应用程序上下文。例如,如果您在 Spring 上下文之外自己创建侦听器容器,则除非您满足…​Aware容器实现的所有接口,否则并非所有功能都可以工作。

这是一个不使用 Spring Boot 的应用程序示例;它同时具有 aConsumerProducer.

示例 4. 没有引导
java
科特林
public class Sender {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
		context.getBean(Sender.class).send("test", 42);
	}

	private final KafkaTemplate<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()) 。KafkaListenerEndpointRegistrystop()start()

为方便起见,该框架还提供了一个ABSwitchCluster支持两组引导服务器;其中之一随时处于活动状态。通过调用配置ABSwitchCluster并将其添加到生产者和消费者工厂,以及. 当你想切换时,调用或调用生产者工厂建立新的连接;对于消费者和所有侦听器容器。使用s和bean时。KafkaAdminsetBootstrapServersSupplier()primary()secondary()reset()stop()start()@KafkaListenerstop()start()KafkaListenerEndpointRegistry

有关更多信息,请参阅 Javadocs。

工厂听众

从 2.5 版开始,DefaultKafkaProducerFactoryDefaultKafkaConsumerFactory可以配置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-idmetrics()beanName.

例如,这些侦听器可用于在创建KafkaClientMetrics新客户端时创建和绑定 Micrometer 实例(并在客户端关闭时关闭它)。

该框架提供的侦听器正是这样做的;见千分尺原生度量

4.1.2. 配置主题

如果您KafkaAdmin在应用程序上下文中定义一个 bean,它可以自动将主题添加到代理。为此,您可以NewTopic @Bean为每个主题添加一个应用程序上下文。2.3 版引入了一个新类TopicBuilder,使创建此类 bean 更加方便。以下示例显示了如何执行此操作:

java
科特林
@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

java
科特林
@Bean
public NewTopic topic4() {
    return TopicBuilder.name("defaultBoth")
            .build();
}

@Bean
public NewTopic topic5() {
    return TopicBuilder.name("defaultPart")
            .replicas(1)
            .build();
}

@Bean
public NewTopic topic6() {
    return TopicBuilder.name("defaultRepl")
            .partitions(3)
            .build();
}

从 2.7 版本开始,您可以NewTopic在单个KafkaAdmin.NewTopicsbean 定义中声明多个 s:

java
科特林
@Bean
public KafkaAdmin.NewTopics topics456() {
    return new NewTopics(
            TopicBuilder.name("defaultBoth")
                .build(),
            TopicBuilder.name("defaultPart")
                .replicas(1)
                .build(),
            TopicBuilder.name("defaultRepl")
                .partitions(3)
                .build());
}
使用 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

sendDefaultAPI 要求已向模板提供默认主题。

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

metricspartitionsFor方法委托给底层的相同方法Producer。该execute方法提供对底层的直接访问Producer

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

@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有两个属性 aProducerRecordRecordMetadata. 有关这些对象的信息,请参阅 Kafka API 文档。

ThrowableinonFailure可以转换为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 发送消息的示例:

示例 5. 非阻塞(异步)
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);
    }
}

请注意,导致的原因ExecutionExceptionKafkaProducerException属性有关failedProducerRecord

使用RoutingKafkaTemplate

从 2.5 版开始,您可以使用 aRoutingKafkaTemplate在运行时根据目标topic名称选择生产者。

路由模板不支持事务、、、executeflush操作,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());
        };
    }

}

此示例的相应@KafkaListeners 显示在Annotation Properties中。

有关实现类似结果的另一种技术,但具有将不同类型发送到同一主题的附加功能,请参阅委托序列化器和反序列化器

使用DefaultKafkaProducerFactory

如UsingKafkaTemplate中所见, aProducerFactory用于创建生产者。

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

When producerPerThreadis ,当不再需要生产者时true,用户代码必须调用工厂。closeThreadBoundProducer()这将物理关闭生产者并将其从ThreadLocal. 调用reset()destroy()不会清理这些生产者。

创建 a时,可以通过调用仅接受属性 Map 的构造函数从配置中获取DefaultKafkaProducerFactory键和/或值类(参见Using中的示例),或者可以将实例传递给构造函数(在这种情况下,所有s 共享相同的实例)。或者,您可以提供s(从 2.3 版开始),用于为每个 s 获取单独的实例:SerializerKafkaTemplateSerializerDefaultKafkaProducerFactoryProducerSupplier<Serializer>SerializerProducer

@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, 。DeserializationExceptioncause

从 2.6.7 版本开始,除了检测DeserializationExceptions 之外,模板还将调用该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;
    }

}

基础设施回显相关@KafkaListenerID 并确定回复主题。

有关发送回复的更多信息,请参阅转发侦听器结果使用@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。这可能对自动缩放有用,但会产生额外的网络流量开销以及丢弃每个不需要的回复的小成本。当您使用此设置时,我们建议您将模板设置sharedReplyTopictrue,这会降低对 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 个属性correlationHeaderNamereplyTopicHeaderNamereplyPartitionHeaderName. 如果您的服务器不是 Spring 应用程序(或不使用@KafkaListener.

请求/回复Message<?>s

2.7 版ReplyingKafkaTemplate向发送和接收spring-messagingMessage<?>抽象添加了方法:

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

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

这些将使用模板的默认值replyTimeout,也有可能在方法调用中超时的重载版本。

如果消费者Deserializer或模板MessageConverter可以通过配置或回复消息中的类型元数据在没有任何附加信息的情况下转换有效负载,则使用第一种方法。

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

示例 6. 模板 Bean
java
科特林
@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;
}
示例 7. 使用模板
java
科特林
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_IDand 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客户端的实现。

与 一样ReplyingKafkaTemplateAggregatingReplyingKafkaTemplate构造函数采用生产者工厂和侦听器容器来接收回复;它有第三个参数BiPredicate<List<ConsumerRecord<K, R>>, Boolean> releaseStrategy,每次收到回复时都会查询该参数;当谓词返回时true,使用s的集合ConsumerRecord来完成方法Future返回的sendAndReceive

还有一个附加属性returnPartialOnTimeout(默认为 false)。当它设置为 时true,不是用 a 完成未来,而是KafkaReplyTimeoutException部分结果正常完成未来(只要已收到至少一个回复记录)。

从版本 2.3.5 开始,谓词也会在超时后调用(如果returnPartialOnTimeouttrue)。第一个参数是当前记录列表;第二个是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,其值是ConsumerRecords 的集合。“外部”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";

中的实数ConsumerRecordsCollection包含收到回复的实际主题。

回复的侦听器容器必须配置为AckMode.MANUALAckMode.MANUAL_IMMEDIATE;消费者属性enable.auto.commit必须是false(自 2.3 版以来的默认值)。为了避免丢失消息的任何可能性,模板仅在有零个未完成的请求时提交偏移量,即当最后一个未完成的请求被释放策略释放时。重新平衡后,可能会出现重复的回复交付;对于任何正在进行的请求,这些都将被忽略;当收到已发布回复的重复回复时,您可能会看到错误日志消息。
如果你使用ErrorHandlingDeserializer这个聚合模板,框架将不会自动检测DeserializationExceptions。相反,记录(带有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.RECORDConsumer
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 ) 提供对. 例如,这可能用于访问拦截器中的消费者指标。ConsumerAwareRecordInterceptorBatchInterceptorConsumer<?, ?>

您不应执行任何影响消费者位置和/或在这些拦截器中提交的偏移量的方法;容器需要管理这些信息。
如果拦截器改变了记录(通过创建一个新记录),则topic,partitionoffset必须保持不变以避免意外的副作用,例如记录丢失。

CompositeRecordInterceptorand可CompositeBatchInterceptor用于调用多个拦截器。

默认情况下,从 2.8 版本开始,当使用事务时,拦截器在事务开始之前被调用。您可以将侦听器容器的interceptBeforeTx属性设置false为在事务开始后调用拦截器。

从版本 2.3.8、2.4.6 开始,当并发大于一时,ConcurrentMessageListenerContainer现在支持静态成员。group.instance.id后缀是-nn开始1。这与增加的 一起session.timeout.ms,可用于减少重新平衡事件,例如,当应用程序实例重新启动时。

使用KafkaMessageListenerContainer

以下构造函数可用:

public KafkaMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,
                    ContainerProperties containerProperties)

它在对象中接收ConsumerFactory有关主题和分区以及其他配置的信息ContainerPropertiesContainerProperties具有以下构造函数:

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可用。启用日志记录时trueINFO每个侦听器容器都会写入一条日志消息,总结其配置属性。

默认情况下,主题偏移提交的日志记录在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这会导致容器在AuthorizationExceptionKafkaConsumer. 例如,当配置的用户被拒绝访问某个主题或凭据不正确时,就会发生这种情况。定义authExceptionRetryInterval允许容器在授予适当权限时恢复。

默认情况下,没有配置时间间隔 - 身份验证和授权错误被认为是致命的,这会导致容器停止。

从 2.8 版本开始,在创建消费者工厂时,如果您将反序列化器作为对象提供(在构造函数中或通过设置器),工厂将调用该configure()方法以使用配置属性对其进行配置。

使用ConcurrentMessageListenerContainer

单构造函数类似于KafkaListenerContainer构造函数。以下清单显示了构造函数的签名:

public ConcurrentMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,
                            ContainerProperties containerProperties)

它也concurrency有财产。例如,container.setConcurrency(3)创建三个KafkaMessageListenerContainer实例。

对于第一个构造函数,Kafka 使用其组管理功能在消费者之间分配分区。

在监听多个主题时,默认的分区分布可能不是你所期望的。例如,如果您有 3 个主题,每个主题有 5 个分区,并且您想使用concurrency=15,那么您只会看到 5 个活动消费者,每个消费者从每个主题分配一个分区,其他 10 个消费者处于空闲状态。这是因为默认的 KafkaPartitionAssignorRangeAssignor(参见它的 Javadoc)。对于这种情况,您可能需要考虑使用RoundRobinAssignor相反,它将分区分布在所有消费者之间。然后,为每个消费者分配一个主题或分区。要更改PartitionAssignor,您可以在提供给 的属性中设置partition.assignment.strategy使用者属性(ConsumerConfigs.PARTITION_ASSIGNMENT_STRATEGY_CONFIGDefaultKafkaConsumerFactory

使用 Spring Boot 时,可以按如下方式指定设置策略:

spring.kafka.consumer.properties.partition.assignment.strategy=\
org.apache.kafka.clients.consumer.RoundRobinAssignor

当容器属性配置为TopicPartitionOffsets 时,ConcurrentMessageListenerContainer将实例分布TopicPartitionOffset在委托KafkaMessageListenerContainer实例之间。

例如,如果提供了六个TopicPartitionOffset实例并且concurrency3; 每个容器有两个分区。对于五个TopicPartitionOffset实例,两个容器获得两个分区,第三个获得一个。如果concurrency大于 的数量TopicPartitionsconcurrency则向下调整,使得每个容器获得一个分区。

client.id属性(如果设置)附加在-nwheren是对应于并发的消费者实例 。当启用 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.commitfalse除非在配置中明确设置。true以前,如果未设置该属性,则使用Kafka 默认值 ( )。

消费者poll()方法返回一个或多个ConsumerRecords。为MessageListener每条记录调用。AckMode以下列表描述了容器对每个(未使用事务时)采取的操作:

  • RECORD:在监听器处理完记录返回时提交偏移量。

  • BATCH:当所有返回的记录poll()都处理完后,提交偏移量。

  • TIME: 当所有返回的记录都被处理完时,提交偏移量poll(),只要ackTime超过了自上次提交的时间。

  • COUNT: 当所有返回的记录都被处理后,提交偏移量poll(),只要ackCount从上次提交后已经收到记录。

  • COUNT_TIME: 与TIMEand类似COUNT,但如果任一条件为 ,则执行提交true

  • MANUAL: 消息监听器acknowledge()Acknowledgment. 之后,应用相同的语义BATCH

  • MANUAL_IMMEDIATEAcknowledgment.acknowledge():当监听器调用方法时立即提交偏移量。

当使用transactions时,偏移量被发送到 transaction 并且语义等同于RECORDor BATCH,具体取决于侦听器类型(记录或批处理)。

MANUAL,并MANUAL_IMMEDIATE要求听者是一个AcknowledgingMessageListener或一个BatchAcknowledgingMessageListener。请参阅消息侦听器

根据syncCommits容器属性,使用消费者上的commitSync()orcommitAsync()方法。 默认情况下syncCommitstrue也见setSyncCommitTimeout。查看setCommitCallback获取异步提交的结果;默认回调是LoggingCommitCallback记录错误(以及调试级别的成功)。

因为侦听器容器有自己的提交偏移量的机制,所以它更喜欢 KafkaConsumerConfig.ENABLE_AUTO_COMMIT_CONFIGfalse. 从 2.3 版开始,它无条件地将其设置为 false,除非在消费者工厂中特别设置或容器的消费者属性覆盖。

有以下Acknowledgment方法:

public interface Acknowledgment {

    void acknowledge();

}

此方法使侦听器可以控制何时提交偏移量。

从 2.3 版开始,该Acknowledgment接口有两个附加方法nack(long sleep)nack(int index, long sleep). 第一个用于记录侦听器,第二个用于批处理侦听器。为您的侦听器类型调用错误的方法将引发IllegalStateException.

如果要提交部分批处理,请使用nack(), 使用事务时,将其设置AckModeMANUAL; 调用nack()会将成功处理的记录的偏移量发送到事务。
nack()只能在调用您的侦听器的消费者线程上调用。

使用记录侦听器,当nack()被调用时,将提交任何未决的偏移量,丢弃上次轮询的剩余记录,并在其分区上执行查找,以便在下一次重新传递失败的记录和未处理的记录poll()sleep通过设置参数,消费者可以在重新交付之前暂停。这与在容器配置了DefaultErrorHandler.

使用批处理侦听器时,您可以指定批处理中发生故障的索引。调用时nack(),将在索引之前为记录提交偏移量,并在分区上执行失败和丢弃记录的搜索,以便在下一次重新传递它们poll()

有关详细信息,请参阅容器错误处理程序

消费者在睡眠期间暂停,以便我们继续轮询代理以保持消费者存活。实际的睡眠时间及其分辨率取决于pollTimeout默认为 5 秒的容器。最小睡眠时间等于pollTimeout并且所有睡眠时间都是它的倍数。对于较小的睡眠时间,或者为了提高其准确性,请考虑减少容器的pollTimeout.
侦听器容器自动启动

侦听器容器实现SmartLifecycle,并且默认情况下autoStartuptrue。容器在后期(Integer.MAX-VALUE - 100)中启动。其他实现SmartLifecycle, 以处理来自侦听器的数据的组件应在较早的阶段启动。为后续阶段留出了空间,- 100以使组件能够在容器之后自动启动。

手动提交偏移量

通常,使用AckMode.MANUALor时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) {
    ...
}

或者,您可以在每条消息中接收带有每个偏移量和其他详细信息ListMessage<?>对象,但它必须是方法上定义的唯一参数(除了 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.idandclient.id属性;它们将被忽略;使用这些groupIdclientIdPrefix注释属性。

这些属性被指定为具有普通 JavaProperties文件格式的单个字符串:foo:barfoo=barfoo 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。您可以通过设置容器的consumerExecutorlistenerExecutor属性来提供自定义执行器ContainerProperties。使用池执行器时,请确保有足够的线程可用于处理使用它们的所有容器的并发性。使用 时ConcurrentMessageListenerContainer,每个使用者 ( concurrency) 使用每个线程。

如果您不提供消费者执行程序,SimpleAsyncTaskExecutor则使用 a。此执行程序创建名称类似于<beanName>-C-1(consumer thread) 的线程。对于ConcurrentMessageListenerContainer<beanName>线程名称的部分变为<beanName>-m,其中m代表消费者实例。 n每次启动容器时递增。所以,如果 bean 名称为container,则该容器中的线程将在第一次启动容器后命名为container-0-C-1container-1-C-1等等;container-0-C-2container-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方法时,有效负载必须已经转换为域对象(这样才能执行匹配)。使用自定义解串器,JsonDeserializerJsonMessageConverter设置TypePrecedenceTYPE_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什么都不做。在为侦听器容器提供自定义侦听器(任一类型)时,重要的是您的实现不要onPartitionsRevokedonPartitionsLost. 如果您实施ConsumerRebalanceListener,您应该覆盖默认方法。这是因为监听器容器会在调用你的实现方法之后onPartitionsRevoked从它的实现中调用它自己onPartitionsLost的。如果您实现委托到默认行为,onPartitionsRevoked则每次Consumer在容器的侦听器上调用该方法时都会被调用两次。
使用转发侦听器结果@SendTo

从 2.0 版本开始,如果您还使用注解对@KafkaListenera 进行@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以执行发送。KafkaTemplatereplyTemplate

除非您使用请求/回复语义,否则仅使用简单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);
        }

        ...

    };
}

如果侦听器方法返回Message<?>or Collection<Message<?>>,则侦听器方法负责设置回复的消息头。例如,在处理来自 a 的请求时ReplyingKafkaTemplate,您可能会执行以下操作:

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

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

即使没有返回结果,您也可以注释@KafkaListener方法。@SendTo这是为了允许配置一个errorHandler可以将有关失败消息传递的信息转发到某个主题的配置。以下示例显示了如何执行此操作:

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

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

有关详细信息,请参阅处理异常

如果侦听器方法返回一个Iterable,则默认情况下,每个元素都会在发送值时记录一条记录。从版本 2.3.5 开始,将splitIterables属性设置@KafkaListenerfalse,整个结果将作为单个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.groupg2.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对象中。在为请求创建TopicPartitionOffsets 时,仅支持正的绝对偏移量。

4.1.5。侦听器容器属性

表 1.ContainerProperties属性
财产 默认 描述

ackCount

1

ackModeisCOUNT或时,提交未决偏移前的记录数COUNT_TIME

adviceChain

null

包装消息侦听器的对象链Advice(例如MethodInterceptor,围绕通知)按顺序调用。

ackMode

控制提交偏移量的频率 - 请参阅提交偏移量

ackTime

5000

ackMode当isTIME或时提交未决偏移量的时间(以毫秒为单位) COUNT_TIME

assignmentCommitOption

LATEST_ONLY _NO_TX

是否承诺在分配的初始位置;默认情况下,只有在存在时才会提交初始偏移量,即使存在事务管理器,它也不会在事务中运行ConsumerConfig.AUTO_OFFSET_RESET_CONFIG。有关可用选项的更多信息,latest请参阅 javadocs 。ContainerProperties.AssignmentCommitOption

authExceptionRetryInterval

null

如果不为 null,则当Kafka 客户端抛出orDuration时,在轮询之间休眠。当为 null 时,此类异常被认为是致命的并且容器将停止。AuthenticationExceptionAuthorizationException

clientId

(空字符串)

client.id消费者属性的前缀。覆盖消费者工厂client.id属性;在并发容器中,-n添加为每个消费者实例的后缀。

checkDeserExWhenKeyNull

错误的

设置为在收到a 时true始终检查DeserializationException标头。null key当消费者代码无法确定ErrorHandlingDeserializer已配置一个时很有用,例如在使用委托解串器时。

checkDeserExWhenValueNull

错误的

设置为在收到a 时true始终检查DeserializationException标头。null value当消费者代码无法确定ErrorHandlingDeserializer已配置一个时很有用,例如在使用委托解串器时。

commitCallback

null

如果存在并且syncCommitsfalse在提交完成后调用的回调。

offsetAndMetadataProvider

null

提供者OffsetAndMetadata;默认情况下,提供者使用空元数据创建偏移量和元数据。提供者提供了一种自定义元数据的方法。

commitLogLevel

调试

与提交偏移量有关的日志的日志记录级别。

consumerRebalanceListener

null

一个再平衡监听器;请参阅重新平衡侦听器

consumerStartTimout

30 多岁

在记录错误之前等待消费者启动的时间;例如,如果您使用线程不足的任务执行器,则可能会发生这种情况。

consumerTaskExecutor

SimpleAsyncTaskExecutor

运行消费者线程的任务执行器。默认执行器创建名为<name>-C-n;的线程。与KafkaMessageListenerContainer,名称是bean名称;名称是带有ConcurrentMessageListenerContainer后缀的bean名称,-n其中n为每个子容器递增。

deliveryAttemptHeader

false

请参阅传递尝试标题

eosMode

V2

Exactly Once 语义模式;请参阅Exactly Once 语义

fixTxOffsets

false

当使用事务生产者生成的记录,并且消费者位于分区的末尾时,延迟可能会错误地报告为大于零,这是由于用于指示事务提交/回滚的伪记录以及可能的存在的回滚记录。这在功能上不会影响消费者,但一些用户表示担心“滞后”不是零。将此属性设置为true,容器将更正此类错误报告的偏移量。检查在下一次轮询之前执行,以避免显着增加提交处理的复杂性。isolation.level=read_committed在撰写本文时,仅当消费者配置为max.poll.records大于 1时才会更正滞后。有关更多信息,请参阅KAFKA-10683

groupId

null

覆盖消费者group.id属性;@KafkaListener id由orgroupId属性自动设置。

idleBeforeDataMultiplier

5.0

idleEventInterval在收到任何记录之前应用乘数。收到记录后,不再应用乘数。从 2.8 版开始可用。

idleBetweenPolls

0

用于通过在轮询之间休眠线程来减慢交付速度。处理一批记录的时间加上这个值必须小于max.poll.interval.ms消费者属性。

idleEventInterval

null

设置后,启用ListenerContainerIdleEvents 的发布,请参阅应用程序事件检测空闲和非响应消费者。另请参阅idleBeforeDataMultiplier

idlePartitionEventInterval

null

设置后,启用ListenerContainerIdlePartitionEvents 的发布,请参阅应用程序事件检测空闲和非响应消费者

kafkaConsumerProperties

没有任何

用于覆盖在消费者工厂上配置的任意消费者属性。

logContainerConfig

false

设置为 true 以在 INFO 级别记录所有容器属性。

messageListener

null

消息侦听器。

micrometerEnabled

true

是否为消费者线程维护 Micrometer 计时器。

missingTopicsFatal

false

当为 true 时,如果代理上不存在已配置的主题,则阻止容器启动。

monitorInterval

30 多岁

多久检查一次消费者线程的状态NonResponsiveConsumerEvent。见noPollThresholdpollTimeout

noPollThreshold

3.0

乘以pollTimeOut决定是否发布一个NonResponsiveConsumerEvent. 见monitorInterval

onlyLogRecordMetadata

false

设置为 false 以记录完整的消费者记录(错误、调试日志等),而不仅仅是topic-partition@offset. 已弃用。替换为KafkaUtils.setConsumerRecordFormatter

pollTimeout

5000

超时传入Consumer.poll().

scheduler

ThreadPoolTaskScheduler

在其上运行消费者监控任务的调度程序。

shutdownTimeout

10000

stop()在所有消费者停止和发布容器停止事件之前阻塞方法的最长时间(以毫秒为单位) 。

stopContainerWhenFenced

false

ProducerFencedException如果抛出a,则停止侦听器容器。有关详细信息,请参阅回滚后处理器

stopImmediate

false

当容器停止时,在当前记录之后停止处理,而不是在处理完之前轮询的所有记录之后。

subBatchPerPartition

见描述。

使用批处理侦听器时,如果是true,则调用侦听器,并将轮询结果拆分为子批处理,每个分区一个。默认false情况下使用事务时除外EOSMode.ALPHA- 请参阅Exactly Once Semantics

syncCommitTimeout

null

使用的超时时间syncCommitstrue。如果未设置,容器将尝试确定default.api.timeout.ms消费者属性并使用它;否则它将使用 60 秒。

syncCommits

true

是否对偏移量使用同步或异步提交;见commitCallback

topics topicPattern topicPartitions

不适用

配置的主题、主题模式或明确分配的主题/分区。互斥;必须提供至少一个;由ContainerProperties构造函数强制执行。

transactionManager

null

请参阅交易

表 2.AbstractListenerContainer属性
财产 默认 描述

afterRollbackProcessor

DefaultAfterRollbackProcessor

AfterRollbackProcessor事务回滚后调用。

applicationEventPublisher

应用上下文

事件发布者。

batchErrorHandler

见描述。

已弃用 - 请参阅commonErrorHandler.

batchInterceptor

null

在调用批处理侦听器之前设置一个BatchInterceptor调用;不适用于录音监听器。另请参阅interceptBeforeTx

beanName

豆名

容器的bean名称;后缀-n为子容器。

commonErrorHandler

见描述。

DefaultErrorHandler或者null当使用 atransactionManager时提供 a DefaultAfterRollbackProcessor。请参阅容器错误处理程序

containerProperties

ContainerProperties

容器属性实例。

errorHandler

见描述。

已弃用 - 请参阅commonErrorHandler.

genericErrorHandler

见描述。

已弃用 - 请参阅commonErrorHandler.

groupId

见描述。

containerProperties.groupId如果存在,否则group.id来自消费者工厂的属性。

interceptBeforeTx

true

确定是recordInterceptor在事务开始之前还是之后调用 。

listenerId

见描述。

用户配置容器的 bean 名称或 s 的id属性@KafkaListener

listenerInfo

无效的

要填充在KafkaHeaders.LISTENER_INFO标题中的值。使用@KafkaListener,该值是从info属性中获得的。这个头文件可以用在不同的地方,例如 aRecordInterceptorRecordFilterStrategy监听器代码本身。

pauseRequested

(只读)

如果已请求消费者暂停,则为真。

recordInterceptor

null

在调用记录监听器之前设置一个RecordInterceptor调用;不适用于批处理侦听器。另请参阅interceptBeforeTx

topicCheckTimeout

30 多岁

missingTopicsFatal容器属性为时,操作完成true等待多长时间(以秒为单位) 。describeTopics

表 3.KafkaMessageListenerContainer属性
财产 默认 描述

assignedPartitions

(只读)

当前分配给此容器的分区(显式或非显式)。

assignedPartitionsByClientId

(只读)

当前分配给此容器的分区(显式或非显式)。

clientIdSuffix

null

由并发容器用来给每个子容器的消费者一个唯一的client.id.

containerPaused

不适用

如果已请求暂停并且消费者实际上已暂停,则为真。

表 4.ConcurrentMessageListenerContainer属性
财产 默认 描述

alwaysClientIdSuffix

true

设置为 false 以禁止向client.id消费者属性添加后缀,当concurrency仅为 1 时。

assignedPartitions

(只读)

当前分配给此容器的子KafkaMessageListenerContainers 的分区聚合(显式或非显式)。

assignedPartitionsByClientId

(只读)

当前分配给此容器的子KafkaMessageListenerContainers 的分区(显式或非显式),由子容器的消费者client.id属性键控。

concurrency

1

KafkaMessageListenerContainer要管理的孩子的数量。

containerPaused

不适用

如果已请求暂停并且所有子容器的使用者实际上已暂停,则为真。

containers

不适用

对所有 childKafkaMessageListenerContainer的引用。

4.1.6。应用程序事件

以下 Spring 应用程序事件由侦听器容器及其使用者发布:

  • ConsumerStartingEvent- 在消费者线程第一次启动时发布,在它开始轮询之前。

  • ConsumerStartedEvent- 在消费者即将开始轮询时发布。

  • ConsumerFailedToStartEvent- 如果没有ConsumerStartingEventconsumerStartTimeout容器属性中发布,则发布。此事件可能表明配置的任务执行器没有足够的线程来支持它所使用的容器及其并发性。发生这种情况时,还会记录一条错误消息。

  • 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属性,除了idleTimepaused

具有以下ListenerContainerPartitionIdleEvent属性:

  • source:发布事件的侦听器容器实例。

  • container:侦听器容器或父侦听器容器,如果源容器是子容器。

  • id:侦听器 ID(或容器 bean 名称)。

  • idleTime: 发布事件时,时间分区消耗一直处于空闲状态。

  • topicPartition:触发​​事件的主题和分区。

  • consumer: 对 KafkaConsumer对象的引用。例如,如果pause()之前调用了消费者的方法,它可以resume()在收到事件时调用。

  • paused:该分区消费当前是否为该消费者暂停。有关详细信息,请参阅暂停和恢复侦听器容器

具有相同的ListenerContainerPartitionNoLongerIdleEvent属性,除了idleTimepaused

具有以下NonResponsiveConsumerEvent属性:

  • source:发布事件的侦听器容器实例。

  • container:侦听器容器或父侦听器容器,如果源容器是子容器。

  • id:侦听器 ID(或容器 bean 名称)。

  • timeSinceLastPoll: 容器最后一次调用之前的时间poll()

  • topicPartitions: 容器在事件生成时分配的主题和分区。

  • consumer: 对 KafkaConsumer对象的引用。例如,如果pause()之前调用了消费者的方法,它可以resume()在收到事件时调用。

  • paused: 容器当前是否处于暂停状态。有关详细信息,请参阅暂停和恢复侦听器容器

ConsumerPausedEvent和事件具有以下属性ConsumerResumedEventConsumerStopping

  • source:发布事件的侦听器容器实例。

  • container:侦听器容器或父侦听器容器,如果源容器是子容器。

  • partitions:TopicPartition涉及的实例。

,ConsumerPartitionPausedEvent事件ConsumerPartitionResumedEvent具有以下属性:

  • source:发布事件的侦听器容器实例。

  • container:侦听器容器或父侦听器容器,如果源容器是子容器。

  • partition:TopicPartition涉及的实例。

ConsumerStartingEventConsumerStartingEvent和事件具有以下属性ConsumerFailedToStartEventConsumerStoppedEventContainerStoppedEvent

  • source:发布事件的侦听器容器实例。

  • container:侦听器容器或父侦听器容器,如果源容器是子容器。

所有容器(无论是子容器还是父容器)都发布ContainerStoppedEvent. 对于父容器,源和容器属性相同。

此外,ConsumerStoppedEvent具有以下附加属性:

  • reason

    • NORMAL- 消费者正常停止(容器已停止)。

    • ERROR- 一个java.lang.Error被抛出。

    • FENCED- 交易生产者被围起来,stopContainerWhenFenced容器财产是true.

    • AUTH- AuthenticationExceptionorAuthorizationException被抛出并且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;
}

以下示例显示了如何设置idleEventIntervalfor a @KafkaListener

@Bean
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<String, String> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
    ...
    factory.getContainerProperties().setIdleEventInterval(60000L);
    ...
    return factory;
}

在每种情况下,当容器空闲时,每分钟发布一次事件。

如果由于某种原因,消费者poll()方法没有退出,则不会收到任何消息并且无法生成空闲事件(这kafka-clients是无法访问代理的早期版本的问题)。NonResponsiveConsumerEvent在这种情况下,如果民意调查未在属性内返回3xpollTimeout则容器会发布 a 。默认情况下,此检查在每个容器中每 30 秒执行一次。您可以通过在配置侦听器容器时设置monitorInterval(默认 30 秒)和noPollThreshold(默认 3.0)属性来修改此行为。应该大于以避免由于竞争条件ContainerProperties而出现虚假事件。接收到这样的事件可以让您停止容器,从而唤醒消费者以便它可以停止。noPollThreshold1.0

从 2.6.2 版本开始,如果容器发布了 a ListenerContainerIdleEvent,它将ListenerContainerNoLongerIdleEvent在随后收到记录时发布 a 。

事件消费

您可以通过实现来捕获这些事件ApplicationListener ——一个通用的侦听器或一个缩小为仅接收此特定事件的侦听器。您也可以使用@EventListenerSpring Framework 4.2 中引入的 ,。

下一个示例将@KafkaListenerand组合@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-nn唯一值以支持并发。这就是我们startsWith在条件下使用的原因。
如果您希望使用空闲事件来停止侦听器容器,则不应调用container.stop()调用侦听器的线程。这样做会导致延迟和不必要的日志消息。相反,您应该将事件交给另一个线程,然后该线程可以停止容器。stop()此外,如果它是子容器,则不应使用容器实例。您应该改为停止并发容器。
空闲时的当前位置

请注意,您可以通过ConsumerSeekAware在侦听器中实现检测到空闲时获取当前位置。请参阅寻求特定偏移onIdleContainer()量。

4.1.7. 主题/分区初始偏移量

有几种方法可以设置分区的初始偏移量。

手动分配分区时,您可以在配置的TopicPartitionOffset参数中设置初始偏移量(如果需要)(请参阅消息侦听器容器)。您也可以随时寻找特定的偏移量。

当您使用代理分配分区的组管理时:

  • 对于一个新group.id的,初始偏移量由auto.offset.reset消费者属性(earliestlatest)确定。

  • 对于现有组 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 版中添加。

onIdleContaineroronPartitionsAssigned方法中为多个分区寻找相同的时间戳时,首选第二种方法,因为在对消费者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=1MessageListener@KafkaListener

  • ThreadLocal<?>在实例中保留状态。

  • 将单例侦听器委托给在SimpleThreadScope(或类似范围)中声明的 bean。

为了便于清理线程状态(针对前面列表中的第二项和第三项),从 2.2 版本开始,监听器容器ConsumerStoppedEvent会在每个线程退出时发布一个。您可以使用ApplicationListeneror@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 名称)

  • resultsuccessfailure

  • exceptionnoneListenerExecutionFailedException

ContainerProperties micrometerTags您可以使用该属性添加其他标签。

使用并发容器,将为每个线程创建计时器,并且标签以where n is toname为后缀。 -n0concurrency-1
监控 KafkaTemplate 性能

从版本 2.5 开始,如果在类路径上检测到,并且应用程序上下文中存在单个,则模板将自动创建和更新 MicrometerTimer以进行发送操作。可以通过将模板的属性设置为 来禁用计时器。MicrometerMeterRegistrymicrometerEnabledfalse

维护了两个计时器 - 一个用于成功调用侦听器,一个用于失败。

计时器被命名spring.kafka.template并具有以下标签:

  • name: (模板 bean 名称)

  • resultsuccessfailure

  • 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

获取 Kafka 指标之一的示例
double count = this.meterRegistry.get("kafka.producer.node.incoming.byte.total")
                .tag("customTag", "customTagValue")
                .tag("spring.id", "myProducerFactory.myClientId-1")
                .functionCounter()
                .count()

StreamsBuilderFactoryBean为- 参见KafkaStreams Micrometer Support提供了一个类似的侦听器。

4.1.12。交易

本节介绍 Spring for Apache Kafka 如何支持事务。

概述

0.11.0.0 客户端库增加了对事务的支持。Spring for Apache Kafka 通过以下方式添加支持:

  • KafkaTransactionManager:与正常的 Spring 事务支持(@TransactionalTransactionTemplate)一起使用。

  • 事务性的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属性设置DefaultKafkaProducerFactoryfalse

虽然批处理侦听器支持事务,但默认情况下不支持僵尸防护,因为批处理可能包含来自多个主题或分区的记录。但是,从版本 2.3.2 开始,如果将容器属性设置subBatchPerPartition为 true,则支持僵尸防护。在这种情况下,从上次轮询接收到的每个分区都会调用一次批处理侦听器,就好像每个轮询仅返回单个分区的记录一样。这是true从 2.5 版开始的默认情况下,使用EOSMode.ALPHA;启用事务时。false如果您正在使用事务但不关心僵尸围栏,请将其设置为。另请参阅Exactly Once Semantics

另请参阅transactionIdPrefix

使用 Spring Boot,只需要设置spring.kafka.producer.transaction-id-prefix属性 - Boot 会自动配置一个KafkaTransactionManagerbean 并将其连接到侦听器容器中。

从版本 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 事务支持一起使用(@TransactionalTransactionTemplate和其他)。如果事务处于活动状态,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 开始,如果同步事务的提交失败(在主事务提交之后),将向调用者抛出异常。以前,这被默默地忽略(在调试时记录)。如有必要,应用程序应采取补救措施,以补偿已提交的主要事务。
使用消费者发起的交易

ChainedKafkaTransactionManager2.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 应用程序中。在以前的版本中,您必须创建两个生产者工厂和KafkaTemplatekafkaTemplate.executeInTransaction()s - 一种用于在侦听器容器线程上生成记录,另一种用于由方法上的事务拦截器启动或由其启动的独立事务@Transactional

现在,您可以覆盖工厂transactionalIdPrefixKafkaTemplateKafkaTransactionManager

当为侦听器容器使用事务管理器和模板时,您通常会将其保留为默认的生产者工厂的属性。在使用EOSMode.ALPHA. EOSMode.BETA不再需要使用相同的,transactional.id即使是消费者发起的交易;事实上,它在每个实例上都必须是唯一的,就像生产者发起的事务一样。对于由模板(或 的事务管理器@Transaction)启动的事务,您应该分别在模板和事务管理器上设置属性。此属性在每个应用程序实例上必须具有不同的值。

使用时(代理版本> = 2.5)transactional.id已消除此问题(不同的规则);EOSMode.BETA请参阅Exactly Once 语义

KafkaTemplate事务性和非事务性发布

通常,当 aKafkaTemplate是事务性的(配置了具有事务能力的生产者工厂)时,需要事务。当配置了. TransactionTemplate_ 任何在事务范围之外使用模板的尝试都会导致模板抛出. 从版本 2.4.3 开始,您可以将模板的属性设置为. 在这种情况下,模板将允许操作在没有事务的情况下运行,方法是调用's方法;生产者将像往常一样被缓存或线程绑定以供重用。请参阅使用@TransactionalexecuteInTransactionKafkaTransactionManagerIllegalStateExceptionallowNonTransactionaltrueProducerFactorycreateNonTransactionalProducer()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 通过使用Producerfor each来管理它group.id/topic/partition;当发生重新平衡时,新实例将使用相同transactional.id的实例,并且旧的生产者被隔离。

使用 mode V2,不必为每个生产者指定一个生产者,group.id/topic/partition因为消费者元数据与事务的偏移量一起发送,并且代理可以确定生产者是否使用该信息被隔离。

从 2.6 版开始,默认EOSMode值为V2.

要将容器配置为使用 mode ALPHA,请将容器属性设置EOSModeALPHA, 以恢复之前的行为。

使用V2(默认),您的代理必须是 2.5 或更高版本;kafka-clients3.0 版,生产者将不再回退到V1; 如果代理不支持V2,则抛出异常。如果您的代理早于 2.5,则必须设置EOSModeV1,将DefaultKafkaProducerFactory producerPerConsumerPartition设置保留为true,如果您使用的是批处理侦听器,则应设置subBatchPerPartitiontrue

当你的 broker 升级到 2.5 或更高版本时,你应该将模式切换为V2,但生产者的数量将保持不变。然后,您可以使用producerPerConsumerPartitionset 对应用程序进行滚动升级false以减少生产者的数量;您也不应该再设置subBatchPerPartition容器属性。

如果您的经纪人已经是 2.5 或更高版本,您应该将该DefaultKafkaProducerFactory producerPerConsumerPartition属性设置为false,以减少所需的生产者数量。

与 一起使用EOSMode.V2producerPerConsumerPartition=falsetransactional.id必须在所有应用程序实例中都是唯一的。

使用V2模式时,不再需要设置subBatchPerPartitiontrue; 它将默认falseEOSModeis V2

有关详细信息,请参阅KIP-447

V1V2以前是ALPHABETA; 它们已被更改以使框架与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和方法。resumeApause()在下一个之前生效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 版开始ConsumerPartitionPausedEventConsumerPartitionResumedEvent实例与容器一起作为source属性和TopicPartition实例发布。

4.1.17。序列化、反序列化和消息转换

概述

Apache Kafka 提供了用于序列化和反序列化记录值及其键的高级 API。它与一些内置实现的org.apache.kafka.common.serialization.Serializer<T>和 抽象一起出现。org.apache.kafka.common.serialization.Deserializer<T>同时,我们可以通过使用ProducerConsumer配置属性来指定序列化器和反序列化器类。以下示例显示了如何执行此操作:

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接受SerializerDeserializer实例。keysvalues

当您使用此 API 时,DefaultKafkaProducerFactoryandDefaultKafkaConsumerFactory还提供属性(通过构造函数或 setter 方法)将自定义SerializerDeserializer实例注入目标ProducerConsumer. 此外,您可以通过构造函数传入Supplier<Serializer>orSupplier<Deserializer>实例 - 这些Suppliers 在创建每个Produceror时被调用Consumer

字符串序列化

从 2.5 版开始,Spring for Apache Kafka 提供了使用字符串表示实体的类ToStringSerializerParseStringDeserializer它们依赖方法toString和一些Function<String>BiFunction<String, Headers>解析字符串并填充实例的属性。通常,这会调用类上的一些静态方法,例如parse

ToStringSerializer<Thing> thingSerializer = new ToStringSerializer<>();
//...
ParseStringDeserializer<Thing> deserializer = new ParseStringDeserializer<>(Thing::parse);

默认情况下,ToStringSerializer配置为在记录中传达有关序列化实体的类型信息HeadersaddTypeInfo您可以通过将该属性设置为 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 对象映射器的实现JsonSerializerJsonDeserializer允许将JsonSerializer任何 Java 对象编写为 JSON byte[]。这JsonDeserializer需要一个额外的Class<?> targetType参数来允许将消费对象反序列化为byte[]正确的目标对象。下面的例子展示了如何创建一个JsonDeserializer

JsonDeserializer<Thing> thingDeserializer = new JsonDeserializer<>(Thing.class);

您可以自定义JsonSerializerJsonDeserializer使用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.JacksonMimeTypeModuleorg.springframework.util.MimeType纯字符串中,以便通过网络实现跨平台兼容性。AJacksonMimeTypeModule可以在应用程序上下文中注册为 bean,并将自动配置到Spring BootObjectMapper实例中。

同样从 2.3 版开始,JsonDeserializer提供了TypeReference基于构造函数的构造函数,用于更好地处理目标通用容器类型。

从 2.1 版本开始,您可以在记录中传达类型信息Headers,从而允许处理多种类型。此外,您可以使用以下 Kafka 属性配置序列化器和反序列化器。如果您分别为和提供SerializerDeserializer实例,它们将无效。KafkaConsumerKafkaProducer

配置属性
  • 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

您只能使用属性执行简单的配置。对于更高级的配置(例如ObjectMapper在序列化器和反序列化器中使用自定义),您应该使用接受预构建的序列化器和反序列化器的生产者和消费者工厂构造函数。以下 Spring Boot 示例覆盖了默认工厂:

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

@Bean
public ProducerFactory<String, Thing> kafkaProducerFactory(JsonSerializer customValueSerializer) {

    return new DefaultKafkaProducerFactory<>(properties.buildProducerProperties(),
        new StringSerializer(), customValueSerializer);
}

还提供了 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_METHODspring.json.key.type.method

  • JsonDeserializer.VALUE_TYPE_METHODspring.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 版引入了DelegatingSerializerand DelegatingDeserializer,它允许生成和使用具有不同键和/或值类型的记录。生产者必须将一个标头设置DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR为一个选择器值,用于选择哪个序列化器用于值和DelegatingSerializer.KEY_SERIALIZATION_SELECTOR键;如果未找到匹配项,IllegalStateException则抛出 an。

对于传入的记录,反序列化器使用相同的标头来选择要使用的反序列化器;如果未找到匹配项或标头不存在,byte[]则返回原始数据。

您可以通过构造函数将选择器映射配置为Serializer/ Deserializer,也可以通过 Kafka 生产者/消费者属性使用键DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIGDelegatingSerializer.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标头设置为thing1thing2

该技术支持向同一主题(或不同主题)发送不同类型。

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和角度来看, Serializerand API 非常简单和灵活,但在使用Spring Integration 的 Apache Kafka Support时,您可能需要在 Spring Messaging 级别具有更大的灵活性。为了让您轻松地与 Kafka 相互转换,Spring for Apache Kafka 提供了具有实现及其(和子类)定制的抽象。您可以直接将其注入实例,也可以使用属性的bean 定义。以下示例显示了如何执行此操作:DeserializerConsumerProducer@KafkaListenerorg.springframework.messaging.MessageMessageConverterMessagingMessageConverterJsonMessageConverterMessageConverterKafkaTemplateAbstractKafkaListenerContainerFactory@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时,参数类型被提供给消息转换器以协助转换。

@KafkaListener只有在方法级别声明注解时,才能实现这种类型推断。对于类级别@KafkaListener,有效负载类型用于选择@KafkaHandler要调用的方法,因此在选择方法之前它必须已经转换。

在消费者方面,您可以配置一个JsonMessageConverter; 它可以处理ConsumerRecordtype 的值byte[]Bytes因此String应该与 aByteArrayDeserializer或结合BytesDeserializer使用StringDeserializer。(byte[]并且Bytes效率更高,因为它们避免了不必要byte[]String转换)。JsonMessageConverter如果您愿意,您还可以配置与反序列化器对应的特定子类。

在生产者端,当您使用 Spring Integration 或KafkaTemplate.send(Message<?> message)方法(请参阅使用KafkaTemplate)时,您必须配置与配置的 Kafka 兼容的消息转换器Serializer

  • StringJsonMessageConverterStringSerializer

  • BytesJsonMessageConverterBytesSerializer

  • ByteArrayJsonMessageConverterByteArraySerializer

同样,使用byte[]orBytes更有效,因为它们避免了Stringtobyte[]转换。

为方便起见,从 2.3 版开始,该框架还提供了一个StringOrBytesSerializer可以序列化所有三种值类型的方法,因此它可以与任何消息转换器一起使用。

从版本 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_CLASSErrorHandlingDeserializer.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 JsonMessageConverterwithin a来转换批处理消息。BatchMessagingMessageConverter有关更多信息,请参阅序列化、反序列化和消息转换Spring Messaging 消息转换

默认情况下,转换的类型是从侦听器参数中推断出来的。如果您使用设置为(而不是默认值)的JsonMessageConverteraDefaultJackson2TypeMapper进行配置,则转换器将使用标头中的类型信息(如果存在)。例如,这允许使用接口而不是具体类来声明侦听器方法。此外,类型转换器支持映射,因此反序列化可以是与源不同的类型(只要数据兼容)。当您使用必须已经转换有效负载以确定调用哪个方法的类级实例时,这也很有用。以下示例创建使用此方法的 bean:TypePrecedenceTYPE_IDINFERRED@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会禁用此功能。KafkaListenerEndpointRegistrarKafkaListenerConfigurer
添加自定义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@PayloadDefaultMessageHandlerMethodFactoryPayloadMethodArgumentResolverKafkaNull

4.1.18。消息头

0.11.0.0 客户端引入了对消息头的支持。从 2.0 版开始,Spring for Apache Kafka 现在支持将这些标头映射到spring-messaging MessageHeaders.

以前的版本映射ConsumerRecordProducerRecord到 spring-messaging Message<?>,其中 value 属性映射到和从 thepayload和其他属性(topicpartition,等等)被映射到标题。情况仍然如此,但现在可以映射附加(任意)标头。

Apache Kafka 标头有一个简单的 API,如以下接口定义所示:

public interface Header {

    String key();

    byte[] value();

}

KafkaHeaderMapper策略用于在 KafkaHeadersMessageHeaders. 其接口定义如下:

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.*)。您可以否定带有前导的模式!。第一个匹配标头名称(无论是正数还是负数)的模式获胜。

当您提供自己的模式时,我们建议包含!idand !timestamp,因为这些标头在入站端是只读的。

默认情况下,映射器仅反序列化java.lang和中的类java.utiladdTrustedPackages您可以通过使用该方法添加受信任的包来信任其他(或所有)包。如果您从不受信任的来源收到消息,您可能希望仅添加您信任的那些包。要信任所有包,您可以使用mapper.addTrustedPackages("*").
在与不知道映射器 JSON 格式的系统通信时,以原始形式映射String标头值很有用。

从版本 2.2.5 开始,您可以指定某些字符串值标头不应使用 JSON 映射,而是映射到/来自 raw byte[]。有新的AbstractKafkaHeaderMapper属性;mapAllStringsOut设置为 true 时,所有字符串值的标头都将转换为byte[]使用该charset属性(默认UTF-8)。此外,还有一个属性rawMappedHeaders,它是 ; 的映射header name : boolean。如果映射包含标头名称,并且标头包含值,则将使用字符集String将其映射为原始数据。当且仅当映射值中的布尔值为 时,byte[]此映射还用于将原始传入byte[]标头映射到使用字符集。如果布尔值是,或者标题名称不在地图中Stringtruefalsetrue值,传入的标头被简单地映射为原始未映射的标头。

下面的测试用例说明了这种机制。

@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就会在MessagingMessageConverterand中使用。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.MimeTypeorg.springframework.http.MediaType以这种方式映射。
从 2.3 版开始,简化了对字符串值标头的处理。默认情况下,此类标头不再是 JSON 编码的(即它们没有"…​"添加封闭)。该类型仍被添加到 JSON_TYPES 标头中,因此接收系统可以转换回字符串(来自byte[])。映射器可以处理(解码)旧版本产生的标头(它检查前导");这样,使用 2.3 的应用程序可以使用旧版本的记录。
要与早期版本兼容,请设置encodeStringstrue,如果使用 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。它可能用于请求/回复场景中,您希望在重试几次后,在死信主题中捕获失败记录后,将失败结果发送给发件人。MessagingMessageConverterBatchMessagingMessageConverterConsumerRecordMessage<?>KafkaHeaders.RAW_DATADeadLetterPublishingRecoverer

@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 版开始,旧版ErrorHandlerBatchErrorHandler接口已被新的CommonErrorHandler. 这些错误处理程序可以处理记录和批处理侦听器的错误,允许单个侦听器容器工厂为两种类型的侦听器创建容器。 CommonErrorHandler提供了替换大多数遗留框架错误处理程序实现的实现,并且不推荐使用遗留错误处理程序。监听器容器和监听器容器工厂仍然支持遗留接口;它们将在未来的版本中被弃用。

有关将自定义错误处理程序迁移CommonErrorHandlerCommonErrorHandler.

使用事务时,默认情况下不配置错误处理程序,因此异常将回滚事务。事务容器的错误处理由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 就会将其添加到自动配置的工厂中。

默认错误处理程序

这个新的错误处理程序取代了SeekToCurrentErrorHandlerand RecoveringBatchErrorHandler,它们现在已经是多个版本的默认错误处理程序。一个区别是批处理侦听器的回退行为(当BatchListenerFailedException抛出 a 以外的异常时)等效于Retrying Complete Batches

错误处理程序可以恢复(跳过)一直失败的记录。默认情况下,十次失败后,记录失败的记录(在ERROR级别)。BiConsumer您可以使用自定义恢复器 ( ) 和BackOff控制交付尝试和每个之间延迟的a 来配置处理程序。使用FixedBackOffwithFixedBackOff.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()三个未处理的记录。

如果AckModeBATCH,容器在调用错误处理程序之前提交前两个分区的偏移量。

对于批处理侦听器,侦听器必须抛出一个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;
}

错误处理程序可以配置一个或多个RetryListeners,接收重试和恢复进度的通知。

@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默认情况下将重置,并且在再次尝试恢复之前,重新交付将再次经历回退。要在恢复失败后跳过重试,请将错误处理程序设置resetStateOnRecoveryFailurefalse.

您可以根据失败的记录和/或异常为错误处理程序提供一个BiFunction<ConsumerRecord<?, ?>, Exception, BackOff>来确定要使用的:BackOff

handler.setBackOffFunction((record, ex) -> { ... });

如果函数返回null,将使用处理程序的默认值BackOff

如果异常类型在两次失败之间发生变化,则设置resetStateOnExceptionChangetrue并重新启动重试序列(包括选择新的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如果侦听器抛出异常,则停止容器。对于记录侦听器,当AckModeis时RECORD,已提交已处理记录的偏移量。对于记录侦听器,当AckMode是任何手动值时,已确认记录的偏移量将被提交。对于记录侦听器, wWhen AckModeis BATCH,或者对于批处理侦听器,在容器重新启动时重播整个批处理。

容器停止后,ListenerExecutionFailedException会抛出一个包装容器的异常。这是为了使事务回滚(如果启用了事务)。

委派错误处理程序

可以委托给不同的CommonDelegatingErrorHandler错误处理程序,具体取决于异常类型。例如,您可能希望DefaultErrorHandler为大多数异常调用 a,或CommonContainerStoppingErrorHandler为其他异常调用 a。

记录错误处理程序

简单地CommonLoggingErrorHandler记录异常;使用记录侦听器,将上次轮询中的剩余记录传递给侦听器。对于批处理侦听器,将记录批处理中的所有记录。

为记录和批处理侦听器使用不同的常见错误处理程序

如果您希望对记录和批处理侦听器使用不同的错误处理策略,CommonMixedErrorHandler则提供了允许为每种侦听器类型配置特定错误处理程序的方法。

常见错误处理程序总结
  • DefaultErrorHandler

  • CommonContainerStoppingErrorHandler

  • CommonDelegatingErrorHandler

  • CommonLoggingErrorHandler

  • CommonMixedErrorHandler

旧版错误处理程序及其替换
旧版错误处理程序 替代品

LoggingErrorHandler

CommonLoggingErrorHandler

BatchLoggingErrorHandler

CommonLoggingErrorHandler

ConditionalDelegatingErrorHandler

DelegatingErrorHandler

ConditionalDelegatingBatchErrorHandler

DelegatingErrorHandler

ContainerStoppingErrorHandler

CommonContainerStoppingErrorHandler

ContainerStoppingBatchErrorHandler

CommonContainerStoppingErrorHandler

SeekToCurrentErrorHandler

DefaultErrorHandler

SeekToCurrentBatchErrorHandler

没有替代品,DefaultErrorHandler与无限一起使用BackOff

RecoveringBatchErrorHandler

DefaultErrorHandler

RetryingBatchErrorHandler

没有替换 - 使用DefaultErrorHandler并抛出除BatchListenerFailedException.

将自定义遗留错误处理程序实现迁移到CommonErrorHandler

请参阅 中的 javadocs CommonErrorHandler

要替换ErrorHandlerorConsumerAwareErrorHandler实现,您应该实现handleRecord()并离开remainingRecords()返回false(默认)。您还应该实现handleOtherException()- 以处理记录处理范围之外发生的异常(例如,消费者错误)。

要替换RemainingRecordsErrorHandler实现,您应该实现handleRemaining()并覆盖remainingRecords()返回true。您还应该实现handleOtherException()- 以处理记录处理范围之外发生的异常(例如,消费者错误)。

要替换任何BatchErrorHandler实现,您应该实现handleBatch() 您还应该实现handleOtherException()- 以处理记录处理范围之外发生的异常(例如消费者错误)。

回滚后处理器

使用事务时,如果侦听器抛出异常(并且错误处理程序,如果存在,则抛出异常),事务将回滚。默认情况下,任何未处理的记录(包括失败的记录)都会在下一次轮询时重新获取。这是通过seekDefaultAfterRollbackProcessor. 使用批处理侦听器,将重新处理整批记录(容器不知道批处理中的哪条记录失败)。要修改此行为,您可以使用自定义配置侦听器容器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发布失败的记录,处理器会将恢复的记录在原始主题/分区中的偏移量发送到事务。要启用此功能,commitRecoveredkafkaTemplateDefaultAfterRollbackProcessor.

如果恢复器失败(抛出异常),失败的记录将包含在搜索中。从版本 2.5.5 开始,如果恢复器失败,BackOff将默认重置,并且在再次尝试恢复之前重新交付将再次经历回退。对于早期版本,BackOff没有重置,并且在下一次失败时重新尝试恢复。要恢复到以前的行为,请将处理器的resetStateOnRecoveryFailure属性设置为false

从 2.6 版开始,您现在可以根据失败记录和/或异常为处理器提供一个BiFunction<ConsumerRecord<?, ?>, Exception, BackOff>来确定要使用的处理器:BackOff

handler.setBackOffFunction((record, ex) -> { ... });

如果函数返回null,将使用处理器的默认值BackOff

从版本 2.6.3 开始,如果异常类型在失败之间发生变化,则设置resetStateOnExceptionChangetrue并且重试序列将重新启动(包括选择新的BackOff,如果已配置)。默认情况下,不考虑异常类型。

从 2.3.1 版本开始,类似于DefaultErrorHandlerDefaultAfterRollbackProcessor认为某些异常是致命的,并且对此类异常跳过重试;在第一次失败时调用恢复器。默认情况下,被认为是致命的异常是:

  • 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版本开始,处理器可以配置一个或多个RetryListeners,接收重试和恢复进度的通知。

@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 版开始,当使用实现的ErrorHandleror时,可以将标头 ( ) 添加到记录中。此标头的值是一个从 1 开始递增的整数。接收原始数据时,该整数位于.AfterRollbackProcessorDeliveryAttemptAwareKafkaHeaders.DELIVERY_ATTEMPTkafka_deliveryAttemptConsumerRecord<?, ?>byte[4]

int delivery = ByteBuffer.wrap(record.headers()
    .lastHeader(KafkaHeaders.DELIVERY_ATTEMPT).value())
    .getInt()

@KafkaListenerDefaultKafkaHeaderMapperor一起使用时SimpleKafkaHeaderMapper,可以通过将@Header(KafkaHeaders.DELIVERY_ATTEMPT) int delivery其作为参数添加到侦听器方法中来获得。

要启用此标头的填充,请将容器属性设置deliveryAttemptHeadertrue。默认情况下禁用它以避免查找每条记录的状态和添加标题的(小)开销。

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 RecordInterceptororRecordFilterStrategy实现中使用时,标头在消费者记录中作为字节数组,使用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<?>.

有关使用批处理的更多信息,请参阅批处理侦听器。

发布死信记录

当达到记录的最大失败次数时,您可以使用记录恢复器配置DefaultErrorHandlerand 。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 版起)。

关键异常仅由DeserializationExceptions 引起,因此没有DLT_KEY_EXCEPTION_CAUSE_FQCN.

有两种机制可以添加更多标头。

  1. 子类化恢复器并覆盖createProducerRecord()- 调用super.createProducerRecord()并添加更多标头。

  2. 提供一个BiFunction接收消费者记录和异常,返回一个Headers对象;那里的标头将被复制到最终的生产者记录中;另请参阅管理死信记录标头。用于setHeadersFunction()设置BiFunction.

第二个实现起来更简单,但第一个有更多可用信息,包括已经组装的标准头文件。

从 2.3 版本开始,当与 an 一起使用时ErrorHandlingDeserializer,发布者会将value()死信生产者记录中的记录恢复为无法反序列化的原始值。以前,value()为 null 并且用户代码必须DeserializationException从消息头中解码 。此外,您可以向KafkaTemplate发布者提供多个 s;这可能是需要的,例如,如果您想发布byte[]from aDeserializationException以及使用与成功反序列化的记录不同的序列化程序的值。下面是一个使用KafkaTemplatesStringbyte[]序列化器配置发布者的示例:

@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 开始,如果异常类型在失败之间发生变化,则设置resetStateOnExceptionChangetrue并且重试序列将重新启动(包括选择新的BackOff,如果已配置)。默认情况下,不考虑异常类型。

从 2.3 版开始,恢复器也可以与 Kafka Streams 一起使用 -有关更多信息,请参阅从反序列化异常中恢复。

在标头中ErrorHandlingDeserializer添加反序列化异常ErrorHandlingDeserializer.VALUE_DESERIALIZER_EXCEPTION_HEADERErrorHandlingDeserializer.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

  • stripPreviousExceptionHeaderstrue(自 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 提供了ExponentialBackOffWithMaxRetrieswhich 是一个子类,它接收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.1.22。生产者和消费者记录日志

从版本 2.7.12、2.8.4 开始,您可以确定这些记录将如何在调试日志等中呈现。

请参阅KafkaUtils.setProducerRecordFormatter()KafkaUtils.setProducerRecordFormatter()了解更多信息。

4.2. Apache Kafka 流支持

从 1.1.4 版开始,Spring for Apache Kafka 为Kafka Streams提供一流的支持。要从 Spring 应用程序中使用它,kafka-streamsjar 必须存在于类路径中。它是 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 定义的所有流都与单个生命周期控制相关联。一旦实例被 关闭,它就无法重新启动。相反,必须创建一个新实例来重新启动流处理。 KafkaStreamsStreamsBuilderStreamsBuilderKafkaStreamsstreams.close()KafkaStreams

4.2.2. 弹簧管理

为了从 Spring 应用程序上下文的角度简化 Kafka Streams 的使用并通过容器使用生命周期管理,Spring for Apache Kafka 引入了StreamsBuilderFactoryBean. 这是一个AbstractFactoryBeanStreamsBuilder单例实例公开为 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 = trueStreamsBuilderFactoryBean,您必须在刷新应用程序上下文之前在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.StateListenerThread.UncaughtExceptionHandlerStateRestoreListener选项StreamsBuilderFactoryBean,它们被委派给内部KafkaStreams实例。此外,除了在 2.1.5 上间接设置这些选项外StreamsBuilderFactoryBean还可以使用KafkaStreamsCustomizer回调接口来配置内部KafkaStreams实例。请注意,它KafkaStreamsCustomizer会覆盖StreamsBuilderFactoryBean. 如果您需要KafkaStreams直接执行某些操作,您可以KafkaStreams使用StreamsBuilderFactoryBean.getKafkaStreams(). 您可以按类型自动装配StreamsBuilderFactoryBeanbean,但应确保在 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 流的有效负载(可以在需要实例的地方以类似的方式使用):JsonSerdeObjectMapperJsonSerdeCatJsonSerde

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会在应用程序上下文中自动声明。您也可以声明和使用任何其他StreamsBuilderFactoryBeanbean。您可以通过提供一个实现StreamsBuilderFactoryBeanConfigurer. 如果有多个这样的bean,它们将根据它们的Ordered.order属性应用。

默认情况下,当工厂 bean 停止时,KafkaStreams.cleanUp()会调用该方法。从版本 2.1.2 开始,工厂 bean 具有额外的构造函数,采用CleanupConfig具有属性的对象,让您控制是否在期间调用方法或不cleanUp()调用方法。从 2.7 版开始,默认是从不清理本地状态。start()stop()

4.2.7。标题丰富器

2.3 版添加HeaderEnricherTransformer. 这可用于在流处理中添加标头;标头值是 SpEL 表达式;表达式求值的根对象有 3 个属性:

  • context- the ProcessorContext,允许访问当前记录元数据

  • key- 当前记录的键

  • value- 当前记录的值

表达式必须返回 abyte[]或 a String(将转换为byte[]using UTF-8)。

要在流中使用扩充器:

.transform(() -> enricher)

变压器不改变keyvalue;它只是添加标题。

如果您的流是多线程的,则每条记录都需要一个新实例。
.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-testjar 包含一些有用的实用程序来帮助测试您的应用程序。

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 版开始,该consumerProps方法ConsumerConfig.AUTO_OFFSET_RESET_CONFIGearliest. 这是因为,在大多数情况下,您希望消费者使用在测试用例中发送的任何消息。ConsumerConfig默认值latest意味着在消费者开始之前已经由测试发送的消息将不会收到这些记录。要恢复到以前的行为,请latest在调用该方法后将属性设置为。

使用嵌入式代理时,通常最好的做法是为每个测试使用不同的主题,以防止串扰。如果由于某种原因无法做到这一点,请注意该consumeFromEmbeddedTopics方法的默认行为是在分配后将分配的分区搜索到开头。由于它无法访问消费者属性,因此您必须使用带有seekToEnd布尔参数的重载方法来寻找结尾而不是开头。

提供了一个 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具有五个分区,一个名为thing110 个分区的主题,一个名为thing215 个分区的主题:

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 的测试应用程序上下文缓存,还可以声明一个EmbeddedKafkaBrokerbean,因此可以跨多个测试类使用单个代理。为方便起见,我们提供了一个测试类级别的注解,调用@EmbeddedKafka它来注册EmbeddedKafkaBrokerbean。以下示例显示了如何使用它:

@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 端口属性。

以下示例设置支持属性占位符解析的topicsbrokerPropertiesbrokerPropertiesLocation属性:@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.propertiesbrokerPropertiesLocation. 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 binderspring-cloud-stream并且如果您想使用嵌入式代理进行测试,则必须删除spring-cloud-stream-test-support依赖项,因为它用测试用例的测试 binder 替换了真正的 binder。如果您希望某些测试使用测试绑定器,而某些测试使用嵌入式代理,则使用真实绑定器的测试需要通过在测试类中排除绑定器自动配置来禁用测试绑定器。以下示例显示了如何执行此操作:

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.autoconfigure.exclude="
    + "org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration")
public class MyApplicationTests {
    ...
}

在 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注释或EmbeddedKafkaBrokerBean

以下示例展示了如何使用@EmbeddedKafkaAnnotation 创建嵌入式代理:

@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.pollTimeoutContainerProperties.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,则抛出异常。
使用RetryTopicConfigurationbean

您还可以通过在带注释的类中创建RetryTopicConfigurationbean 来配置非阻塞重试支持。@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注释可以用于同一个主题,有或没有手动分区分配以及非阻塞重试,但只有一个配置将用于给定主题。最好使用单个RetryTopicConfigurationbean 来配置此类主题;如果多个@RetryableTopic注解用于同一主题,则它们都应具有相同的值,否则其中一个将应用于该主题的所有侦听器,而其他注解的值将被忽略。

4.4.4. 特征

大多数功能都可用于@RetryableTopic注释和RetryTopicConfigurationbean。

退避配置

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,因此如果您提供maxAttempts4 的值,则原始尝试加上 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);
}
默认行为是包含所有主题。
主题自动创建

除非另有说明,否则框架将使用NewTopicbean 使用的 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决定是追加还是替换标头。

默认情况下,它显式设置appendOriginalHeadersfalse保留.stripPreviousExceptionHeadersDeadLetterPublishingRecover

这意味着只有第一个“原始”和最后一个异常标头保留在默认配置中。这是为了避免在涉及许多重试步骤时创建过大的消息(例如,由于堆栈跟踪标头)。

有关详细信息,请参阅管理死信记录标头

要重新配置框架以对这些属性使用不同的设置,请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_ERRORFAIL_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 名称,使用RetryTopicConfigurationbean 您可以提供 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 之前的版本,可以替换标准RetryTopicConfigurerbean 并设置useLegacyFactoryConfigurertrue,如:

@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将在每次启动应用程序时加载所有记录。您还应该将容器设置AckModeMANUAL以防止容器为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;
    }

}

6. 其他资源

除了本参考文档之外,我们还推荐了一些其他资源,可以帮助您了解 Spring 和 Apache Kafka。

附录 A:覆盖 Spring Boot 依赖项

在 Spring Boot 应用程序中使用 Spring for Apache Kafka 时,Apache Kafka 依赖项版本由 Spring Boot 的依赖项管理确定。如果您希望使用不同版本的kafka-clientsorkafka-streams并使用嵌入式 kafka 代理进行测试,则需要覆盖 Spring Boot 依赖管理使用的版本并test为 Apache Kafka 添加两个工件。

Maven
Gradle
<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现在可用。

有关详细信息,请参阅侦听器容器属性

使用BackOffbetween 传递尝试(例如SeekToCurrentErrorHandlerDefaultAfterRollbackProcessor)的错误处理程序现在将在容器停止后立即退出回退间隔,而不是延迟停止。有关详细信息,请参阅回滚后处理器[seek-to-current]

错误处理程序和扩展后的回滚处理器FailedRecordProcessor现在可以配置一个或多个RetryListeners 以接收有关重试和恢复进度的信息。

有关详细信息,请参阅回滚后处理器[seek-to-current][recovering-batch-eh]

now在RecordInterceptor侦听器返回后(通常或通过抛出异常)调用了其他方法。它还有一个子界面ConsumerAwareRecordInterceptor。此外,现在还有一个BatchInterceptor用于批处理侦听器。有关详细信息,请参阅消息侦听器容器

B.1.4。@KafkaListener变化

您现在可以验证@KafkaHandler方法(类级侦听器)的有效负载参数。有关详细信息,请参阅@KafkaListener @Payload验证

您现在可以在 and 上设置属性,rawRecordHeader这会导致将 raw添加到转换后的. 这很有用,例如,如果您希望在侦听器错误处理程序中使用 a。有关详细信息,请参阅侦听器错误处理程序MessagingMessageConverterBatchMessagingMessageConverterConsumerRecordMessage<?>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变化

新方法createOrModifyTopicsdescribeTopics已添加。 KafkaAdmin.NewTopics已添加以方便在单个 bean 中配置多个主题。有关更多信息,请参阅配置主题

B.1.10。MessageConverter变化

现在可以在 中添加一个spring-messaging SmartMessageConverterMessagingMessageConverter允许基于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.1。卡夫卡客户端版本

此版本需要 2.6.0 kafka-clients

B.2.2。侦听器容器更改

默认EOSMode值为现在BETA。有关更多信息,请参阅Exactly Once 语义

各种错误处理程序(扩展FailedRecordProcessor)和DefaultAfterRollbackProcessor现在重置BackOff如果恢复失败。此外,您现在可以BackOff根据失败的记录和/或异常选择要使用的。有关详细信息,请参阅[seek-to-current][recovering-batch-eh]发布死信记录回滚后处理器

您现在可以adviceChain在容器属性中配置一个。有关详细信息,请参阅侦听器容器属性

当容器配置为发布时ListenerContainerIdleEvent,它现在ListenerContainerNoLongerIdleEvent在发布空闲事件后收到记录时发布。有关更多信息,请参阅应用程序事件检测空闲和非响应消费者

B.2.3。@KafkaListener 更改

使用手动分区分配时,您现在可以指定通配符来确定哪些分区应重置为初始偏移量。另外,如果监听器实现了ConsumerSeekAwareonPartitionsAssigned()在手动赋值之后调用。(也在 2.5.5 版中添加)。有关详细信息,请参阅显式分区分配

添加了方便的方法AbstractConsumerSeekAware以使搜索更容易。有关详细信息,请参阅寻求特定偏移量。

B.2.4。错误处理程序更改

如果异常的类型与先前使用此记录发生的异常类型不同,则(例如 、 )的子类现在可以配置为重置重FailedRecordProcessorSeekToCurrentErrorHandler状态。有关详细信息,请参阅[seek-to-current]After-rollback Processor[recovering-batch-eh]DefaultAfterRollbackProcessorRecoveringBatchErrorHandler

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.3。卡夫卡客户端版本

此版本需要 2.5.0 kafka-clients

B.3.4。类/包更改

SeekUtils已从o.s.k.support包中移至o.s.k.listener.

B.3.5。传递尝试标题

现在可以选择添加一个标头,该标头在使用某些错误处理程序时和回滚处理器后跟踪传递尝试。有关详细信息,请参阅传递尝试标头

B.3.6。@KafkaListener 更改

@KafkaListener当返回类型为时,如果需要,默认回复标头现在将自动填充Message<?>。有关详细信息,请参阅回复类型消息<?>

当传入记录有键时,KafkaHeaders.RECEIVED_MESSAGE_KEY不再填充值;标题被完全省略。nullnull

@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/ StringDeserializers 以及关联的。SerDe有关详细信息,请参阅字符串序列化

B.3.10。JsonDeserializer

现在JsonDeserializer可以更灵活地确定反序列化类型。有关详细信息,请参阅使用方法确定类型

B.3.11。委派串行器/解串器

DelegatingSerializer出站记录没有标题时,现在可以处理“标准”类型。有关更多信息,请参阅委托序列化器和反序列化器。

B.3.12。测试更改

KafkaTestUtils.consumerProps()助手记录现在默认设置ConsumerConfig.AUTO_OFFSET_RESET_CONFIGearliest。有关更多信息,请参阅JUnit

B.4。2.3 和 2.4 之间的变化

B.4.1。卡夫卡客户端版本

此版本需要 2.4.0kafka-clients或更高版本,并支持新的增量再平衡功能。

B.4.2。ConsumerAwareRebalanceListener

就像ConsumerRebalanceListener,这个接口现在多了一个方法onPartitionsLost。有关更多信息,请参阅 Apache Kafka 文档。

与 不同的是ConsumerRebalanceListener,默认实现调用onPartitionsRevoked. 相反,侦听器容器将在调用该方法后调用该方法onPartitionsLost;因此,在实施ConsumerAwareRebalanceListener.

B.4.3. 通用错误处理程序

默认实现现在isAckAfterHandle()默认返回 true。

B.4.4。卡夫卡模板

现在KafkaTemplate支持非事务性发布和事务性发布。有关详细信息,请参阅KafkaTemplate事务性和非事务性发布

B.4.5。聚合回复Kafka模板

现在releaseStrategy是一个BiConsumer. 现在在超时后(以及记录到达时)调用它;第二个参数是true在超时后调用的情况。

有关详细信息,请参阅聚合多个回复

B.4.6。侦听器容器

提供ContainerProperties了一个authorizationExceptionRetryInterval选项,让侦听器容器AuthorizationExceptionKafkaConsumer. 有关更多信息,请参阅其 JavaDocs 和UsingKafkaMessageListenerContainer

B.4.7. @KafkaListener

@KafkaListener注释有一个新属性splitIterables;默认为真。当回复侦听器返回Iterablethis 属性时,控制返回结果是作为单个记录发送还是作为每个元素的记录发送。有关更多信息,请参阅使用转发侦听器结果@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.2. 卡夫卡客户端版本

此版本需要 2.3.0kafka-clients或更高版本。

B.5.3. 类/包更改

TopicPartitionInitialOffset已弃用,取而代之的是TopicPartitionOffset.

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@KafkaListenerGenericMessageListenerListenerExecutionFailedExceptioncausegroup.id

因为侦听器容器有自己的提交偏移量的机制,所以它更喜欢 KafkaConsumerConfig.ENABLE_AUTO_COMMIT_CONFIGfalse. 它现在自动将其设置为 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 消息转换

JsonSerializerJsonDeserializer现在具有流畅的JsonSerdeAPI 以使编程配置更简单。有关更多信息,请参阅 javadocs、序列化、反序列化和消息转换,以及Streams JSON 序列化和反序列化

B.5.11。回复Kafka模板

当回复超时时,future 会异常完成,用 aKafkaReplyTimeoutException而不是 a KafkaException

此外,现在提供了一种重载sendAndReceive方法,允许基于每条消息指定回复超时。

B.5.12。聚合回复Kafka模板

ReplyingKafkaTemplate通过聚合来自多个接收者的回复来扩展。有关详细信息,请参阅聚合多个回复

B.5.13。交易变更

您现在可以覆盖 和 上的生产者transactionIdPrefix工厂。有关更多信息,请参阅。KafkaTemplateKafkaTransactionManagertransactionIdPrefix

B.5.14。新的委托序列化器/反序列化器

该框架现在提供了一个委托SerializerDeserializer,利用标头来生成和使用具有多种键/值类型的记录。有关更多信息,请参阅委托序列化器和反序列化器。

B.5.15。新的重试反序列化器

该框架现在提供了一个委托RetryingDeserializer,以在可能发生网络问题等暂时错误时重试序列化。有关详细信息,请参阅重试反序列化程序

B.6。2.1 和 2.2 之间的变化

B.6.1。卡夫卡客户端版本

此版本需要 2.0.0kafka-clients或更高版本。

B.6.2. 类和包更改

课程ContainerProperties已从org.springframework.kafka.listener.config移至。org.springframework.kafka.listener

枚举AckMode已从AbstractMessageListenerContainer移至。ContainerProperties

和方法已从两者移至setBatchErrorHandler()and 。setErrorHandler()ContainerPropertiesAbstractMessageListenerContainerAbstractKafkaListenerContainerFactory

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. 标头映射更改

类型MimeTypeMediaType的标头现在映射为RecordHeader值中的简单字符串。以前,它们被映射为 JSON 并且仅MimeType被解码。 MediaType无法解码。它们现在是用于互操作性的简单字符串。

此外,DefaultKafkaHeaderMapper还有一个新方法,允许使用而不是 JSONaddToStringClasses来指定应该映射的类型。toString()有关详细信息,请参阅消息标头

B.6.8. 嵌入式 Kafka 更改

该类KafkaEmbedded及其KafkaRule接口已被弃用,取而代之的是JUnit 4 包装器EmbeddedKafkaBroker及其 JUnit 4EmbeddedKafkaRule包装器。注释现在@EmbeddedKafka填充一个EmbeddedKafkaBrokerbean 而不是 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.1. 卡夫卡客户端版本

此版本需要 1.0.0kafka-clients或更高版本。

2.2 版原生支持 1.1.x 客户端。

B.7.2. JSON 改进

现在添加类型信息StringJsonMessageConverter,让转换器在接收时根据消息本身而不是固定配置类型创建特定类型。有关详细信息,请参阅序列化、反序列化和消息转换JsonSerializerHeadersJsonDeserializer

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.10。链式KafkaTransactionManager

2.1.3 版引入了ChainedKafkaTransactionManager. (现在已弃用)。

B.7.11。从 2.0 迁移指南

请参阅2.0 到 2.1 迁移指南。

B.8. 1.3 和 2.0 之间的变化

B.8.1. Spring 框架和 Java 版本

Spring for Apache Kafka 项目现在需要 Spring Framework 5.0 和 Java 8。

B.8.2. @KafkaListener变化

您现在可以@KafkaListener使用. 如果方法返回结果,则将其转发到指定主题。有关详细信息,请参阅使用转发侦听器结果@KafkaHandler@SendTo@SendTo

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.3. 创建主题

0.11.0.0 客户端库提供了一个AdminClient,您可以使用它来创建主题。使用KafkaAdmin此客户端自动添加定义为@Bean实例的主题。

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的属性设置为。idIsGroupfalse

B.9.6。@EmbeddedKafka注解

为方便起见,提供了一个测试类级别的@EmbeddedKafka注释,用于注册KafkaEmbedded为 bean。有关详细信息,请参阅测试应用程序

B.9.7. Kerberos 配置

现在提供了对配置 Kerberos 的支持。有关详细信息,请参阅JAAS 和 Kerberos

B.10。1.1 和 1.2 之间的变化

此版本使用 0.10.2.x 客户端。

B.11。1.0 和 1.1 之间的变化

B.11.1。卡夫卡客户端

此版本使用 Apache Kafka 0.10.xx 客户端。

B.11.2。批处理监听器

侦听器可以配置为接收操作返回的整批消息consumer.poll(),而不是一次接收一个。

B.11.3. 空负载

当您使用日志压缩时,Null 有效负载用于“删除”键。

B.11.4。初始偏移

当显式分配分区时,您现在可以为消费者组配置相对于当前位置的初始偏移量,而不是绝对或相对于当前端。

B.11.5。寻找

您现在可以查找每个主题或分区的位置。当使用组管理并且Kafka分配分区时,您可以使用它来设置初始化期间的初始位置。您还可以在检测到空闲容器时或在应用程序执行中的任意点进行查找。有关详细信息,请参阅寻求特定偏移量。


1. see XML Configuration