© 2016 - 2023 VMware, Inc.

このドキュメントのコピーは、あなた自身の使用および他者への配布のために作成することができますが、そのようなコピーに料金を請求しないこと、さらに、印刷物または電子的に配布されるかどうかにかかわらず、各コピーにこの著作権表示が含まれていることを条件とします。

1. 序文

Spring for Apache Kafka プロジェクトは、コア Spring の概念を Kafka ベースのメッセージングソリューションの開発に適用します。メッセージを送信するための高レベルの抽象化として「テンプレート」を提供します。また、メッセージ駆動型 POJO のサポートも提供します。

2. 新着情報

2.1. 2.9 以降の 3.0 の新機能

このセクションでは、バージョン 2.9 からバージョン 3.0 に加えられた変更について説明します。以前のバージョンでの変更については、変更履歴を参照してください。

2.1.1. Kafka クライアントバージョン

このバージョンには、3.3.1 kafka-clients が必要です。

2.1.2. 正確に一度セマンティクス

EOSMode.V1 (別名 ALPHA) はサポートされなくなりました。

トランザクションを使用する場合、ブローカーの最小バージョンは 2.5 です。

詳細については、正確に一度セマンティクスおよび KIP-447 [Apache] (英語) を参照してください。

2.1.3. 観測

タイマーの監視と Micrometer を使用したトレースの有効化がサポートされるようになりました。詳細については、Micrometer Observation を参照してください。

2.1.4. ネイティブイメージ

ネイティブイメージの作成がサポートされます。詳細については、ネイティブイメージを参照してください。

2.1.5. グローバルシングルエンベデッド Kafka

組み込み Kafka (EmbeddedKafkaBroker) は、テスト計画全体の単一のグローバルインスタンスとして開始できるようになりました。詳細については、複数のテストクラスに同じブローカーを使用するを参照してください。

2.1.6. 再試行可能なトピックの変更

この機能は (API に関する限り) 実験的とは見なされなくなりました。機能自体は 2.7 以降サポートされていますが、API の変更が中断される可能性が通常よりも大きくなっています。

ノンブロッキング再試行インフラストラクチャ Bean のブートストラップは、アプリケーションの初期化に関して一部のアプリケーションで発生したタイミングの問題を回避するために、このリリースで変更されました。

再試行コンテナーに別の concurrency を設定できるようになりました。デフォルトでは、並行性はメインコンテナーと同じです。

@RetryableTopic は、@AliasFor プロパティのサポートを含め、カスタムアノテーションのメタアノテーションとして使用できるようになりました。

詳細については、構成を参照してください。

再試行トピックのデフォルトのレプリケーション係数が -1 (ブローカーのデフォルトを使用) になりました。ブローカーがそのバージョン 2.4 より前の場合、プロパティを明示的に設定する必要があります。

同じアプリケーションコンテキスト内の同じトピックで複数の @RetryableTopic リスナーを構成できるようになりました。以前は、これは不可能でした。詳細については、複数のリスナー、同じトピックを参照してください。

RetryTopicConfigurationSupport には破壊的な API の変更があります。具体的には、destinationTopicResolverkafkaConsumerBackoffManager、/ または retryTopicConfigurer の Bean 定義メソッドをオーバーライドする場合。これらのメソッドには、ObjectProvider<RetryTopicComponentFactory> パラメーターが必要になりました。

2.1.7. リスナーコンテナーの変更

コンシューマーの認証と認可の失敗に関連するイベントが、コンテナーによって発行されるようになりました。詳細については、アプリケーションイベントを参照してください。

コンシューマースレッドで使用されるスレッド名をカスタマイズできるようになりました。詳細については、コンテナースレッドの命名を参照してください。

コンテナープロパティ restartAfterAuthException が追加されました。詳細については、リスナーコンテナーのプロパティを参照してください。

2.1.8. KafkaTemplate の変更

このクラスによって返される先物は、ListenableFuture ではなく CompletableFuture になりました。KafkaTemplate の使用を参照してください。

2.1.9. ReplyingKafkaTemplate の変更

このクラスによって返される先物は、ListenableFuture ではなく CompletableFuture になりました。ReplyingKafkaTemplate の使用および Message<?> でのリクエスト / リプライを参照してください。

2.1.10. @KafkaListener の変更

返信メッセージでエコーされるカスタム相関ヘッダーを使用できるようになりました。詳細については、ReplyingKafkaTemplate を使用するの最後にある注を参照してください。

2.1.11. KafkaHeaders の変更

2.9.x で非推奨となった KafkaHeaders の 4 つの定数が削除されました。

  • MESSAGE_KEY の代わりに KEY を使用します。

  • PARTITION_ID の代わりに PARTITION を使用してください

同様に、RECEIVED_MESSAGE_KEY は RECEIVED_KEY に置き換えられ、RECEIVED_PARTITION_ID は RECEIVED_PARTITION に置き換えられます。

2.1.12. 変更のテスト

バージョン 3.0.7 では、MockConsumerFactory および MockProducerFactory が導入されました。詳細については、"コンシューマーとプロデューサーのモックアップ" を参照してください。

3. 導入

リファレンスドキュメントのこの最初の部分は、Spring for Apache Kafka の概要とその基礎となる概念、およびできるだけ早く起動して実行するのに役立ついくつかのコードスニペットです。

3.1. クイックツアー

前提条件: Apache Kafka をインストールして実行する必要があります。次に、Spring for Apache Kafka (spring-kafka) JAR とそのすべての依存関係をクラスパスに配置する必要があります。これを行う最も簡単な方法は、ビルドツールで依存関係を宣言することです。

Spring Boot を使用していない場合は、プロジェクトの依存関係として spring-kafka jar を宣言してください。

Maven
<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
  <version>3.0.7</version>
</dependency>
Gradle
compile 'org.springframework.kafka:spring-kafka:3.0.7'
Spring Boot を使用している場合(および start.spring.io を使用してプロジェクトを作成していない場合)、バージョンを省略すると、Boot は Boot バージョンと互換性のある正しいバージョンを自動的に取り込みます。
Maven
<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
</dependency>
Gradle
compile 'org.springframework.kafka:spring-kafka'

ただし、開始する最も簡単な方法は、start.spring.io(または Spring ToolSuits と IntellijIDEA のウィザード)を使用してプロジェクトを作成し、依存関係として "Spring for Apache Kafka" を選択することです。

3.1.1. 互換性

このクイックツアーは、次のバージョンで機能します。

  • Apache Kafka クライアント 3.3.x

  • Spring Framework 6.0.x

  • Java の最小バージョン: 17

3.1.2. 入門

開始する最も簡単な方法は、start.spring.io(または Spring ToolSuits と IntellijIDEA のウィザード)を使用してプロジェクトを作成し、依存関係として "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);
    }

}
Kotlin
@SpringBootApplication
class Application {

    @Bean
    fun topic() = NewTopic("topic1", 10, 1)

    @KafkaListener(id = "myId", topics = ["topic1"])
    fun listen(value: String?) {
        println(value)
    }

}

fun main(args: Array<String>) = runApplication<Application>(*args)
例 2: application.properties
spring.kafka.consumer.auto-offset-reset=earliest

NewTopic Bean により、トピックがブローカー上に作成されます。トピックがすでに存在する場合は必要ありません。

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

}
Kotlin
@SpringBootApplication
class Application {

    @Bean
    fun topic() = NewTopic("topic1", 10, 1)

    @Bean
    fun runner(template: KafkaTemplate<String?, String?>) =
        ApplicationRunner { template.send("topic1", "test") }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) = runApplication<Application>(*args)
    }

}
Java 構成を使用 (Spring Boot なし)
Spring for Apache Kafka は、Spring アプリケーションコンテキストで使用するように設計されています。例: Spring コンテキストの外部でリスナーコンテナーを自分で作成した場合、コンテナーが実装するすべての …​Aware インターフェースを満たさない限り、すべての関数が機能するとは限りません。

SpringBoot を使用しないアプリケーションの例を次に示します。Consumer と Producer の両方があります。

例 4: Boot なし
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);
    }

}
Kotlin
class Sender(private val template: KafkaTemplate<Int, String>) {

    fun send(toSend: String, key: Int) {
        template.send("topic1", key, toSend)
    }

}

class Listener {

    @KafkaListener(id = "listen1", topics = ["topic1"])
    fun listen1(`in`: String) {
        println(`in`)
    }

}

@Configuration
@EnableKafka
class Config {

    @Bean
    fun kafkaListenerContainerFactory(consumerFactory: ConsumerFactory<Int, String>) =
        ConcurrentKafkaListenerContainerFactory<Int, String>().also { it.consumerFactory = consumerFactory }


    @Bean
    fun consumerFactory() = DefaultKafkaConsumerFactory<Int, String>(consumerProps)

    val consumerProps = mapOf(
        ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
        ConsumerConfig.GROUP_ID_CONFIG to "group",
        ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to IntegerDeserializer::class.java,
        ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
        ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest"
    )

    @Bean
    fun sender(template: KafkaTemplate<Int, String>) = Sender(template)

    @Bean
    fun listener() = Listener()

    @Bean
    fun producerFactory() = DefaultKafkaProducerFactory<Int, String>(senderProps)

    val senderProps = mapOf(
        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
        ProducerConfig.LINGER_MS_CONFIG to 10,
        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to IntegerSerializer::class.java,
        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java
    )

    @Bean
    fun kafkaTemplate(producerFactory: ProducerFactory<Int, String>) = KafkaTemplate(producerFactory)

}

ご覧のとおり、Spring Boot を使用しない場合は、いくつかのインフラストラクチャ Bean を定義する必要があります。

4. リファレンス

リファレンスドキュメントのこのパートでは、Spring for Apache Kafka を構成するさまざまなコンポーネントについて詳しく説明します。メインの章では、Spring を使用して Kafka アプリケーションを開発するためのコアクラスについて説明します。

4.1. Spring for Apache Kafka の使用

このセクションでは、Spring for Apache Kafka の使用に影響を与えるさまざまな関心事について詳しく説明します。簡単ですが詳細ではない導入については、クイックツアーを参照してください。

4.1.1. Kafka への接続

バージョン 2.5 以降、これらはそれぞれ KafkaResourceFactory を継承します。これにより、Supplier<String> を構成に追加することにより、実行時にブートストラップサーバーを変更できます: setBootstrapServersSupplier(() → …​)。これは、サーバーのリストを取得するために、すべての新しい接続に対して呼び出されます。コンシューマーとプロデューサーは一般的に長命です。既存のプロデューサーを閉じるには、DefaultKafkaProducerFactory で reset() を呼び出します。既存のコンシューマーを閉じるには、KafkaListenerEndpointRegistry で stop() (次に start())を呼び出すか、他のリスナーコンテナー Bean で stop() と start() を呼び出します。

便宜上、フレームワークは 2 セットのブートストラップサーバーをサポートする ABSwitchCluster も提供します。そのうちの 1 つはいつでもアクティブです。setBootstrapServersSupplier() を呼び出して、ABSwitchCluster を構成し、それをプロデューサーファクトリとコンシューマーファクトリ、および KafkaAdmin に追加します。切り替えたい場合は、primary() または secondary() を呼び出し、プロデューサーファクトリで reset() を呼び出して、新しい接続を確立します。コンシューマーの場合、stop() および start() はすべてリスナーコンテナーです。@KafkaListenerstop()start() を使用する場合は、KafkaListenerEndpointRegistry Bean。

詳細については、Javadoc を参照してください。

ファクトリリスナー

バージョン 2.5 以降、DefaultKafkaProducerFactory および DefaultKafkaConsumerFactory は、プロデューサーまたはコンシューマーが作成またはクローズされるたびに通知を受信するように Listener で構成できます。

プロデューサーファクトリリスナー
interface Listener<K, V> {

    default void producerAdded(String id, Producer<K, V> producer) {
    }

    default void producerRemoved(String id, Producer<K, V> producer) {
    }

}
コンシューマーファクトリリスナー
interface Listener<K, V> {

    default void consumerAdded(String id, Consumer<K, V> consumer) {
    }

    default void consumerRemoved(String id, Consumer<K, V> consumer) {
    }

}

いずれの場合も、id は、. で区切られたファクトリ beanName プロパティに client-id プロパティ(作成後に metrics() から取得)を追加することによって作成されます。

これらのリスナーは、たとえば、新しいクライアントが作成されたときに Micrometer KafkaClientMetrics インスタンスを作成してバインドするために使用できます(クライアントが閉じられたときに閉じます)。

フレームワークは、まさにそれを行うリスナーを提供します。Micrometer ネイティブメトリクスを参照してください。

4.1.2. トピックの構成

アプリケーションコンテキストで KafkaAdmin Bean を定義すると、ブローカーにトピックが自動的に追加されます。これを行うには、トピックごとに NewTopic @Bean をアプリケーションコンテキストに追加します。バージョン 2.3 では、このような Bean の作成をより便利にするために、新しいクラス TopicBuilder が導入されました。次の例は、その方法を示しています。

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();
}
Kotlin
@Bean
fun admin() = KafkaAdmin(mapOf(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092"))

@Bean
fun topic1() =
    TopicBuilder.name("thing1")
        .partitions(10)
        .replicas(3)
        .compact()
        .build()

@Bean
fun topic2() =
    TopicBuilder.name("thing2")
        .partitions(10)
        .replicas(3)
        .config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd")
        .build()

@Bean
fun topic3() =
    TopicBuilder.name("thing3")
        .assignReplicas(0, Arrays.asList(0, 1))
        .assignReplicas(1, Arrays.asList(1, 2))
        .assignReplicas(2, Arrays.asList(2, 0))
        .config(TopicConfig.COMPRESSION_TYPE_CONFIG, "zstd")
        .build()

バージョン 2.6 以降では、partitions() または replicas()、あるいはその両方を省略でき、ブローカーのデフォルトがこれらのプロパティに適用されます。この機能をサポートするには、ブローカーのバージョンが少なくとも 2.4.0 である必要があります。KIP-464 [Apache] (英語) を参照してください。

Java
@Bean
public NewTopic topic4() {
    return TopicBuilder.name("defaultBoth")
            .build();
}

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

@Bean
public NewTopic topic6() {
    return TopicBuilder.name("defaultRepl")
            .partitions(3)
            .build();
}
Kotlin
@Bean
fun topic4() = TopicBuilder.name("defaultBoth").build()

@Bean
fun topic5() = TopicBuilder.name("defaultPart").replicas(1).build()

@Bean
fun topic6() = TopicBuilder.name("defaultRepl").partitions(3).build()

バージョン 2.7 以降、単一の KafkaAdmin.NewTopics Bean 定義で複数の NewTopic を宣言できます。

Java
@Bean
public KafkaAdmin.NewTopics topics456() {
    return new NewTopics(
            TopicBuilder.name("defaultBoth")
                .build(),
            TopicBuilder.name("defaultPart")
                .replicas(1)
                .build(),
            TopicBuilder.name("defaultRepl")
                .partitions(3)
                .build());
}
Kotlin
@Bean
fun topics456() = KafkaAdmin.NewTopics(
    TopicBuilder.name("defaultBoth")
        .build(),
    TopicBuilder.name("defaultPart")
        .replicas(1)
        .build(),
    TopicBuilder.name("defaultRepl")
        .partitions(3)
        .build()
)
Spring Boot を使用する場合、KafkaAdmin Bean が自動的に登録されるため、必要なのは NewTopic (および / または NewTopics) @Bean のみです。

デフォルトでは、ブローカーが使用できない場合、メッセージはログに記録されますが、コンテキストは引き続きロードされます。プログラムで管理者の initialize() メソッドを呼び出して、後で再試行できます。この状態を致命的と見なす場合は、管理者の fatalIfBrokerNotAvailable プロパティを true に設定します。その後、コンテキストは初期化に失敗します。

ブローカーがそれをサポートしている場合(1.0.0 以降)、既存のトピックのパーティションが NewTopic.numPartitions より少ないことがわかった場合、管理者はパーティションの数を増やします。

バージョン 2.7 以降、KafkaAdmin は、実行時にトピックを作成および検査するためのメソッドを提供します。

  • createOrModifyTopics

  • describeTopics

より高度な機能については、AdminClient を直接使用できます。次の例は、その方法を示しています。

@Autowired
private KafkaAdmin admin;

...

    AdminClient client = AdminClient.create(admin.getConfigurationProperties());
    ...
    client.close();

4.1.3. メッセージ送信

このセクションでは、メッセージの送信方法について説明します。

KafkaTemplate を使用する

このセクションでは、KafkaTemplate を使用してメッセージを送信する方法について説明します。

概要

KafkaTemplate はプロデューサーをラップし、Kafka トピックにデータを送信するための便利なメソッドを提供します。次のリストは、KafkaTemplate の関連するメソッドを示しています。

CompletableFuture<SendResult<K, V>> sendDefault(V data);

CompletableFuture<SendResult<K, V>> sendDefault(K key, V data);

CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, K key, V data);

CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, Long timestamp, K key, V data);

CompletableFuture<SendResult<K, V>> send(String topic, V data);

CompletableFuture<SendResult<K, V>> send(String topic, K key, V data);

CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, V data);

CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, Long timestamp, K key, V data);

CompletableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record);

CompletableFuture<SendResult<K, V>> send(Message<?> message);

Map<MetricName, ? extends Metric> metrics();

List<PartitionInfo> partitionsFor(String topic);

<T> T execute(ProducerCallback<K, V, T> callback);

// Flush the producer.

void flush();

interface ProducerCallback<K, V, T> {

    T doInKafka(Producer<K, V> producer);

}

詳細については、Javadoc を参照してください。

バージョン 3.0 では、以前は ListenableFuture を返していたメソッドが CompletableFuture を返すように変更されました。移行を容易にするために、2.9 バージョンはメソッド usingCompletableFuture() を追加しました。これは、CompletableFuture 戻り値の型で同じメソッドを提供しました。この方法は使用できなくなりました。

sendDefault API では、デフォルトのトピックがテンプレートに提供されている必要があります。

API は、timestamp をパラメーターとして受け取り、このタイムスタンプをレコードに格納します。ユーザー提供のタイムスタンプがどのように保存されるかは、Kafka トピックで構成されたタイムスタンプの種類によって異なります。トピックが CREATE_TIME を使用するように構成されている場合、ユーザーが指定したタイムスタンプが記録されます(指定されていない場合は生成されます)。トピックが LOG_APPEND_TIME を使用するように構成されている場合、ユーザー指定のタイムスタンプは無視され、ブローカーはローカルブローカー時間を追加します。

metrics メソッドと partitionsFor メソッドは、基盤となる Producer [Apache] (英語) の同じメソッドに委譲します。execute メソッドは、基盤となる Producer [Apache] (英語) への直接アクセスを提供します。

テンプレートを使用するには、プロデューサーファクトリを構成し、テンプレートのコンストラクターで提供します。次の例は、その方法を示しています。

@Bean
public ProducerFactory<Integer, String> producerFactory() {
    return new DefaultKafkaProducerFactory<>(producerConfigs());
}

@Bean
public Map<String, Object> producerConfigs() {
    Map<String, Object> props = new HashMap<>();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    // See https://kafka.apache.org/documentation/#producerconfigs for more properties
    return props;
}

@Bean
public KafkaTemplate<Integer, String> kafkaTemplate() {
    return new KafkaTemplate<Integer, String>(producerFactory());
}

バージョン 2.5 以降、ファクトリの ProducerConfig プロパティをオーバーライドして、同じファクトリから異なるプロデューサー構成でテンプレートを作成できるようになりました。

@Bean
public KafkaTemplate<String, String> stringTemplate(ProducerFactory<String, String> pf) {
    return new KafkaTemplate<>(pf);
}

@Bean
public KafkaTemplate<String, byte[]> bytesTemplate(ProducerFactory<String, byte[]> pf) {
    return new KafkaTemplate<>(pf,
            Collections.singletonMap(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class));
}

型 ProducerFactory<?, ?> の Bean(Spring Boot によって自動構成されたものなど)は、さまざまなナロージェネリクス型で参照できることに注意してください。

標準の <bean/> 定義を使用してテンプレートを構成することもできます。

次に、テンプレートを使用するために、そのメソッドの 1 つを呼び出すことができます。

Message<?> パラメーターを指定してメソッドを使用する場合、トピック、パーティション、キー情報は、以下の項目を含むメッセージヘッダーで提供されます。

  • KafkaHeaders.TOPIC

  • KafkaHeaders.PARTITION

  • KafkaHeaders.KEY

  • KafkaHeaders.TIMESTAMP

メッセージペイロードはデータです。

オプションで、Future が完了するのを待つ代わりに、送信の結果(成功または失敗)を含む非同期コールバックを取得するように ProducerListener を使用して KafkaTemplate を構成できます。次のリストは、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 で構成されています。LoggingProducerListener はエラーをログに記録し、送信が成功しても何もしません。

便宜上、メソッドの 1 つだけを実装する場合に備えて、デフォルトのメソッド実装が提供されています。

send メソッドが CompletableFuture<SendResult> を返すことに注意してください。コールバックをリスナーに登録して、送信の結果を非同期で受信できます。次の例は、その方法を示しています。

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

SendResult には、ProducerRecord と RecordMetadata の 2 つのプロパティがあります。これらのオブジェクトについては、Kafka API のドキュメントを参照してください。

Throwable は KafkaProducerException にキャストできます。その failedProducerRecord プロパティには、失敗したレコードが含まれています。

結果を待つために送信スレッドをブロックしたい場合は、future の get() メソッドを呼び出すことができます。タイムアウトのあるメソッドを使用することをお勧めします。linger.ms を設定した場合は、待機する前に flush() を呼び出すか、便宜上、テンプレートに autoFlush パラメーターを持つコンストラクターがあり、送信ごとにテンプレートが flush() になるようにすることができます。フラッシングが必要になるのは、linger.ms プロデューサープロパティを設定していて、部分的なバッチをすぐに送信したい場合だけです。

サンプル

このセクションでは、Kafka にメッセージを送信する例を示します。

例 5: ノンブロッキング (非同期)
public void sendToKafka(final MyOutputData data) {
    final ProducerRecord<String, String> record = createRecord(data);

    CompletableFuture<SendResult<Integer, String>> future = template.send(record);
    future.whenComplete((result, ex) -> {
        if (ex == null) {
            handleSuccess(data);
        }
        else {
            handleFailure(data, record, ex);
        }
    });
}
ブロッキング (同期化)
public void sendToKafka(final MyOutputData data) {
    final ProducerRecord<String, String> record = createRecord(data);

    try {
        template.send(record).get(10, TimeUnit.SECONDS);
        handleSuccess(data);
    }
    catch (ExecutionException e) {
        handleFailure(data, record, e.getCause());
    }
    catch (TimeoutException | InterruptedException e) {
        handleFailure(data, record, e);
    }
}

ExecutionException の原因は、failedProducerRecord プロパティを持つ KafkaProducerException であることに注意してください。

RoutingKafkaTemplate を使用する

バージョン 2.5 以降では、RoutingKafkaTemplate を使用して、宛先 topic 名に基づいて実行時にプロデューサーを選択できます。

ルーティングテンプレートは、トランザクション、executeflushmetrics 操作についてトピックが不明であるため、これらの操作をサポートしていません。

テンプレートには、java.util.regex.Pattern から ProducerFactory<Object, Object> インスタンスへのマップが必要です。このマップは順番にトラバースされるため、順序付けする必要があります(LinkedHashMap など)。最初に、より具体的なパターンを追加する必要があります。

次の単純な Spring Boot アプリケーションは、同じテンプレートを使用して、それぞれが異なる値のシリアライザーを使用して、異なるトピックに送信する方法の例を示しています。

@SpringBootApplication
public class Application {

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

    @Bean
    public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context,
            ProducerFactory<Object, Object> pf) {

        // Clone the PF with a different Serializer, register with Spring for shutdown
        Map<String, Object> configs = new HashMap<>(pf.getConfigurationProperties());
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);
        DefaultKafkaProducerFactory<Object, Object> bytesPF = new DefaultKafkaProducerFactory<>(configs);
        context.registerBean(DefaultKafkaProducerFactory.class, "bytesPF", bytesPF);

        Map<Pattern, ProducerFactory<Object, Object>> map = new LinkedHashMap<>();
        map.put(Pattern.compile("two"), bytesPF);
        map.put(Pattern.compile(".+"), pf); // Default PF with StringSerializer
        return new RoutingKafkaTemplate(map);
    }

    @Bean
    public ApplicationRunner runner(RoutingKafkaTemplate routingTemplate) {
        return args -> {
            routingTemplate.send("one", "thing1");
            routingTemplate.send("two", "thing2".getBytes());
        };
    }

}

この例に対応する @KafkaListener は、アノテーションプロパティに示されています。

同様の結果を達成するための別の手法については、同じトピックに異なる型を送信する追加機能を使用して、シリアライザーとデシリアライザーの委譲を参照してください。

DefaultKafkaProducerFactory を使用する

KafkaTemplate を使用するに見られるように、ProducerFactory はプロデューサーを作成するために使用されます。

トランザクションを使用しない場合、デフォルトでは、KafkaProducer javadoc で推奨されているように、DefaultKafkaProducerFactory はすべてのクライアントで使用されるシングルトンプロデューサーを作成します。ただし、テンプレートで flush() を呼び出すと、同じプロデューサーを使用する他のスレッドで遅延が発生する可能性があります。バージョン 2.3 以降、DefaultKafkaProducerFactory には新しいプロパティ producerPerThread があります。true に設定すると、この課題を回避するために、ファクトリはスレッドごとに個別のプロデューサーを作成(およびキャッシュ)します。

producerPerThread が true ある場合はプロデューサーが不要になった際に、ユーザーコードは、提供時に closeThreadBoundProducer() を呼び出す必要があります。これにより、プロデューサーが物理的に閉じられ、ThreadLocal から削除されます。reset() または destroy() を呼び出しても、これらのプロデューサーはクリーンアップされません。

DefaultKafkaProducerFactory を作成する場合、プロパティのマップのみを取り込むコンストラクター(KafkaTemplate の使用の例を参照)を呼び出すことにより、構成からキーや値の Serializer クラスを取得できます。または、Serializer インスタンスを DefaultKafkaProducerFactory コンストラクターに渡すこともできます(すべての Producer が同じインスタンスを共有する場合)。または、Producer ごとに個別の Serializer インスタンスを取得するために使用される Supplier<Serializer> (バージョン 2.3 以降)を提供することもできます。

@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() を呼び出して既存のプロデューサーを閉じ、新しいプロパティを使用して新しいプロデューサーが作成されるようにします。注: トランザクションプロデューサーファクトリを非トランザクションに変更したり、その逆を行ったりすることはできません。

2 つの新しいメソッドが提供されるようになりました。

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

void removeConfig(String configKey);

バージョン 2.8 以降、シリアライザーをオブジェクトとして(コンストラクター内または setter 経由で)提供する場合、ファクトリは configure() メソッドを呼び出して、構成プロパティを使用して構成します。

ReplyingKafkaTemplate を使用する

バージョン 2.1.3 は、リクエスト / 応答セマンティクスを提供するために KafkaTemplate のサブクラスを導入しました。このクラスの名前は ReplyingKafkaTemplate で、2 つの追加メソッドがあります。以下にメソッドシグネチャーを示します。

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

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

Message<?> を使用したリクエスト / 応答も参照してください)。

結果は CompletableFuture であり、結果(またはタイムアウトの場合は例外)が非同期で入力されます。結果には、KafkaTemplate.send() を呼び出した結果である sendFuture プロパティも含まれます。この future を使用して、送信操作の結果を判別できます。

バージョン 3.0 では、これらのメソッド (およびその sendFuture プロパティ) によって返される先物は、ListenableFuture ではなく CompletableFuture に変更されました。

最初の方法が使用される場合、または replyTimeout 引数が null の場合、テンプレートの defaultReplyTimeout プロパティが使用されます(デフォルトでは 5 秒)。

バージョン 2.8.8 以降、テンプレートには新しいメソッド waitForAssignment があります。これは、応答コンテナーが auto.offset.reset=latest で構成されている場合に役立ち、コンテナーが初期化される前にリクエストと応答が送信されないようにします。

手動のパーティション割り当て(グループ管理なし)を使用する場合、最初のポーリングが完了するまで通知が送信されないため、待機時間はコンテナーの pollTimeout プロパティよりも長くする必要があります。

次の Spring Boot アプリケーションは、この機能の使用方法の例を示しています。

@SpringBootApplication
public class KRequestingApplication {

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

    @Bean
    public ApplicationRunner runner(ReplyingKafkaTemplate<String, String, String> template) {
        return args -> {
            if (!template.waitForAssignment(Duration.ofSeconds(10))) {
                throw new IllegalStateException("Reply container did not initialize");
            }
            ProducerRecord<String, String> record = new ProducerRecord<>("kRequests", "foo");
            RequestReplyFuture<String, String, String> replyFuture = template.sendAndReceive(record);
            SendResult<String, String> sendResult = replyFuture.getSendFuture().get(10, TimeUnit.SECONDS);
            System.out.println("Sent ok: " + sendResult.getRecordMetadata());
            ConsumerRecord<String, String> consumerRecord = replyFuture.get(10, TimeUnit.SECONDS);
            System.out.println("Return value: " + consumerRecord.value());
        };
    }

    @Bean
    public ReplyingKafkaTemplate<String, String, String> replyingTemplate(
            ProducerFactory<String, String> pf,
            ConcurrentMessageListenerContainer<String, String> repliesContainer) {

        return new ReplyingKafkaTemplate<>(pf, repliesContainer);
    }

    @Bean
    public ConcurrentMessageListenerContainer<String, String> repliesContainer(
            ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {

        ConcurrentMessageListenerContainer<String, String> repliesContainer =
                containerFactory.createContainer("kReplies");
        repliesContainer.getContainerProperties().setGroupId("repliesGroup");
        repliesContainer.setAutoStartup(false);
        return repliesContainer;
    }

    @Bean
    public NewTopic kRequests() {
        return TopicBuilder.name("kRequests")
            .partitions(10)
            .replicas(2)
            .build();
    }

    @Bean
    public NewTopic kReplies() {
        return TopicBuilder.name("kReplies")
            .partitions(10)
            .replicas(2)
            .build();
    }

}

Boot の自動構成されたコンテナーファクトリを使用して応答コンテナーを作成できることに注意してください。

重要なデシリアライザーが応答に使用されている場合は、構成済みのデシリアライザーに委譲する ErrorHandlingDeserializer の使用を検討してください。このように構成すると、RequestReplyFuture は例外的に完了し、DeserializationException を cause プロパティに含めることで ExecutionException をキャッチできます。

バージョン 2.6.7 以降、DeserializationException の検出に加えて、テンプレートは、提供されている場合、replyErrorChecker 関数を呼び出します。例外が返された場合、将来は例外的に完了します。

次に例を示します。

template.setReplyErrorChecker(record -> {
    Header error = record.headers().lastHeader("serverSentAnError");
    if (error != null) {
        return new MyException(new String(error.value()));
    }
    else {
        return null;
    }
});

...

RequestReplyFuture<Integer, String, String> future = template.sendAndReceive(record);
try {
    future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
    ConsumerRecord<Integer, String> consumerRecord = future.get(10, TimeUnit.SECONDS);
    ...
}
catch (InterruptedException e) {
    ...
}
catch (ExecutionException e) {
    if (e.getCause instanceof MyException) {
        ...
    }
}
catch (TimeoutException e) {
    ...
}

テンプレートはヘッダー(デフォルトでは KafkaHeaders.CORRELATION_ID という名前)を設定します。これはサーバー側でエコーバックする必要があります。

この場合、次の @KafkaListener アプリケーションが応答します。

@SpringBootApplication
public class KReplyingApplication {

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

    @KafkaListener(id="server", topics = "kRequests")
    @SendTo // use default replyTo expression
    public String listen(String in) {
        System.out.println("Server received: " + in);
        return in.toUpperCase();
    }

    @Bean
    public NewTopic kRequests() {
        return TopicBuilder.name("kRequests")
            .partitions(10)
            .replicas(2)
            .build();
    }

    @Bean // not required if Jackson is on the classpath
    public MessagingMessageConverter simpleMapperConverter() {
        MessagingMessageConverter messagingMessageConverter = new MessagingMessageConverter();
        messagingMessageConverter.setHeaderMapper(new SimpleKafkaHeaderMapper());
        return messagingMessageConverter;
    }

}

@KafkaListener インフラストラクチャは相関 ID をエコーし、応答トピックを決定します。

返信の送信の詳細については、@SendTo を使用したリスナー結果の転送を参照してください。テンプレートは、デフォルトのヘッダー KafKaHeaders.REPLY_TOPIC を使用して、返信先のトピックを示します。

バージョン 2.2 以降、テンプレートは、構成された応答コンテナーから応答トピックまたはパーティションを検出しようとします。コンテナーが単一のトピックまたは単一の TopicPartitionOffset をリッスンするように構成されている場合、コンテナーは応答ヘッダーを設定するために使用されます。コンテナーが別の方法で構成されている場合、ユーザーは応答ヘッダーを設定する必要があります。この場合、INFO ログメッセージが初期化中に書き込まれます。次の例では、KafkaHeaders.REPLY_TOPIC を使用しています。

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

単一の応答 TopicPartitionOffset で構成する場合、各インスタンスが異なるパーティションでリッスンする限り、複数のテンプレートに同じ応答トピックを使用できます。単一の応答トピックで構成する場合、各インスタンスは異なる group.id を使用する必要があります。この場合、すべてのインスタンスが各応答を受信しますが、リクエストを送信したインスタンスのみが相関 ID を検出します。これは自動スケーリングに役立つ場合がありますが、追加のネットワークトラフィックのオーバーヘッドと、不要な応答を破棄するためのわずかなコストが伴います。この設定を使用する場合は、テンプレートの sharedReplyTopic を true に設定することをお勧めします。これにより、デフォルトの ERROR ではなく DEBUG への予期しない応答のログレベルが低下します。

以下は、同じ共有返信トピックを使用するように返信コンテナーを構成する例です。

@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 には、4 バイトの int(ビッグエンディアン)が含まれています。サーバーはこのヘッダーを使用して、応答を正しいパーティションにルーティングする必要があります(@KafkaListener がこれを行います)。ただし、この場合、応答コンテナーは Kafka のグループ管理機能を使用してはならず、固定パーティションでリッスンするように構成する必要があります(ContainerProperties コンストラクターで TopicPartitionOffset を使用することにより)。
DefaultKafkaHeaderMapper では、Jackson がクラスパス上にある必要があります(@KafkaListener の場合)。使用できない場合、メッセージコンバーターにはヘッダーマッパーがないため、前に示したように、SimpleKafkaHeaderMapper を使用して MessagingMessageConverter を構成する必要があります。

デフォルトでは、3 つのヘッダーが使用されます。

  • KafkaHeaders.CORRELATION_ID - 応答をリクエストに関連付けるために使用されます

  • KafkaHeaders.REPLY_TOPIC - サーバーに返信先を指示するために使用されます

  • KafkaHeaders.REPLY_PARTITION - (オプション)どのパーティションに応答するかをサーバーに指示するために使用されます

これらのヘッダー名は、@KafkaListener インフラストラクチャーが応答をルーティングするために使用します。

バージョン 2.3 以降、ヘッダー名をカスタマイズできます。テンプレートには 3 つのプロパティ correlationHeaderNamereplyTopicHeaderNamereplyPartitionHeaderName があります。これは、サーバーが Spring アプリケーションではない(または @KafkaListener を使用していない)場合に役立ちます。

逆に、リクエスト元のアプリケーションが Spring アプリケーションではなく、バージョン 3.0 以降の別のヘッダーに相関情報を配置する場合、リスナーコンテナーファクトリでカスタム correlationHeaderName を構成すると、そのヘッダーがエコーバックされます。以前は、リスナーはカスタム相関ヘッダーをエコーする必要がありました。
Message<?> を使用したリクエスト / 応答

バージョン 2.7 は、spring-messaging の Message<?> 抽象化を送受信するためのメソッドを ReplyingKafkaTemplate に追加しました。

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

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

これらはテンプレートのデフォルトの replyTimeout を使用します。また、メソッド呼び出しでタイムアウトが発生する可能性のあるオーバーロードされたバージョンもあります。

バージョン 3.0 では、これらのメソッド (およびその sendFuture プロパティ) によって返される先物は、ListenableFuture ではなく CompletableFuture に変更されました。

コンシューマーの Deserializer またはテンプレートの MessageConverter が、構成を介して、または応答メッセージにメタデータを入力することにより、追加情報なしでペイロードを変換できる場合は、最初の方法を使用します。

メッセージコンバーターを支援するために、戻り値の型の型情報を提供する必要がある場合は、2 番目の方法を使用してください。これにより、サーバー側が 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;
}
Kotlin
@Bean
fun template(
    pf: ProducerFactory<String?, String>?,
    factory: ConcurrentKafkaListenerContainerFactory<String?, String?>
): ReplyingKafkaTemplate<String?, String, String?> {
    val replyContainer = factory.createContainer("replies")
    replyContainer.containerProperties.groupId = "request.replies"
    val template = ReplyingKafkaTemplate(pf, replyContainer)
    template.messageConverter = ByteArrayJsonMessageConverter()
    template.defaultTopic = "requests"
    return template
}
例 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()));
Kotlin
val future1: RequestReplyTypedMessageFuture<String?, String?, Thing?>? =
    template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(),
        object : ParameterizedTypeReference<Thing?>() {})
log.info(future1?.sendFuture?.get(10, TimeUnit.SECONDS)?.recordMetadata?.toString())
val thing = future1?.get(10, TimeUnit.SECONDS)?.payload
log.info(thing.toString())

val future2: RequestReplyTypedMessageFuture<String?, String?, List<Thing?>?>? =
    template.sendAndReceive(MessageBuilder.withPayload("getThings").build(),
        object : ParameterizedTypeReference<List<Thing?>?>() {})
log.info(future2?.sendFuture?.get(10, TimeUnit.SECONDS)?.recordMetadata.toString())
val things = future2?.get(10, TimeUnit.SECONDS)?.payload
things?.forEach(Consumer { thing1: Thing? -> log.info(thing1.toString()) })
返信型 Message<?>

@KafkaListener が 2.5 より前のバージョンの Message<?> を返す場合、応答トピックと相関 ID ヘッダーを設定する必要がありました。この例では、リクエストの返信トピックヘッダーを使用します。

@KafkaListener(id = "requestor", topics = "request")
@SendTo
public Message<?> messageReturn(String in) {
    return MessageBuilder.withPayload(in.toUpperCase())
            .setHeader(KafkaHeaders.TOPIC, replyTo)
            .setHeader(KafkaHeaders.KEY, 42)
            .setHeader(KafkaHeaders.CORRELATION_ID, correlation)
            .build();
}

これは、応答レコードにキーを設定する方法も示しています。

バージョン 2.5 以降、フレームワークはこれらのヘッダーが欠落しているかどうかを検出し、トピック(@SendTo 値から決定されたトピックまたは受信 KafkaHeaders.REPLY_TOPIC ヘッダー(存在する場合))を入力します。また、存在する場合は、受信 KafkaHeaders.CORRELATION_ID および KafkaHeaders.REPLY_PARTITION をエコーします。

@KafkaListener(id = "requestor", topics = "request")
@SendTo  // default REPLY_TOPIC header
public Message<?> messageReturn(String in) {
    return MessageBuilder.withPayload(in.toUpperCase())
            .setHeader(KafkaHeaders.KEY, 42)
            .build();
}
複数の返信を集約する

ReplyingKafkaTemplate を使用するのテンプレートは、厳密には単一のリクエスト / 応答シナリオ用です。1 つのメッセージの複数の受信者が応答を返す場合は、AggregatingReplyingKafkaTemplate を使用できます。これは、Scatter-Gather Enterprise Integration パターン (英語) のクライアント側の実装です。

ReplyingKafkaTemplate と同様に、AggregatingReplyingKafkaTemplate コンストラクターは、プロデューサーファクトリとリスナーコンテナーを使用して応答を受け取ります。応答を受信するたびに参照される 3 番目のパラメーター BiPredicate<List<ConsumerRecord<K, R>>, Boolean> releaseStrategy があります。述語が true を返す場合、ConsumerRecord のコレクションは、sendAndReceive メソッドによって返される Future を完了するために使用されます。

追加のプロパティ returnPartialOnTimeout があります(デフォルトは false)。これが true に設定されている場合、KafkaReplyTimeoutException で future を完了する代わりに、部分的な結果が正常に future を完了します(少なくとも 1 つの応答レコードが受信されている場合)。

バージョン 2.3.5 以降、述語はタイムアウト後にも呼び出されます(returnPartialOnTimeout が true の場合)。最初の引数は、現在のレコードのリストです。この呼び出しがタイムアウトによるものである場合、2 番目は true です。述語はレコードのリストを変更できます。

AggregatingReplyingKafkaTemplate<Integer, String, String> template =
        new AggregatingReplyingKafkaTemplate<>(producerFactory, container,
                        coll -> coll.size() == releaseSize);
...
RequestReplyFuture<Integer, String, Collection<ConsumerRecord<Integer, String>>> future =
        template.sendAndReceive(record);
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
ConsumerRecord<Integer, Collection<ConsumerRecord<Integer, String>>> consumerRecord =
        future.get(30, TimeUnit.SECONDS);

戻り値の型は、ConsumerRecord のコレクションである値を持つ ConsumerRecord であることに注意してください。「外部」 ConsumerRecord は「実際の」レコードではなく、リクエストに対して受信した実際の応答レコードのホルダーとして、テンプレートによって合成されます。通常のリリースが発生すると(リリース戦略が true を返す)、トピックは aggregatedResults に設定されます。returnPartialOnTimeout が true で、タイムアウトが発生した場合(および、少なくとも 1 つの応答レコードが受信された場合)、トピックは partialResultsAfterTimeout に設定されます。テンプレートは、これらの「トピック」名に一定の静的変数を提供します。

/**
 * Pseudo topic name for the "outer" {@link ConsumerRecords} that has the aggregated
 * results in its value after a normal release by the release strategy.
 */
public static final String AGGREGATED_RESULTS_TOPIC = "aggregatedResults";

/**
 * Pseudo topic name for the "outer" {@link ConsumerRecords} that has the aggregated
 * results in its value after a timeout.
 */
public static final String PARTIAL_RESULTS_AFTER_TIMEOUT_TOPIC = "partialResultsAfterTimeout";

Collection の実際の ConsumerRecord には、応答を受信する実際のトピックが含まれています。

応答のリスナーコンテナーは、AckMode.MANUAL または AckMode.MANUAL_IMMEDIATE で構成する必要があります。コンシューマープロパティ enable.auto.commit は false (バージョン 2.3 以降のデフォルト)である必要があります。メッセージが失われる可能性を回避するために、テンプレートは、未処理のリクエストがゼロの場合、つまり、最後の未処理のリクエストがリリース戦略によってリリースされた場合にのみオフセットをコミットします。リバランス後、返信が重複して配信される機能があります。これらは、実行中のリクエストでは無視されます。すでにリリースされている返信に対して重複した返信を受信すると、エラーログメッセージが表示される場合があります。
この集約テンプレートで ErrorHandlingDeserializer を使用する場合、フレームワークは DeserializationException を自動的に検出しません。代わりに、(null 値を持つ)レコードは、ヘッダーの逆直列化例外を除いて、そのまま返されます。アプリケーションは、ユーティリティメソッド ReplyingKafkaTemplate.checkDeserialization() メソッドを呼び出して、逆直列化例外が発生したかどうかを判断することをお勧めします。詳細については、その javadoc を参照してください。replyErrorChecker も、この集約テンプレートでは呼び出されません。応答の各要素に対してチェックを実行する必要があります。

4.1.4. メッセージの受信

MessageListenerContainer を構成してメッセージリスナーを提供するか、@KafkaListener アノテーションを使用することにより、メッセージを受信できます。

メッセージリスナー

メッセージリスナーコンテナーを使用する場合は、データを受信するためのリスナーを提供する必要があります。現在、メッセージリスナー用に 8 つのインターフェースがサポートされています。次のリストは、これらのインターフェースを示しています。

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 自動コミットまたはコンテナー管理のコミット方法のいずれかを使用する場合、Kafka コンシューマー poll() 操作から受け取った個々の ConsumerRecord インスタンスを処理するには、このインターフェースを使用します。
2 手動コミット方法の 1 つを使用する場合、Kafka コンシューマー poll() 操作から受け取った個々の ConsumerRecord インスタンスを処理するには、このインターフェースを使用します。
3 自動コミットまたはコンテナー管理のコミット方法のいずれかを使用する場合、Kafka コンシューマー poll() 操作から受け取った個々の ConsumerRecord インスタンスを処理するには、このインターフェースを使用します。Consumer オブジェクトへのアクセスが提供されます。
4 手動コミット方法の 1 つを使用する場合、Kafka コンシューマー poll() 操作から受け取った個々の ConsumerRecord インスタンスを処理するには、このインターフェースを使用します。Consumer オブジェクトへのアクセスが提供されます。
5 自動コミットまたはコンテナー管理のコミット方法のいずれかを使用する場合、Kafka コンシューマー poll() 操作から受け取ったすべての ConsumerRecord インスタンスを処理するために、このインターフェースを使用します。リスナーには完全なバッチが与えられるため、このインターフェースを使用する場合、AckMode.RECORD はサポートされません。
6 手動コミット方法の 1 つを使用する場合、Kafka コンシューマー poll() 操作から受け取ったすべての ConsumerRecord インスタンスを処理するために、このインターフェースを使用します。
7 自動コミットまたはコンテナー管理のコミット方法のいずれかを使用する場合、Kafka コンシューマー poll() 操作から受け取ったすべての ConsumerRecord インスタンスを処理するために、このインターフェースを使用します。リスナーには完全なバッチが与えられるため、このインターフェースを使用する場合、AckMode.RECORD はサポートされません。Consumer オブジェクトへのアクセスが提供されます。
8 手動コミット方法の 1 つを使用する場合、Kafka コンシューマー poll() 操作から受け取ったすべての ConsumerRecord インスタンスを処理するために、このインターフェースを使用します。Consumer オブジェクトへのアクセスが提供されます。
Consumer オブジェクトはスレッドセーフではありません。リスナーを呼び出すスレッドでのみメソッドを呼び出す必要があります。
リスナー内のコンシューマーの位置やコミットされたオフセットに影響を与える Consumer<?, ?> メソッドを実行しないでください。コンテナーはそのような情報を管理する必要があります。
メッセージリスナコンテナー

2 つの MessageListenerContainer 実装が提供されています。

  • KafkaMessageListenerContainer

  • ConcurrentMessageListenerContainer

KafkaMessageListenerContainer は、単一のスレッド上のすべてのトピックまたはパーティションからすべてのメッセージを受信します。ConcurrentMessageListenerContainer は、1 つ以上の KafkaMessageListenerContainer インスタンスに委譲して、マルチスレッドの消費を提供します。

バージョン 2.2.7 以降、リスナーコンテナーに RecordInterceptor を追加できます。リスナーを呼び出す前に呼び出され、インスペクションまたはレコードの変更が許可されます。インターセプターが null を返す場合、リスナーは呼び出されません。バージョン 2.7 以降、リスナーの終了後に(通常、例外をスローすることによって)呼び出される追加のメソッドがあります。また、バージョン 2.7 以降、BatchInterceptor があり、バッチリスナーに同様の機能を提供します。さらに、ConsumerAwareRecordInterceptor (および BatchInterceptor)は Consumer<?, ?> へのアクセスを提供します。これは、たとえば、インターセプターのコンシューマーメトリクスにアクセスするために使用される場合があります。

これらのインターセプターでコンシューマーの位置やコミットされたオフセットに影響を与えるメソッドを実行しないでください。コンテナーはそのような情報を管理する必要があります。
インターセプターが(新しいレコードを作成することによって)レコードを変更する場合、レコードの損失などの予期しない副作用を回避するために、topicpartitionoffset は同じままである必要があります。

CompositeRecordInterceptor および CompositeBatchInterceptor は、複数のインターセプターを呼び出すために使用できます。

デフォルトでは、バージョン 2.8 以降、トランザクションを使用する場合、トランザクションが開始される前にインターセプターが呼び出されます。代わりに、リスナーコンテナーの interceptBeforeTx プロパティを false に設定して、トランザクションの開始後にインターセプターを呼び出すことができます。バージョン 2.9 以降、これは KafkaAwareTransactionManager だけでなく、すべてのトランザクションマネージャーに適用されます。これにより、たとえば、コンテナーによって開始された JDBC トランザクションにインターセプターが参加できるようになります。

バージョン 2.3.8、2.4.6 以降、同時実行性が 1 より大きい場合、ConcurrentMessageListenerContainer は静的メンバーシップ [Apache] (英語) をサポートするようになりました。group.instance.id の接尾辞は -n で、n は 1 で始まります。これは、session.timeout.ms の増加とともに、たとえばアプリケーションインスタンスが再起動されたときなど、リバランスイベントを減らすために使用できます。

KafkaMessageListenerContainer を使用する

次のコンストラクターを使用できます。

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

ConsumerFactory と、トピックとパーティションに関する情報、およびその他の構成を ContainerProperties オブジェクトで受け取ります。ContainerProperties には、次のコンストラクターがあります。

public ContainerProperties(TopicPartitionOffset... topicPartitions)

public ContainerProperties(String... topics)

public ContainerProperties(Pattern topicPattern)

最初のコンストラクターは、TopicPartitionOffset 引数の配列を受け取り、使用するパーティションについてコンテナーに明示的に指示し(コンシューマー assign() メソッドを使用)、オプションの初期オフセットを使用します。正の値は、デフォルトでは絶対オフセットです。負の値は、デフォルトでパーティション内の現在の最後のオフセットを基準にしています。追加の boolean 引数を取る TopicPartitionOffset のコンストラクターが提供されています。これが true の場合、初期オフセット(正または負)は、このコンシューマーの現在の位置を基準にしています。オフセットは、コンテナーの開始時に適用されます。2 つ目はトピックの配列を取り、Kafka は group.id プロパティに基づいてパーティションを割り当てます。グループ全体にパーティションを分散します。3 つ目は、正規表現 Pattern を使用してトピックを選択します。

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 コンストラクターに渡すこともできます。その場合、すべてのコンシューマーが同じインスタンスを共有します。別のオプションは、Consumer ごとに個別の Deserializer インスタンスを取得するために使用される Supplier<Deserializer> (バージョン 2.3 以降)を提供することです。

DefaultKafkaConsumerFactory<Integer, CustomValue> cf =
                        new DefaultKafkaConsumerFactory<>(consumerProps(), null, () -> new CustomValueDeserializer());
KafkaMessageListenerContainer<Integer, String> container =
                        new KafkaMessageListenerContainer<>(cf, containerProps);
return container;

設定できるさまざまなプロパティの詳細については、ContainerProperties の Javadoc を参照してください。

バージョン 2.1.1 以降、logContainerConfig と呼ばれる新しいプロパティが使用可能になりました。true および INFO ロギングが有効になっている場合、各リスナーコンテナーは、その構成プロパティを要約したログメッセージを書き込みます。

デフォルトでは、トピックオフセットコミットのロギングは DEBUG ロギングレベルで実行されます。バージョン 2.1.2 以降、commitLogLevel と呼ばれる ContainerProperties のプロパティを使用して、これらのメッセージのログレベルを指定できます。例: ログレベルを INFO に変更するには、containerProperties.setCommitLogLevel(LogIfLevelEnabled.Level.INFO); を使用できます。

バージョン 2.2 以降、missingTopicsFatal と呼ばれる新しいコンテナープロパティが追加されました(デフォルト: 2.3.4 以降の false)。これにより、構成されたトピックのいずれかがブローカーに存在しない場合にコンテナーが開始されなくなります。コンテナーがトピックパターン(正規表現)をリッスンするように構成されている場合は適用されません。以前は、コンテナースレッドは consumer.poll() メソッド内でループし、多くのメッセージをログに記録している間、トピックが表示されるのを待っていました。ログを除いて、問題があったという兆候はありませんでした。

バージョン 2.8 の時点で、新しいコンテナープロパティ authExceptionRetryInterval が導入されました。これにより、コンテナーは KafkaConsumer から AuthenticationException または AuthorizationException を取得した後、メッセージのフェッチを再試行します。これは、たとえば、構成されたユーザーが特定のトピックを読み取るためのアクセスを拒否された場合、または資格情報が正しくない場合に発生する可能性があります。authExceptionRetryInterval を定義すると、適切な権限が付与されたときにコンテナーをリカバリできます。

デフォルトでは、間隔は構成されていません。認証および認可エラーは致命的と見なされ、コンテナーが停止します。

バージョン 2.8 以降、コンシューマーファクトリを作成するときに、デシリアライザーをオブジェクトとして(コンストラクター内または setter 経由で)提供すると、ファクトリは configure() メソッドを呼び出して、構成プロパティで構成します。

ConcurrentMessageListenerContainer を使用する

単一のコンストラクターは、KafkaListenerContainer コンストラクターに似ています。次のリストは、コンストラクターの署名を示しています。

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

また、concurrency プロパティもあります。例: container.setConcurrency(3) は 3 つの KafkaMessageListenerContainer インスタンスを作成します。

最初のコンストラクターの場合、Kafka は、グループ管理機能を使用して、コンシューマー全体にパーティションを分散します。

複数のトピックを聞いている場合、デフォルトのパーティション分散は期待したものではない可能性があります。例: それぞれ 5 つのパーティションを持つ 3 つのトピックがあり、concurrency=15 を使用する場合、アクティブなコンシューマーは 5 つだけで、それぞれが各トピックから 1 つのパーティションに割り当てられ、他の 10 のコンシューマーはアイドル状態です。これは、デフォルトの Kafka PartitionAssignor が RangeAssignor であるためです(Javadoc を参照)。このシナリオでは、代わりに RoundRobinAssignor の使用を検討することをお勧めします。これにより、すべてのコンシューマーにパーティションが分散されます。次に、各コンシューマーに 1 つのトピックまたはパーティションが割り当てられます。PartitionAssignor を変更するには、DefaultKafkaConsumerFactory に提供されているプロパティで partition.assignment.strategy コンシューマープロパティ(ConsumerConfigs.PARTITION_ASSIGNMENT_STRATEGY_CONFIG)を設定できます。

Spring Boot を使用する場合、次のように戦略を設定することができます。

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

コンテナーのプロパティが TopicPartitionOffset で構成されている場合、ConcurrentMessageListenerContainer は TopicPartitionOffset インスタンスをデリゲート KafkaMessageListenerContainer インスタンス全体に分散します。

たとえば、6 つの TopicPartitionOffset インスタンスが提供され、concurrency が 3 である場合。各コンテナーは 2 つのパーティションを取得します。5 つの TopicPartitionOffset インスタンスの場合、2 つのコンテナーが 2 つのパーティションを取得し、3 番目が 1 つのパーティションを取得します。concurrency が TopicPartitions の数より大きい場合、concurrency は、各コンテナーが 1 つのパーティションを取得するように調整されます。

client.id プロパティ(設定されている場合)には -n が追加されます。ここで、n は、同時実行性に対応するコンシューマーインスタンスです。これは、JMX が有効になっているときに MBean に一意の名前を指定するために必要です。

バージョン 1.3 以降、MessageListenerContainer は、基盤となる KafkaConsumer のメトリクスへのアクセスを提供します。ConcurrentMessageListenerContainer の場合、metrics() メソッドはすべてのターゲット KafkaMessageListenerContainer インスタンスのメトリクスを返します。メトリクスは、基礎となる KafkaConsumer に提供される client-id によって Map<MetricName, ? extends Metric> にグループ化されます。

バージョン 2.3 以降、ContainerProperties は idleBetweenPolls オプションを提供し、リスナーコンテナーのメインループを KafkaConsumer.poll() 呼び出し間でスリープさせます。実際のスリープ間隔は、提供されたオプションと max.poll.interval.ms コンシューマー構成と現在のレコードのバッチ処理時間の差から最小値として選択されます。

オフセットのコミット

オフセットをコミットするためのいくつかのオプションが提供されています。enable.auto.commit コンシューマープロパティが true の場合、Kafka はその構成に従ってオフセットを自動コミットします。false の場合、コンテナーはいくつかの AckMode 設定をサポートします(次のリストで説明)。デフォルトの AckMode は BATCH です。バージョン 2.3 以降、フレームワークは、構成で明示的に設定されていない限り、enable.auto.commit を false に設定します。以前は、プロパティが設定されていない場合、Kafka のデフォルト(true)が使用されていました。

コンシューマー poll() メソッドは、1 つ以上の ConsumerRecords を返します。MessageListener はレコードごとに呼び出されます。以下のリストは、各 AckMode に対してコンテナーによって実行されるアクションを説明しています(トランザクションが使用されていない場合)。

  • RECORD: レコードの処理後にリスナーが戻ったときにオフセットをコミットします。

  • BATCHpoll() によって返されたすべてのレコードが処理されたら、オフセットをコミットします。

  • TIME: 最後のコミット以降の ackTime を超えている限り、poll() によって返されたすべてのレコードが処理されたときにオフセットをコミットします。

  • COUNTpoll() によって返されたすべてのレコードが処理されたら、最後のコミット以降に ackCount レコードが受信されている限り、オフセットをコミットします。

  • COUNT_TIMETIME および COUNT に似ていますが、いずれかの条件が true の場合にコミットが実行されます。

  • MANUAL: メッセージリスナーは、acknowledge()Acknowledgment を担当します。その後、BATCH と同じセマンティクスが適用されます。

  • MANUAL_IMMEDIATEAcknowledgment.acknowledge() メソッドがリスナーによって呼び出されたら、すぐにオフセットをコミットします。

transactions を使用する場合、オフセットはトランザクションに送信され、セマンティクスはリスナーの型 (レコードまたはバッチ) に応じて RECORD または BATCH と同等です。

MANUAL、および MANUAL_IMMEDIATE では、リスナーが AcknowledgingMessageListener または BatchAcknowledgingMessageListener である必要があります。メッセージリスナーを参照してください。

syncCommits コンテナーのプロパティに応じて、コンシューマーの commitSync() または commitAsync() メソッドが使用されます。syncCommits はデフォルトで true です。setSyncCommitTimeout も参照してください。非同期コミットの結果を取得するには、setCommitCallback を参照してください。デフォルトのコールバックは、エラー(およびデバッグレベルでの成功)をログに記録する LoggingCommitCallback です。

リスナーコンテナーにはオフセットをコミットするための独自のメカニズムがあるため、Kafka ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG は false であることが優先されます。バージョン 2.3 以降、コンシューマーファクトリで特に設定されていない限り、またはコンテナーのコンシューマープロパティがオーバーライドされない限り、無条件に false に設定されます。

Acknowledgment には次の方法があります。

public interface Acknowledgment {

    void acknowledge();

}

このメソッドにより、リスナーはオフセットがコミットされるタイミングを制御できます。

バージョン 2.3 以降、Acknowledgment インターフェースには 2 つの追加メソッド nack(long sleep) および nack(int index, long sleep) があります。1 つ目はレコードリスナーで使用され、2 つ目はバッチリスナーで使用されます。リスナー型に対して間違ったメソッドを呼び出すと、IllegalStateException がスローされます。

nack() を使用して部分バッチをコミットする場合、トランザクションを使用する場合は、AckMode を MANUAL に設定します。nack() を呼び出すと、正常に処理されたレコードのオフセットがトランザクションに送信されます。
nack() は、リスナーを呼び出すコンシューマースレッドでのみ呼び出すことができます。
順不同のコミットを使用する場合、nack() は許可されません。

レコードリスナーを使用すると、nack() が呼び出されると、保留中のオフセットがコミットされ、最後のポーリングの残りのレコードが破棄され、パーティションでシークが実行され、失敗したレコードと未処理のレコードが次の poll() で再配信されます。sleep 引数を設定することにより、再配信の前にコンシューマーを一時停止できます。これは、コンテナーが DefaultErrorHandler で構成されている場合に例外をスローするのと同様の機能です。

バッチリスナーを使用する場合、障害が発生したバッチ内のインデックスを指定できます。nack() が呼び出されると、インデックスの前にレコードのオフセットがコミットされ、失敗したレコードと破棄されたレコードのパーティションでシークが実行され、次の poll() で再配信されます。

詳細については、コンテナーエラーハンドラーを参照してください。

スリープ中はコンシューマーが一時停止されるため、コンシューマーを存続させるためにブローカーのポーリングを続行します。実際のスリープ時間とその解決は、デフォルトで 5 秒に設定されているコンテナーの pollTimeout によって異なります。最小スリープ時間は pollTimeout と同じであり、すべてのスリープ時間はその倍数になります。スリープ時間が短い場合、または精度を上げるために、コンテナーの pollTimeout を減らすことを検討してください。
リスナーコンテナーの自動起動

リスナーコンテナーは SmartLifecycle を実装し、autoStartup はデフォルトで true です。コンテナーは後期フェーズ(Integer.MAX-VALUE - 100)で開始されます。リスナーからのデータを処理するために SmartLifecycle を実装する他のコンポーネントは、早い段階で開始する必要があります。- 100 は、後のフェーズの余地を残して、コンテナーの後でコンポーネントを自動起動できるようにします。

オフセットの手動コミット

通常、AckMode.MANUAL または AckMode.MANUAL_IMMEDIATE を使用する場合、Kafka は各レコードの状態を維持せず、各グループ / パーティションのコミットされたオフセットのみを維持するため、確認応答を順番に確認する必要があります。バージョン 2.8 以降、コンテナープロパティ asyncAcks を設定できるようになりました。これにより、ポーリングによって返されたレコードの確認応答を任意の順序で確認応答できます。リスナーコンテナーは、欠落している確認応答が受信されるまで、順不同のコミットを延期します。前のポーリングのすべてのオフセットがコミットされるまで、コンシューマーは一時停止されます(新しいレコードは配信されません)。

この機能により、アプリケーションはレコードを非同期で処理できますが、障害後に重複配信される可能性が高くなることを理解しておく必要があります。
@KafkaListener アノテーション

@KafkaListener アノテーションは、Bean メソッドをリスナーコンテナーのリスナーとして指定するために使用されます。Bean は、メソッドパラメーターに一致するように必要に応じてデータを変換するコンバーターなど、さまざまな機能で構成された MessagingMessageListenerAdapter にラップされています。

#{…​} またはプロパティプレースホルダー(${…​})を使用して、SpEL でアノテーションのほとんどの属性を構成できます。詳細については、Javadoc を参照してください。

レコードリスナー

@KafkaListener アノテーションは、単純な POJO リスナーにメカニズムを提供します。次の例は、その使用方法を示しています。

public class Listener {

    @KafkaListener(id = "foo", topics = "myTopic", clientIdPrefix = "myClientId")
    public void listen(String data) {
        ...
    }

}

このメカニズムでは、@Configuration クラスの 1 つに @EnableKafka アノテーションが必要であり、リスナーコンテナーファクトリが必要です。これは、基盤となる ConcurrentMessageListenerContainer を構成するために使用されます。デフォルトでは、kafkaListenerContainerFactory という名前の Bean が予期されています。次の例は、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 または partitionOffsets 属性で各パーティションを指定できますが、両方を指定することはできません。

ほとんどのアノテーションプロパティと同様に、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 属性のすべてのパーティションを表します。各 @TopicPartition には、ワイルドカードを使用した @PartitionOffset が 1 つだけ存在する必要があります。

さらに、リスナーが 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_KEY

  • KafkaHeaders.RECEIVED_TOPIC

  • KafkaHeaders.RECEIVED_PARTITION

  • KafkaHeaders.RECEIVED_TIMESTAMP

  • KafkaHeaders.TIMESTAMP_TYPE

バージョン 2.5 以降、受信レコードに null キーがある場合、RECEIVED_KEY は存在しません。以前は、ヘッダーに null 値が入力されていました。この変更は、null 値のヘッダーが存在しない spring-messaging 規則とフレームワークを一致させるためです。

次の例は、ヘッダーの使用方法を示しています。

@KafkaListener(id = "qux", topicPattern = "myTopic1")
public void listen(@Payload String foo,
        @Header(name = KafkaHeaders.RECEIVED_KEY, required = false) Integer key,
        @Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
        @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
        @Header(KafkaHeaders.RECEIVED_TIMESTAMP) long ts
        ) {
    ...
}
パラメーターアノテーション (@Payload@Header) は、リスナーメソッドの具体的な実装で指定する必要があります。インターフェースで定義されている場合は検出されません。

バージョン 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 以降では、@KafkaListener アノテーションの batch プロパティを使用して、ファクトリの batchListener プロパティをオーバーライドできます。これは、コンテナーエラーハンドラーへの変更とともに、レコードリスナーとバッチリスナーの両方に同じファクトリを使用できるようにします。
バージョン 2.9.6 以降、コンテナーファクトリには、recordMessageConverter および batchMessageConverter プロパティ用の個別の setter があります。以前は、レコードリスナーとバッチリスナーの両方に適用されるプロパティ messageConverter が 1 つしかありませんでした。

次の例は、ペイロードのリストを受信する方法を示しています。

@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_KEY) List<Integer> keys,
        @Header(KafkaHeaders.RECEIVED_PARTITION) List<Integer> partitions,
        @Header(KafkaHeaders.RECEIVED_TOPIC) List<String> topics,
        @Header(KafkaHeaders.OFFSET) List<Long> offsets) {
    ...
}

または、各メッセージに各オフセットとその他の詳細が含まれる Message<?> オブジェクトの List を受け取ることもできますが、メソッドで定義されている唯一のパラメーター(手動コミットを使用する場合のオプションの 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<?, ?> オブジェクトのリストを受け取ることもできますが、メソッドで定義されている唯一のパラメーター(手動コミットおよび Consumer<?, ?> パラメーターを使用する場合のオプションの Acknowledgment を除く)である必要があります。次の例は、その方法を示しています。

@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 以降、リスナーは poll() メソッドによって返される完全な ConsumerRecords<?, ?> オブジェクトを受信できるため、リスナーは partitions() (リスト内の TopicPartition インスタンスを返す)や records(TopicPartition) (選択レコードを取得する)などの追加のメソッドにアクセスできます。繰り返しますが、これはメソッドの唯一のパラメーター(手動コミットまたは Consumer<?, ?> パラメーターを使用する場合のオプションの Acknowledgment を除く)である必要があります。次の例は、その方法を示しています。

@KafkaListener(id = "pollResults", topics = "myTopic", containerFactory = "batchFactory")
public void pollResults(ConsumerRecords<?, ?> records) {
    ...
}
コンテナーファクトリに RecordFilterStrategy が構成されている場合、ConsumerRecords<?, ?> リスナーでは無視され、WARN ログメッセージが発行されます。<List<?>> 形式のリスナーが使用されている場合、レコードはバッチリスナーでのみフィルタリングできます。デフォルトでは、レコードは一度に 1 つずつフィルタリングされます。バージョン 2.8 以降では、filterBatch をオーバーライドして、1 回の呼び出しでバッチ全体をフィルタリングできます。
アノテーションプロパティ

バージョン 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;
    }

}

万が一、__listener という実際の Bean がある場合は、beanRef 属性を使用して式トークンを変更できます。次の例は、その方法を示しています。

@KafkaListener(beanRef = "__x", topics = "#{__x.topic}",
    groupId = "#{__x.topic}.group")

バージョン 2.2.4 以降、Kafka コンシューマープロパティをアノテーションに直接指定できます。これらは、コンシューマーファクトリで構成された同じ名前のプロパティをオーバーライドします。この方法で group.id および client.id プロパティを指定することはできません。それらは無視されます。それらには groupId および clientIdPrefix アノテーションプロパティを使用します。

プロパティは、通常の Java Properties ファイル形式(foo:barfoo=barfoo bar)の個別の文字列として指定されます。

@KafkaListener(topics = "myTopic", groupId = "group", properties = {
    "max.poll.interval.ms:60000",
    ConsumerConfig.MAX_POLL_RECORDS_CONFIG + "=100"
})

以下は、RoutingKafkaTemplate を使用するの例に対応するリスナーの例です。

@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 メカニズムを使用してください。
コンテナースレッドの命名

TaskExecutor は、コンシューマーとリスナーを呼び出すために使用されます。コンテナーの ContainerProperties の consumerExecutor プロパティを設定することで、カスタムエグゼキュータを提供できます。プールされたエグゼキューターを使用する場合は、それらが使用されているすべてのコンテナーで同時実行を処理するのに十分なスレッドが利用可能であることを確認してください。ConcurrentMessageListenerContainer を使用する場合、executor からのスレッドが各コンシューマー (concurrency) に使用されます。

コンシューマーエグゼキュータを提供しない場合、各コンテナーに SimpleAsyncTaskExecutor が使用されます。このエグゼキュータは、<beanName>-C-<n> に似た名前のスレッドを作成します。ConcurrentMessageListenerContainer の場合、スレッド名の <beanName> 部分は <beanName>-m になり、m はコンシューマーインスタンスを表します。n は、コンテナーが開始されるたびに増加します。Bean 名が container の場合、このコンテナー内のスレッドは、コンテナーが最初に開始された後に container-0-C-1container-1-C-1 などの名前になります。container-0-C-2container-1-C-2 など、停止およびその後の起動後。

バージョン 3.0.1 から、使用されているエグゼキューターに関係なく、スレッドの名前を変更できるようになりました。AbstractMessageListenerContainer.changeConsumerThreadName プロパティを true に設定すると、AbstractMessageListenerContainer.threadNameSupplier が呼び出されてスレッド名が取得されます。これは Function<MessageListenerContainer, String> であり、デフォルトの実装では container.getListenerId() が返されます。

メタアノテーションとしての @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";

}

topicstopicPatterntopicPartitions の少なくとも 1 つをエイリアス化する必要があります(通常、コンシューマーファクトリ構成で group.id を指定していない限り、id または groupId)。次の例は、その方法を示しています。

@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 メソッドを指定できます。せいぜい 1 つのメソッドをそのように指定することができます。@KafkaHandler メソッドを使用する場合、ペイロードはすでにドメインオブジェクトに変換されている必要があります(一致を実行できるようにするため)。カスタムデシリアライザー、JsonDeserializerTypePrecedence を TYPE_ID に設定した JsonMessageConverter を使用します。詳細については、直列化、逆直列化、メッセージ変換を参照してください。

Spring がメソッド引数を解決する方法にいくつかの制限があるため、デフォルトの @KafkaHandler は個別のヘッダーを受信できません。コンシューマーレコードのメタデータに従って、ConsumerRecordMetadata を使用する必要があります。

例:

@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 以降、コンテナーが作成される前に、プログラムでアノテーション属性を変更できるようになりました。これを行うには、1 つ以上の 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 ではありません。代わりに、型 KafkaListenerEndpointRegistry のインフラストラクチャ Bean に登録されます。この 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 以降、Validator を追加して @KafkaListener @Payload 引数を検証するのが簡単になりました。以前は、カスタム DefaultMessageHandlerMethodFactory を構成して、レジストラに追加する必要がありました。これで、バリデーターをレジストラ自体に追加できます。次のコードは、その方法を示しています。

@Configuration
@EnableKafka
public class Config implements KafkaListenerConfigurer {

    ...

    @Override
    public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
      registrar.setValidator(new MyValidator());
    }

}
検証スターターで Spring Boot を使用すると、次の例に示すように、LocalValidatorFactoryBean が自動構成されます。
@Configuration
@EnableKafka
public class Config implements KafkaListenerConfigurer {

    @Autowired
    private LocalValidatorFactoryBean validator;
    ...

    @Override
    public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
      registrar.setValidator(this.validator);
    }
}

次の例は、検証する方法を示しています。

public static class ValidatedClass {

  @Max(10)
  private int bar;

  public int getBar() {
    return this.bar;
  }

  public void setBar(int bar) {
    this.bar = bar;
  }

}
@KafkaListener(id="validated", topics = "annotated35", errorHandler = "validationErrorHandler",
      containerFactory = "kafkaJsonListenerContainerFactory")
public void validatedListener(@Payload @Valid ValidatedClass val) {
    ...
}

@Bean
public KafkaListenerErrorHandler validationErrorHandler() {
    return (m, e) -> {
        ...
    };
}

バージョン 2.5.11 以降、検証はクラスレベルのリスナーの @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);

}

パーティションが取り消されると、2 つのコールバックがあることに注意してください。最初のものはすぐに呼び出されます。2 つ目は、保留中のオフセットがコミットされた後に呼び出されます。これは、次の例に示すように、外部リポジトリでオフセットを維持する場合に役立ちます。

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 を呼び出します。ConsumerAwareRebalanceListener のデフォルトの実装は何もしません。リスナーコンテナーに(いずれかの型の)カスタムリスナーを提供する場合、実装が onPartitionsLost から onPartitionsRevoked を呼び出さないことが重要です。ConsumerRebalanceListener を実装する場合は、デフォルトのメソッドをオーバーライドする必要があります。これは、リスナーコンテナーが、実装でメソッドを呼び出した後、onPartitionsLost の実装から独自の onPartitionsRevoked を呼び出すためです。実装がデフォルトの動作に委譲する場合、Consumer がコンテナーのリスナーでそのメソッドを呼び出すたびに onPartitionsRevoked が 2 回呼び出されます。
@SendTo を使用したリスナー結果の転送

バージョン 2.0 以降、@KafkaListener にも @SendTo アノテーションを付け、メソッド呼び出しが結果を返す場合、結果は @SendTo で指定されたトピックに転送されます。

@SendTo 値には、いくつかの形式があります。

  • @SendTo("someTopic") は文字通りのトピックにルーティングします

  • @SendTo("#{someExpression}") は、アプリケーションコンテキストの初期化中に式を 1 回評価することによって決定されたトピックにルーティングします。

  • @SendTo("!{someExpression}") は、実行時に式を評価することによって決定されたトピックにルーティングします。評価用の #root オブジェクトには、次の 3 つのプロパティがあります。

    • request: 受信 ConsumerRecord (またはバッチリスナーの場合は ConsumerRecords オブジェクト))

    • sourcerequest から変換された org.springframework.messaging.Message<?>

    • result: メソッドは結果を返します。

  • @SendTo (プロパティなし): これは !{source.headers['kafka_replyTopic']} として扱われます(バージョン 2.1.3 以降)。

バージョン 2.1.11 および 2.2.1 以降、プロパティプレースホルダーは @SendTo 値内で解決されます。

式の評価の結果は、トピック名を表す String である必要があります。次の例は、@SendTo を使用するさまざまな方法を示しています。

@KafkaListener(topics = "annotated21")
@SendTo("!{request.value()}") // runtime SpEL
public String replyingListener(String in) {
    ...
}

@KafkaListener(topics = "${some.property:annotated22}")
@SendTo("#{myBean.replyTopic}") // config time SpEL
public Collection<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_KEY) int key) {
        ...
    }

}
@SendTo をサポートするには、リスナーコンテナーファクトリに KafkaTemplate (replyTemplate プロパティ内)を提供する必要があります。これは、応答の送信に使用されます。これは KafkaTemplate である必要があり、リクエスト / 応答処理のためにクライアント側で使用される ReplyingKafkaTemplate ではありません。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 を使用する場合、送信を実行するには、replyTemplate プロパティで KafkaTemplate を使用して ConcurrentKafkaListenerContainerFactory を構成する必要があります。Spring Boot は、自動構成されたテンプレート (または単一のインスタンスが存在する場合はいずれか) に自動的に接続します。

リクエスト / 応答のセマンティクスを使用しない限り、単純な send(topic, value) メソッドのみが使用されるため、パーティションまたはキーを生成するためのサブクラスを作成することをお勧めします。次の例は、その方法を示しています。
@Bean
public KafkaTemplate<String, String> myReplyingTemplate() {
    return new KafkaTemplate<Integer, String>(producerFactory()) {

        @Override
        public CompletableFuture<SendResult<String, String>> send(String topic, String data) {
            return super.send(topic, partitionForData(data), keyForData(data), data);
        }

        ...

    };
}

リスナーメソッドが Message<?> または Collection<Message<?>> を返す場合、リスナーメソッドは応答のメッセージヘッダーを設定する責任があります。例: 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.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 以降、@KafkaListener の splitIterables プロパティを false に設定すると、結果全体が単一の ProducerRecord の値として送信されます。これには、応答テンプレートのプロデューサー構成に適切なシリアライザーが必要です。ただし、応答が Iterable<Message<?>> の場合、プロパティは無視され、各メッセージは個別に送信されます。
メッセージのフィルタリング

リバランスなどの特定のシナリオでは、すでに処理されたメッセージが再配信される場合があります。フレームワークは、そのようなメッセージが処理されたかどうかを知ることはできません。これはアプリケーションレベルの関数です。これはべき等レシーバー (英語) パターンとして知られており、Spring Integration はその実装を提供します。

Spring for Apache Kafka プロジェクトは、MessageListener をラップできる FilteringMessageListenerAdapter クラスを使用して支援も提供します。このクラスは RecordFilterStrategy の実装を取り、filter メソッドを実装して、メッセージが重複しているため破棄する必要があることを通知します。これには、アダプターが破棄されたレコードを確認する必要があるかどうかを示す ackDiscarded と呼ばれる追加のプロパティがあります。デフォルトは false です。

@KafkaListener を使用する場合は、リスナーが適切なフィルタリングアダプターにラップされるように、コンテナーファクトリで RecordFilterStrategy (およびオプションで ackDiscarded)を設定します。

さらに、バッチメッセージリスナーを使用する場合に備えて、FilteringBatchMessageListenerAdapter が提供されます。

ConsumerRecords は不変であるため、@KafkaListener が List<ConsumerRecord<?, ?>> ではなく ConsumerRecords<?, ?> を受信した場合、FilteringBatchMessageListenerAdapter は無視されます。

バージョン 2.8.4 以降では、リスナーアノテーションの filter プロパティを使用して、リスナーコンテナーファクトリのデフォルトの RecordFilterStrategy をオーバーライドできます。

@KafkaListener(id = "filtered", topics = "topic", filter = "differentFilter")
public void listen(Thing thing) {
    ...
}
配信の再試行

例外の処理の DefaultErrorHandler を参照してください。

@KafkaListener を順番に開始する

一般的な使用例は、別のリスナーがトピック内のすべてのレコードを消費した後にリスナーを開始することです。例: 他のトピックのレコードを処理する前に、1 つ以上の圧縮されたトピックのコンテンツをメモリにロードしたい場合があります。バージョン 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");
}

ここでは、g1 と g2 の 2 つのグループに 4 つのリスナーがあります。

アプリケーションコンテキストの初期化中に、シーケンサーは、提供されたグループ内のすべてのコンテナーの autoStartup プロパティを false に設定します。また、コンテナー(まだセットがない)の idleEventInterval を指定された値(この場合は 5000ms)に設定します。次に、シーケンサーがアプリケーションコンテキストによって開始されると、最初のグループのコンテナーが開始されます。ListenerContainerIdleEvent が受信されると、各コンテナー内の個々の子コンテナーが停止します。ConcurrentMessageListenerContainer 内のすべての子コンテナーが停止すると、親コンテナーも停止します。グループ内のすべてのコンテナーが停止すると、次のグループ内のコンテナーが開始されます。グループまたはグループ内のコンテナーの数に制限はありません。

デフォルトでは、最終グループ(上記の g2)のコンテナーは、アイドル状態になっても停止しません。この動作を変更するには、シーケンサーで stopLastGroupWhenIdle を true に設定します。

余談として ; 以前は、各グループのコンテナーが型 Collection<MessageListenerContainer> の Bean に追加され、Bean 名は containerGroup でした。これらのコレクションは非推奨になり、.group がサフィックスとして付けられたグループ名である Bean 名を持つ型 ContainerGroup の Bean が優先されます。上記の例では、2 つの Bean g1.group と g2.group があります。Collection Bean は、将来のリリースで削除される予定です。

KafkaTemplate を使用した受信

このセクションでは、KafkaTemplate を使用してメッセージを受信する方法について説明します。

バージョン 2.8 以降、テンプレートには 4 つの 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 が作成(およびクローズ)されます。

最後の 2 つの方法では、各レコードが個別に取得され、結果が ConsumerRecords オブジェクトにアセンブルされます。リクエストの TopicPartitionOffset を作成する場合、正の絶対オフセットのみがサポートされます。

4.1.5. リスナーコンテナーのプロパティ

表 1: ContainerProperties プロパティ
プロパティ デフォルト 説明

ackCount

1

ackMode が COUNT または COUNT_TIME の場合に、保留中のオフセットをコミットする前のレコード数。

adviceChain

null

メッセージリスナーをラップする Advice オブジェクトのチェーン(アドバイスの周囲の MethodInterceptor など)。順番に呼び出されます。

ackMode

BATCH

オフセットがコミットされる頻度を制御します。オフセットのコミットを参照してください。

ackTime

5000

ackMode が TIME または COUNT_TIME の場合に、保留中のオフセットがコミットされるまでのミリ秒単位の時間。

assignmentCommitOption

LATEST_ONLY _NO_TX

割り当ての初期位置をコミットするかどうか。デフォルトでは、初期オフセットは ConsumerConfig.AUTO_OFFSET_RESET_CONFIG が latest の場合にのみコミットされ、トランザクションマネージャーが存在する場合でもトランザクションで実行されません。使用可能なオプションの詳細については、ContainerProperties.AssignmentCommitOption の javadoc を参照してください。

authExceptionRetryInterval

null

null でない場合、AuthenticationException または AuthorizationException が Kafka クライアントによってスローされたときに、ポーリング間でスリープする Duration。null の場合、そのような例外は致命的と見なされ、コンテナーは停止します。

clientId

(空の文字列)

client.id コンシューマープロパティのプレフィックス。コンシューマーファクトリの client.id プロパティをオーバーライドします。並行コンテナーでは、-n が各コンシューマーインスタンスのサフィックスとして追加されます。

checkDeserExWhenKeyNull

false

null key を受信したときに、常に DeserializationException ヘッダーをチェックするには、true に設定します。委譲デシリアライザーを使用している場合など、コンシューマーコードが ErrorHandlingDeserializer が構成されていることを判別できない場合に役立ちます。

checkDeserExWhenValueNull

false

null value を受信したときに、常に DeserializationException ヘッダーをチェックするには、true に設定します。委譲デシリアライザーを使用している場合など、コンシューマーコードが ErrorHandlingDeserializer が構成されていることを判別できない場合に役立ちます。

commitCallback

null

存在し、syncCommits が false の場合、コミットの完了後にコールバックが呼び出されます。

offsetAndMetadataProvider

null

OffsetAndMetadata のプロバイダー ; デフォルトでは、プロバイダーは空のメタデータを使用してオフセットとメタデータを作成します。プロバイダーは、メタデータをカスタマイズする方法を提供します。

commitLogLevel

DEBUG

オフセットのコミットに関連するログのログレベル。

consumerRebalanceListener

null

リバランスリスナー。リスナーのリバランスを参照してください。

consumerStartTimout

30 代

エラーをログに記録する前に、コンシューマーが開始するのを待つ時間。これは、たとえば、スレッドが不十分なタスクエグゼキュータを使用している場合に発生する可能性があります。

consumerTaskExecutor

SimpleAsyncTaskExecutor

コンシューマースレッドを実行するタスクエグゼキュータ。デフォルトのエグゼキュータは、<name>-C-n という名前のスレッドを作成します。KafkaMessageListenerContainer の場合、名前は Bean 名です。ConcurrentMessageListenerContainer の場合、名前は -n が付加された Bean 名であり、n は子コンテナーごとに増分されます。

deliveryAttemptHeader

false

配信試行ヘッダーを参照してください。

eosMode

V2

正確に一度のセマンティクスモード。正確に一度セマンティクスを参照してください。

fixTxOffsets

false

トランザクションプロデューサーによって作成されたレコードを消費するときに、コンシューマーがパーティションの最後に位置する場合、トランザクションのコミット / ロールバックを示すために使用される疑似レコードと、おそらくロールバックされたレコードの存在により、ラグが誤ってゼロより大きく報告されることがあります。これは関数にはコンシューマーに影響を与えませんが、一部のユーザーは「ラグ」がゼロでないことに関心事を示しています。このプロパティを true に設定すると、コンテナーはこのようなオフセットの誤報を修正します。このチェックは、コミット処理が著しく複雑にならないように、次のポーリングの前に実行されます。執筆時点では、コンシューマーが isolation.level=read_committed で構成されており、max.poll.records が 1 より大きい場合にのみラグが修正されます。詳細は KAFKA-10683 [Apache] (英語) を参照してください。

groupId

null

コンシューマーの group.id プロパティをオーバーライドします。@KafkaListener id または groupId プロパティによって自動的に設定されます。

idleBeforeDataMultiplier

5.0

レコードを受信する前に適用される idleEventInterval の乗数。レコードを受信すると、乗数は適用されなくなります。バージョン 2.8 以降で使用できます。

idleBetweenPolls

0

ポーリングの間にスレッドをスリープさせることにより、配信を遅くするために使用されます。レコードのバッチとこの値を処理する時間は、max.poll.interval.ms コンシューマープロパティよりも短くする必要があります。

idleEventInterval

null

設定すると、ListenerContainerIdleEvent の公開が有効になります。アプリケーションイベントおよびアイドル状態のコンシューマーと無反応なコンシューマーの検出を参照してください。idleBeforeDataMultiplier も参照してください。

idlePartitionEventInterval

null

設定すると、ListenerContainerIdlePartitionEvent の公開が有効になります。アプリケーションイベントおよびアイドル状態のコンシューマーと無反応なコンシューマーの検出を参照してください。

kafkaConsumerProperties

なし

コンシューマーファクトリで構成された任意のコンシューマープロパティをオーバーライドするために使用されます。

logContainerConfig

false

true に設定すると、すべてのコンテナープロパティが INFO レベルでログに記録されます。

messageListener

null

メッセージリスナー。

micrometerEnabled

true

コンシューマースレッドの Micrometer タイマーを維持するかどうか。

micrometerTags

micrometer メトリクスに追加される静的タグのマップ。

micrometerTagsProvider

null

コンシューマーの記録に基づいて動的タグを提供する機能。

missingTopicsFatal

false

true の場合、構成されたトピックがブローカーに存在しない場合にコンテナーが開始されないようにします。

monitorInterval

30 代

NonResponsiveConsumerEvent のコンシューマースレッドの状態をチェックする頻度。noPollThreshold および pollTimeout を参照してください。

noPollThreshold

3.0

pollTimeOut を掛けて、NonResponsiveConsumerEvent を公開するかどうかを決定します。monitorInterval を参照してください。

onlyLogRecordMetadata

false

topic-partition@offset だけでなく、完全なコンシューマーレコード(エラー、デバッグログなど)をログに記録するには、false に設定します。

pauseImmediate

false

コンテナーが一時停止すると、前のポーリングからのすべてのレコードを処理した後ではなく、現在のレコードの後に処理を停止します。残りのレコードはメモリに保持され、コンテナーが再開されたときにリスナーに渡されます。

pollTimeout

5000

ミリ秒単位で Consumer.poll() に渡されたタイムアウト。

pollTimeoutWhilePaused

100

コンテナーが一時停止状態のときに Consumer.poll() に渡されるタイムアウト (ミリ秒単位)。

restartAfterAuthExceptions

false

認可 / 認証の例外が原因でコンテナーが停止した場合にコンテナーを再起動するには、True。

scheduler

ThreadPoolTaskScheduler

コンシューマーモニタータスクを実行するスケジューラー。

shutdownTimeout

10000

すべてのコンシューマーが停止し、コンテナー停止イベントを公開するまでに stop() メソッドをブロックする最大時間(ミリ秒単位)。

stopContainerWhenFenced

false

ProducerFencedException がスローされた場合は、リスナーコンテナーを停止します。詳細については、ロールバック後のプロセッサーを参照してください。

stopImmediate

false

コンテナーが停止したら、前のポーリングのすべてのレコードを処理した後ではなく、現在のレコードの後で処理を停止します。

subBatchPerPartition

説明を参照してください。

バッチリスナーを使用する場合、これが true の場合、リスナーはポーリングの結果を使用して呼び出され、パーティションごとに 1 つのサブバッチに分割されます。デフォルト false

syncCommitTimeout

null

syncCommits が true の場合に使用するタイムアウト。設定されていない場合、コンテナーは default.api.timeout.ms コンシューマープロパティを決定し、それを使用しようとします。それ以外の場合は 60 秒を使用します。

syncCommits

true

オフセットに同期コミットと非同期コミットのどちらを使用するか。commitCallback を参照してください。

topics topicPattern topicPartitions

なし

構成されたトピック、トピックパターン、明示的に割り当てられたトピック / パーティション。相互に排他的。少なくとも 1 つ提供する必要があります。ContainerProperties コンストラクターによって実施されます。

transactionManager

null

トランザクションを参照してください。

表 2: AbstractListenerContainer プロパティ
プロパティ デフォルト 説明

afterRollbackProcessor

DefaultAfterRollbackProcessor

トランザクションがロールバックされた後に呼び出す AfterRollbackProcessor

applicationEventPublisher

アプリケーションコンテキスト

イベント発行者。

batchErrorHandler

説明を参照してください。

非推奨 - commonErrorHandler を参照してください。

batchInterceptor

null

バッチリスナーを呼び出す前に呼び出すように BatchInterceptor を設定します。レコードリスナーには適用されません。interceptBeforeTx も参照してください。

beanName

Bean 名

コンテナーの Bean 名。子コンテナーの接尾辞は -n です。

commonErrorHandler

説明を参照してください。

DefaultErrorHandler or null when a transactionManager is provided when a DefaultAfterRollbackProcessor is used. See Container Error Handlers.

containerProperties

ContainerProperties

コンテナープロパティインスタンス。

errorHandler

説明を参照してください。

非推奨 - commonErrorHandler を参照してください。

genericErrorHandler

説明を参照してください。

非推奨 - commonErrorHandler を参照してください。

groupId

説明を参照してください。

containerProperties.groupId が存在する場合は、それ以外の場合は、コンシューマーファクトリからの group.id プロパティ。

interceptBeforeTx

true

トランザクションの開始前または開始後に recordInterceptor を呼び出すかどうかを決定します。

listenerId

説明を参照してください。

ユーザー構成コンテナーの Bean 名、または @KafkaListener の id 属性。

listenerInfo

null

KafkaHeaders.LISTENER_INFO ヘッダーに入力する値。@KafkaListener の場合、この値は info 属性から取得されます。このヘッダーは、RecordInterceptorRecordFilterStrategy などのさまざまな場所や、リスナーコード自体で使用できます。

pauseRequested

(読み取り専用)

コンシューマーの一時停止がリクエストされた場合は True。

recordInterceptor

null

レコードリスナーを呼び出す前に呼び出すように RecordInterceptor を設定します。バッチリスナーには適用されません。interceptBeforeTx も参照してください。

topicCheckTimeout

30 代

missingTopicsFatal コンテナーのプロパティが true の場合、describeTopics 操作が完了するまでの待機時間(秒単位)。

表 3: KafkaMessageListenerContainer プロパティ
プロパティ デフォルト 説明

assignedPartitions

(読み取り専用)

このコンテナーに現在割り当てられているパーティション(明示的かどうかに関係なく)。

assignedPartitionsByClientId

(読み取り専用)

このコンテナーに現在割り当てられているパーティション(明示的かどうかに関係なく)。

clientIdSuffix

null

並行コンテナーによって使用され、各子コンテナーのコンシューマーに一意の client.id を提供します。

containerPaused

なし

一時停止がリクエストされ、コンシューマーが実際に一時停止した場合は True。

表 4: ConcurrentMessageListenerContainer プロパティ
プロパティ デフォルト 説明

alwaysClientIdSuffix

true

false に設定すると、concurrency が 1 しかない場合に、client.id コンシューマープロパティへのサフィックスの追加が抑制されます。

assignedPartitions

(読み取り専用)

このコンテナーの子 KafkaMessageListenerContainer に現在割り当てられているパーティションの集合体(明示的かどうかに関係なく)。

assignedPartitionsByClientId

(読み取り専用)

このコンテナーの子 KafkaMessageListenerContainer に現在割り当てられているパーティション(明示的かどうかに関係なく)。子コンテナーのコンシューマーの client.id プロパティによってキー設定されます。

concurrency

1

管理する子 KafkaMessageListenerContainer の数。

containerPaused

なし

一時停止がリクエストされ、すべての子コンテナーのコンシューマーが実際に一時停止した場合は True。

containers

なし

すべての子 KafkaMessageListenerContainer への参照。

4.1.6. コンテナーの動的作成

実行時にリスナーコンテナーを作成するために使用できる手法がいくつかあります。このセクションでは、これらのテクニックのいくつかを探ります。

MessageListener の実装

独自のリスナーを直接実装する場合は、コンテナーファクトリを使用して、そのリスナーの生のコンテナーを作成するだけです。

例 8: ユーザーリスナー
Java
public class MyListener implements MessageListener<String, String> {

    @Override
    public void onMessage(ConsumerRecord<String, String> data) {
        // ...
    }

}

private ConcurrentMessageListenerContainer<String, String> createContainer(
        ConcurrentKafkaListenerContainerFactory<String, String> factory, String topic, String group) {

    ConcurrentMessageListenerContainer<String, String> container = factory.createContainer(topic);
    container.getContainerProperties().setMessageListener(new MyListener());
    container.getContainerProperties().setGroupId(group);
    container.setBeanName(group);
    container.start();
    return container;
}
Kotlin
class MyListener : MessageListener<String?, String?> {

    override fun onMessage(data: ConsumerRecord<String?, String?>) {
        // ...
    }

}

private fun createContainer(
    factory: ConcurrentKafkaListenerContainerFactory<String, String>, topic: String, group: String
): ConcurrentMessageListenerContainer<String, String> {
    val container = factory.createContainer(topic)
    container.containerProperties.messageListener = MyListener()
    container.containerProperties.groupId = group
    container.beanName = group
    container.start()
    return container
}
試作 Bean

@KafkaListener でアノテーションが付けられたメソッドのコンテナーは、Bean をプロトタイプとして宣言することで動的に作成できます。

例 9: プロトタイプ
Java
public class MyPojo {

    private final String id;

    private final String topic;

    public MyPojo(String id, String topic) {
        this.id = id;
        this.topic = topic;
    }

    public String getId() {
        return this.id;
    }

    public String getTopic() {
        return this.topic;
    }

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

}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
MyPojo pojo(String id, String topic) {
	return new MyPojo(id, topic);
}

applicationContext.getBean(MyPojo.class, "one", "topic2");
applicationContext.getBean(MyPojo.class, "two", "topic3");
Kotlin
class MyPojo(id: String?, topic: String?) {

    @KafkaListener(id = "#{__listener.id}", topics = ["#{__listener.topics}"])
    fun listen(`in`: String?) {
        println(`in`)
    }

}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
fun pojo(id: String?, topic: String?): MyPojo {
    return MyPojo(id, topic)
}

applicationContext.getBean(MyPojo::class.java, "one", arrayOf("topic2"))
applicationContext.getBean(MyPojo::class.java, "two", arrayOf("topic3"))
リスナーには一意の ID が必要です。バージョン 2.8.9 以降、KafkaListenerEndpointRegistry には新しいメソッド unregisterListenerContainer(String id) があり、ID を再利用できます。コンテナーの登録を解除しても、コンテナーは stop() されません。自分で行う必要があります。

4.1.7. アプリケーションイベント

次の Spring アプリケーションイベントは、リスナーコンテナーとそのコンシューマーによって公開されます。

  • ConsumerStartingEvent - コンシューマースレッドが最初に開始されたとき、ポーリングを開始する前に公開されます。

  • ConsumerStartedEvent - コンシューマーがポーリングを開始しようとしているときに公開されます。

  • ConsumerFailedToStartEvent - consumerStartTimeout コンテナープロパティ内に ConsumerStartingEvent が公開されていない場合に公開されます。このイベントは、構成されたタスクエグゼキューターに、使用されているコンテナーとその並行性をサポートするのに十分なスレッドがないことを示している可能性があります。この状態が発生すると、エラーメッセージもログに記録されます。

  • ListenerContainerIdleEventidleInterval でメッセージが受信されていないときに公開されます(構成されている場合)。

  • ListenerContainerNoLongerIdleEvent: 以前に ListenerContainerIdleEvent を公開した後にレコードが消費されたときに公開されます。

  • ListenerContainerPartitionIdleEventidlePartitionEventInterval のそのパーティションからメッセージが受信されていないときに公開されます(構成されている場合)。

  • ListenerContainerPartitionNoLongerIdleEvent: 以前に ListenerContainerPartitionIdleEvent を公開したパーティションからレコードが消費されたときに公開されます。

  • NonResponsiveConsumerEvent: コンシューマーが poll メソッドでブロックされているように見えるときに公開されます。

  • ConsumerPartitionPausedEvent: パーティションが一時停止されたときに各コンシューマーによって公開されます。

  • ConsumerPartitionResumedEvent: パーティションが再開されたときに各コンシューマーによって公開されます。

  • ConsumerPausedEvent: コンテナーが一時停止されたときに各コンシューマーによって公開されます。

  • ConsumerResumedEvent: コンテナーが再開されたときに各コンシューマーによって公開されます。

  • ConsumerStoppingEvent: 停止する直前に各コンシューマーによって公開されました。

  • ConsumerStoppedEvent: コンシューマーが閉鎖された後に公開されます。スレッドセーフを参照してください。

  • ConsumerRetryAuthEvent: コンシューマーの認証または認可が失敗し、再試行されているときに発行されます。

  • ConsumerRetryAuthSuccessfulEvent: 認証または認可が正常に再試行されたときに発行されます。以前に ConsumerRetryAuthEvent があった場合にのみ発生する可能性があります。

  • ContainerStoppedEvent: すべてのコンシューマーが停止したときに公開されます。

デフォルトでは、アプリケーションコンテキストのイベントマルチキャストは、呼び出し元のスレッドでイベントリスナーを呼び出します。非同期エグゼキュータを使用するようにマルチキャストを変更する場合、イベントにコンシューマーへの参照が含まれているときに Consumer メソッドを呼び出さないでください。

ListenerContainerIdleEvent には次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

  • id: リスナー ID(またはコンテナー Bean 名)。

  • idleTime: イベントが公開されたときにコンテナーがアイドル状態だった時間。

  • topicPartitions: イベントの生成時にコンテナーに割り当てられたトピックとパーティション。

  • consumer: Kafka Consumer オブジェクトへの参照。例: コンシューマーの pause() メソッドが以前に呼び出された場合、イベントの受信時に resume() を実行できます。

  • paused: コンテナーが現在一時停止されているかどうか。詳細については、リスナーコンテナーの一時停止と再開を参照してください。

ListenerContainerNoLongerIdleEvent は、idleTime と paused を除いて、同じプロパティを持っています。

ListenerContainerPartitionIdleEvent には次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

  • id: リスナー ID(またはコンテナー Bean 名)。

  • idleTime: イベントが公開されたとき、時間パーティションの消費はアイドル状態でした。

  • topicPartition: イベントをトリガーしたトピックとパーティション。

  • consumer: Kafka Consumer オブジェクトへの参照。例: コンシューマーの pause() メソッドが以前に呼び出された場合、イベントの受信時に resume() を実行できます。

  • paused: そのパーティションの消費がそのコンシューマーに対して現在一時停止されているかどうか。詳細については、リスナーコンテナーの一時停止と再開を参照してください。

ListenerContainerPartitionNoLongerIdleEvent は、idleTime と paused を除いて、同じプロパティを持っています。

NonResponsiveConsumerEvent には次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

  • id: リスナー ID(またはコンテナー Bean 名)。

  • timeSinceLastPoll: コンテナーが最後に poll() と呼ばれる直前の時間。

  • topicPartitions: イベントの生成時にコンテナーに割り当てられたトピックとパーティション。

  • consumer: Kafka Consumer オブジェクトへの参照。例: コンシューマーの pause() メソッドが以前に呼び出された場合、イベントの受信時に resume() を実行できます。

  • paused: コンテナーが現在一時停止されているかどうか。詳細については、リスナーコンテナーの一時停止と再開を参照してください。

ConsumerPausedEventConsumerResumedEventConsumerStopping イベントには、次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

  • partitions: 関係する TopicPartition インスタンス。

ConsumerPartitionPausedEventConsumerPartitionResumedEvent イベントには、次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

  • partition: 関係する TopicPartition インスタンス。

ConsumerRetryAuthEvent イベントには次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

  • reason

    • AUTHENTICATION - 認証例外のため、イベントが発行されました。

    • AUTHORIZATION - 認可例外のため、イベントが発行されました。

ConsumerStartingEventConsumerStartingEventConsumerFailedToStartEventConsumerStoppedEventConsumerRetryAuthSuccessfulEventContainerStoppedEvent イベントには、次のプロパティがあります。

  • source: イベントを公開したリスナーコンテナーインスタンス。

  • container: ソースコンテナーが子の場合は、リスナーコンテナーまたは親リスナーコンテナー。

すべてのコンテナー(子または親)は ContainerStoppedEvent を公開します。親コンテナーの場合、ソースプロパティとコンテナープロパティは同じです。

さらに、ConsumerStoppedEvent には次の追加プロパティがあります。

  • reason

    • NORMAL - コンシューマーは正常に停止しました(コンテナーは停止しました)。

    • ERROR - java.lang.Error がスローされました。

    • FENCED - トランザクションプロデューサーはフェンスで囲まれ、stopContainerWhenFenced コンテナープロパティは true です。

    • AUTH - AuthenticationException または AuthorizationException がスローされ、authExceptionRetryInterval が構成されていません。

    • NO_OFFSET - パーティションのオフセットはなく、auto.offset.reset ポリシーは none です。

このイベントを使用して、次のような状態の後にコンテナーを再起動できます。

if (event.getReason.equals(Reason.FENCED)) {
    event.getSource(MessageListenerContainer.class).start();
}
アイドル状態のコンシューマーと無反応なコンシューマーの検出

効率的ではありますが、非同期コンシューマーの問題の 1 つは、アイドル状態を検出することです。一定期間メッセージが到着しない場合は、何らかのアクションを実行することをお勧めします。

メッセージが配信されずに時間が経過したときに 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;
}

次の例は、@KafkaListener の idleEventInterval を設定する方法を示しています。

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

いずれの場合も、コンテナーがアイドル状態のときにイベントが 1 分に 1 回公開されます。

何らかの理由で、コンシューマー poll() メソッドが終了しない場合、メッセージは受信されず、アイドルイベントを生成できません(これは、ブローカーに到達できなかった初期バージョンの kafka-clients の問題でした)。この場合、ポーリングが 3x 内で pollTimeout プロパティを返さない場合、コンテナーは NonResponsiveConsumerEvent を公開します。デフォルトでは、このチェックは各コンテナーで 30 秒ごとに 1 回実行されます。この動作を変更するには、リスナーコンテナーを構成するときに、ContainerProperties で monitorInterval (デフォルトは 30 秒)および noPollThreshold (デフォルトは 3.0)プロパティを設定します。noPollThreshold は、競合状態が原因で誤ったイベントが発生しないように、1.0 よりも大きくする必要があります。このようなイベントを受信すると、コンテナーを停止できるため、コンシューマーを起こして停止させることができます。

バージョン 2.6.2 以降、コンテナーが ListenerContainerIdleEvent を公開している場合、その後レコードが受信されたときに ListenerContainerNoLongerIdleEvent が公開されます。

イベントの消費

これらのイベントは、ApplicationListener を実装することでキャプチャーできます。一般的なリスナー、またはこの特定のイベントのみを受信するように絞り込まれたリスナーのいずれかです。Spring Framework 4.2 で導入された @EventListener も使用できます。

次の例では、@KafkaListener と @EventListener を 1 つのクラスに結合しています。アプリケーションリスナーはすべてのコンテナーのイベントを取得するため、アイドル状態のコンテナーに基づいて特定のアクションを実行する場合は、リスナー ID を確認する必要があることを理解しておく必要があります。この目的のために @EventListener condition を使用することもできます。

イベントのプロパティについては、アプリケーションイベントを参照してください。

イベントは通常、コンシューマースレッドで公開されるため、Consumer オブジェクトと安全にやり取りできます。

次の例では、@KafkaListener と @EventListener の両方を使用しています。

public class Listener {

    @KafkaListener(id = "qux", topics = "annotated")
    public void listen4(@Payload String foo, Acknowledgment ack) {
        ...
    }

    @EventListener(condition = "event.listenerId.startsWith('qux-')")
    public void eventHandler(ListenerContainerIdleEvent event) {
        ...
    }

}
イベントリスナーは、すべてのコンテナーのイベントを表示します。前の例では、リスナー ID に基づいて受信するイベントを絞り込みます。@KafkaListener 用に作成されたコンテナーは並行性をサポートするため、実際のコンテナーには id-n という名前が付けられます。n は、並行性をサポートする各インスタンスの一意の値です。ということでコンディションで startsWith を採用。
アイドルイベントを使用してリスタコンテナーを停止する場合は、リスナーを呼び出すスレッドで container.stop() を呼び出さないでください。これを行うと、遅延や不要なログメッセージが発生します。代わりに、コンテナーを停止できる別のスレッドにイベントを渡す必要があります。また、コンテナーインスタンスが子コンテナーである場合、コンテナーインスタンスを stop() しないでください。代わりに、並行コンテナーを停止する必要があります。
アイドル時の現在位置

リスナーに ConsumerSeekAware を実装することで、アイドルが検出されたときの現在位置を取得できることに注意してください。特定のオフセットを求めての onIdleContainer() を参照してください。

4.1.8. トピック / パーティションの初期オフセット

パーティションの初期オフセットを設定するには、いくつかの方法があります。

パーティションを手動で割り当てる場合、構成済みの TopicPartitionOffset 引数に初期オフセットを設定できます (必要に応じて) ( メッセージリスナコンテナーを参照)。いつでも特定のオフセットにシークすることもできます。

ブローカーがパーティションを割り当てるグループ管理を使用する場合:

  • 新しい group.id の場合、初期オフセットは auto.offset.reset コンシューマープロパティ (earliest または latest) によって決定されます。

  • 既存のグループ ID の場合、初期オフセットはそのグループ ID の現在のオフセットです。ただし、初期化中 (またはその後のいつでも) に特定のオフセットをシークできます。

4.1.9. 特定のオフセットを求めて

シークするには、リスナーは次のメソッドを持つ 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 は、コンテナーの起動時およびパーティションが割り当てられるたびに呼び出されます。このコールバックは、初期化後の任意の時点でシークするときに使用する必要があります。コールバックへの参照を保存する必要があります。複数のコンテナー (または ConcurrentMessageListenerContainer) で同じリスナーを使用する場合は、コールバックを ThreadLocal またはリスナー Thread によってキー設定されたその他の構造に保存する必要があります。

グループ管理の場合、パーティションの割り当て時に onPartitionsAssigned が呼び出されます。たとえば、コールバックを呼び出すことにより、パーティションの初期オフセットを設定するために、このメソッドを使用できます。このメソッドを使用して、このスレッドのコールバックを割り当てられたパーティションに関連付けることもできます (以下の例を参照)。registerSeekCallback に渡されるものではなく、コールバック引数を使用する必要があります。バージョン 2.5.5 以降では、手動パーティション割り当てを使用している場合でも、このメソッドが呼び出されます。

onPartitionsRevoked は、コンテナーが停止したとき、または Kafka が割り当てを取り消したときに呼び出されます。このスレッドのコールバックを破棄し、取り消されたパーティションへの関連付けをすべて削除する必要があります。

コールバックには次のメソッドがあります。

void seek(String topic, int partition, long offset);

void seekToBeginning(String topic, int partition);

void seekToBeginning(Collection=<TopicPartitions> partitions);

void seekToEnd(String topic, int partition);

void seekToEnd(Collection=<TopicPartitions> partitions);

void seekRelative(String topic, int partition, long offset, boolean toCurrent);

void seekToTimestamp(String topic, int partition, long timestamp);

void seekToTimestamp(Collection<TopicPartition> topicPartitions, long timestamp);

seekRelative は、相対シークを実行するためにバージョン 2.3 で追加されました。

  • offset 負および toCurrent false - パーティションの最後に対してシークします。

  • offset ポジティブおよび toCurrent false - パーティションの先頭からの相対シーク。

  • offset 負および toCurrent true - 現在の位置に対してシーク (巻き戻し)。

  • offset 正および toCurrent true - 現在の位置を基準にシークします (早送り)。

seekToTimestamp メソッドもバージョン 2.3 で追加されました。

onIdleContainer または onPartitionsAssigned メソッドで複数のパーティションに対して同じタイムスタンプをシークする場合は、コンシューマーの offsetsForTimes メソッドへの 1 回の呼び出しでタイムスタンプのオフセットを見つける方が効率的であるため、2 番目の方法が推奨されます。他の場所から呼び出されると、コンテナーはすべてのタイムスタンプシークリクエストを収集し、offsetsForTimes を 1 回呼び出します。

アイドル状態のコンテナーが検出されたときに、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 クラスを追加しました。これは、トピック / パーティションに使用されるコールバックを追跡します。次の例は、コンテナーがアイドル状態になるたびに、各パーティションで最後に処理されたレコードを探す方法を示しています。また、任意の外部呼び出しでパーティションを 1 レコードずつ巻き戻すことを可能にするメソッドもあります。

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.10. コンテナーファクトリ

@KafkaListener アノテーションに従って、ConcurrentKafkaListenerContainerFactory はアノテーション付きメソッドのコンテナーを作成するために使用されます。

バージョン 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.11. スレッドセーフ

同時メッセージリスナーコンテナーを使用する場合、単一のリスナーインスタンスがすべてのコンシューマースレッドで呼び出されます。リスナーはスレッドセーフである必要があり、ステートレスリスナーを使用することをお勧めします。リスナーをスレッドセーフにすることができない場合、または同期を追加すると同時実行性を追加するメリットが大幅に減少する場合は、次のいずれかの手法を使用できます。

  • n コンテナーを concurrency=1 で使用し、プロトタイプスコープ MessageListener Bean を使用して、各コンテナーが独自のインスタンスを取得できるようにします (これは、@KafkaListener を使用する場合は不可能です)。

  • 状態を ThreadLocal<?> インスタンスに保持します。

  • シングルトンリスナーに、SimpleThreadScope (または同様のスコープ)で宣言されている Bean に委譲させます。

スレッド状態 (前のリストの 2 番目と 3 番目の項目) のクリーンアップを容易にするために、バージョン 2.2 以降、リスナーコンテナーは各スレッドが終了するときに ConsumerStoppedEvent を発行します。これらのイベントを ApplicationListener または @EventListener メソッドで使用して、スコープから ThreadLocal<?> インスタンスまたは remove() スレッドスコープ Bean を削除できます。SimpleThreadScope は、破棄インターフェースを持つ Bean ( DisposableBean など) を破棄しないため、インスタンスを自分で destroy() する必要があることに注意してください。

デフォルトでは、アプリケーションコンテキストのイベントマルチキャスタは、呼び出しスレッドでイベントリスナーを呼び出します。マルチキャスタを変更して非同期エグゼキュータを使用する場合、スレッドのクリーンアップは効果的ではありません。

4.1.12. モニター

リスナーのパフォーマンスの監視

バージョン 2.3 以降、Micrometer がクラスパスで検出され、単一の MeterRegistry がアプリケーションコンテキストに存在する場合、リスナーコンテナーはリスナーの Micrometer Timer を自動的に作成および更新します。ContainerProperty micrometerEnabled を false に設定することで、タイマーを無効にすることができます。

2 つのタイマーが維持されます。1 つはリスナーの呼び出しが成功した場合、もう 1 つは失敗した場合です。

タイマーには spring.kafka.listener という名前が付けられ、次のタグが付いています。

  • name : (コンテナー Bean 名)

  • result : success または failure

  • exception : none または ListenerExecutionFailedException

ContainerProperties micrometerTags プロパティを使用してタグを追加できます。

バージョン 2.9.8、3.0.6 以降では、ContainerProperties micrometerTagsProvider で関数を提供できます。この関数は ConsumerRecord<?, ?> を受け取り、そのレコードに基づいて micrometerTags 内の静的タグとマージできるタグを返します。

並行コンテナーを使用すると、スレッドごとにタイマーが作成され、name タグに -n という接尾辞が付けられます。ここで、n は 0 から concurrency-1 です。
KafkaTemplate パフォーマンスの監視

バージョン 2.5 以降、Micrometer がクラスパスで検出され、単一の MeterRegistry がアプリケーションコンテキストに存在する場合、テンプレートは送信操作用の Micrometer Timer を自動的に作成および更新します。テンプレートの micrometerEnabled プロパティを false に設定することで、タイマーを無効にすることができます。

2 つのタイマーが維持されます。1 つはリスナーの呼び出しが成功した場合、もう 1 つは失敗した場合です。

タイマーには spring.kafka.template という名前が付けられ、次のタグが付いています。

  • name : (テンプレート Bean 名)

  • result : success または failure

  • exception : none または失敗時の例外クラス名

テンプレートの micrometerTags プロパティを使用して、タグを追加できます。

バージョン 2.9.8、3.0.6 以降では、KafkaTemplate.setMicrometerTagsProvider(Function<ProducerRecord<?, ?>, Map<String, String>>) プロパティを提供できます。この関数は ProducerRecord<?, ?> を受け取り、そのレコードに基づいて micrometerTags 内の静的タグとマージできるタグを返します。

Micrometer ネイティブメトリクス

バージョン 2.5 以降、フレームワークは、プロデューサーとコンシューマーが作成およびクローズされるたびに Micrometer KafkaClientMetrics インスタンスを管理するファクトリリスナーを提供します。

この機能を有効にするには、リスナーをプロデューサーおよびコンシューマーファクトリに追加するだけです。

@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 メトリクスの 1 つを取得する例
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 サポートを参照してください。

Micrometer Observation

バージョン 3.0 以降、KafkaTemplate およびリスナーコンテナーの監視に Micrometer を使用することがサポートされるようになりました。

KafkaTemplate と ContainerProperties の observationEnabled を true に設定して、観測を有効にします。これにより、タイマーが監視ごとに管理されるようになるため、Micrometer タイマーが無効になります。

詳細については、Micrometer トレース (英語) を参照してください。

タイマー / トレースにタグを追加するには、カスタム KafkaTemplateObservationConvention または KafkaListenerObservationConvention をテンプレートまたはリスナーコンテナーにそれぞれ構成します。

デフォルトの実装では、テンプレートの監視用に bean.name タグが追加され、コンテナー用に listener.id タグが追加されます。

DefaultKafkaTemplateObservationConvention または DefaultKafkaListenerObservationConvention をサブクラス化するか、まったく新しい実装を提供できます。

記録されるデフォルトの観測値の詳細については、"Micrometer 観測資料" を参照してください。

バージョン 3.0.6 以降では、コンシューマーまたはプロデューサーレコードの情報に基づいて、タイマーとトレースに動的タグを追加できます。これを行うには、カスタム KafkaListenerObservationConvention および / または KafkaTemplateObservationConvention をそれぞれリスナーコンテナープロパティまたは KafkaTemplate に追加します。両方の観測コンテキストの record プロパティには、それぞれ ConsumerRecord または ProducerRecord が含まれます。

4.1.13. トランザクション

このセクションでは、Spring for Apache Kafka がトランザクションをサポートする方法について説明します。

概要

0.11.0.0 クライアントライブラリでは、トランザクションのサポートが追加されました。Spring for Apache Kafka は、次の方法でサポートを追加します。

  • KafkaTransactionManager: 通常の Spring トランザクションサポート (@TransactionalTransactionTemplate など) で使用されます。

  • トランザクション KafkaMessageListenerContainer

  • KafkaTemplate とのローカルトランザクション

  • 他のトランザクションマネージャーとのトランザクションの同期

トランザクションは、DefaultKafkaProducerFactory に transactionIdPrefix を提供することによって有効になります。その場合、単一の共有 Producer を管理する代わりに、ファクトリはトランザクションプロデューサーのキャッシュを維持します。ユーザーがプロデューサーで close() を呼び出すと、プロデューサーは実際に閉じられるのではなく、再利用のためにキャッシュに返されます。各プロデューサーの transactional.id プロパティは transactionIdPrefix + n です。ここで、n は 0 で始まり、新しいプロデューサーごとに増分されます。以前のバージョンの Spring for Apache Kafka では、3.0 で始まる唯一のオプションである EOSMode.V2 を使用して、フェンシングゾンビをサポートするために、レコードベースのリスナーを持つリスナーコンテナーによって開始されたトランザクションに対して transactional.id が異なる方法で生成されました。複数のインスタンスで実行されているアプリケーションの場合、transactionIdPrefix はインスタンスごとに一意である必要があります。

正確に一度セマンティクスも参照してください。

transactionIdPrefix も参照してください。

Spring Boot では、spring.kafka.producer.transaction-id-prefix プロパティを設定するだけです。Boot は自動的に KafkaTransactionManager Bean を構成し、それをリスナーコンテナーに接続します。

バージョン 2.5.8 以降、プロデューサーファクトリで maxAge プロパティを設定できるようになりました。これは、ブローカーの transactional.id.expiration.ms のためにアイドル状態になる可能性のあるトランザクションプロデューサーを使用する場合に役立ちます。現在の kafka-clients では、これによりリバランスなしで ProducerFencedException が発生する可能性があります。maxAge を transactional.id.expiration.ms 未満に設定することにより、ファクトリはプロデューサーが最大年齢を過ぎた場合にプロデューサーをリフレッシュします。
KafkaTransactionManager を使用する

KafkaTransactionManager は、Spring Framework の PlatformTransactionManager の実装です。コンストラクターでプロデューサーファクトリへの参照が提供されます。カスタムプロデューサーファクトリを提供する場合は、トランザクションをサポートする必要があります。ProducerFactory.transactionCapable() を参照してください。

KafkaTransactionManager は、通常の Spring トランザクションサポート (@TransactionalTransactionTemplate など) とともに使用できます。トランザクションがアクティブな場合、トランザクションのスコープ内で実行されるすべての KafkaTemplate 操作は、トランザクションの Producer を使用します。マネージャーは、成功または失敗に応じて、トランザクションをコミットまたはロールバックします。トランザクションマネージャーと同じ ProducerFactory を使用するように KafkaTemplate を構成する必要があります。

トランザクションの同期

このセクションでは、プロデューサーのみのトランザクション (リスナーコンテナーによって開始されないトランザクション) を参照します。コンテナーがトランザクションを開始するときのトランザクションのチェーンについては、コンシューマー主導のトランザクションの使用を参照してください。

レコードを 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 ファーストまたは DB ファースト構成で JDBC および Kafka トランザクションを同期するアプリケーションの例については、他のトランザクションマネージャーとの Kafka トランザクションの例を参照してください。

バージョン 2.5.17, 2.6.12, 2.7.9, 2.8.0 以降、同期されたトランザクションでコミットが失敗した場合(プライマリトランザクションがコミットした後)、例外が呼び出し元にスローされます。以前は、これは確認なしで無視されていました(デバッグ時にログに記録されました)。アプリケーションは、コミットされたプライマリトランザクションを補うために、必要に応じて是正措置を講じる必要があります。
コンシューマー主導のトランザクションの使用

バージョン 2.7 以降、ChainedKafkaTransactionManager は非推奨になりました。詳細については、スーパークラス ChainedTransactionManager の javadoc を参照してください。代わりに、コンテナーで KafkaTransactionManager を使用して 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

唯一サポートされているモードである EOSMode.V2 (別名 BETA) では、コンシューマーが開始したトランザクションであっても、同じ transactional.id を使用する必要がなくなりました。実際、プロデューサーが開始したトランザクションと同じように、インスタンスごとに一意である必要があります。このプロパティは、アプリケーションインスタンスごとに異なる値を持つ必要があります。

KafkaTemplate トランザクションおよび非トランザクションパブリッシング

通常、KafkaTemplate がトランザクション (トランザクション対応のプロデューサーファクトリで構成されている) である場合、トランザクションが必要です。トランザクションは、TransactionTemplate@Transactional メソッド、executeInTransaction の呼び出し、または KafkaTransactionManager で構成されている場合はリスナーコンテナーによって開始できます。トランザクションの範囲外でテンプレートを使用しようとすると、テンプレートは IllegalStateException をスローします。バージョン 2.4.3 以降、テンプレートの allowNonTransactional プロパティを true に設定できます。その場合、テンプレートは ProducerFactory の createNonTransactionalProducer() メソッドを呼び出すことにより、トランザクションなしで操作を実行できるようにします。プロデューサーは、再利用のために通常どおりキャッシュされるか、スレッドにバインドされます。DefaultKafkaProducerFactory の使用を参照してください。

バッチリスナーとのトランザクション

トランザクションの使用中にリスナーが失敗すると、AfterRollbackProcessor が呼び出され、ロールバックの発生後に何らかのアクションが実行されます。レコードリスナーでデフォルトの AfterRollbackProcessor を使用する場合、失敗したレコードが再配信されるようにシークが実行されます。ただし、バッチリスナーを使用すると、フレームワークはバッチ内のどのレコードが失敗したかを認識できないため、バッチ全体が再配信されます。詳細については、ロールバック後のプロセッサーを参照してください。

バッチリスナーを使用する場合、バージョン 2.4.2 では、バッチ処理中の障害に対処するための代替メカニズムが導入されました。BatchToRecordAdapterbatchListener が true に設定されたコンテナーファクトリが BatchToRecordAdapter で構成されている場合、リスナーは一度に 1 つのレコードで呼び出されます。これにより、バッチ内のエラー処理が可能になり、例外の型に応じてバッチ全体の処理を停止することができます。デフォルトの BatchToRecordAdapter が提供されており、DeadLetterPublishingRecoverer などの標準の ConsumerRecordRecoverer で構成できます。次のテストケース構成スニペットは、この機能の使用方法を示しています。

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.14. 正確に一度セマンティクス

リスナーコンテナーに KafkaAwareTransactionManager インスタンスを提供できます。そのように構成されている場合、コンテナーはリスナーを呼び出す前にトランザクションを開始します。リスナーによって実行されるすべての KafkaTemplate 操作は、トランザクションに参加します。リスナーがレコード (または BatchMessageListener を使用している場合は複数のレコード) を正常に処理した場合、トランザクションマネージャーがトランザクションをコミットする前に、コンテナーは producer.sendOffsetsToTransaction() を使用してオフセットをトランザクションに送信します。リスナーが例外をスローした場合、トランザクションはロールバックされ、ロールバックされたレコードを次のポーリングで取得できるようにコンシューマーが再配置されます。詳細および繰り返し失敗するレコードの処理については、ロールバック後のプロセッサーを参照してください。

トランザクションを使用すると、Exactly Once Semantics (EOS) が有効になります。

これは、read→process-write シーケンスの場合、シーケンスが 1 回だけ完了することが保証されることを意味します。(読み取りとプロセスには、少なくとも 1 回のセマンティクスがあります)。

Spring for Apache Kafka バージョン 3.0 以降は EOSMode.V2 のみをサポートします。

  • V2 - 別名 fetch-offset-request フェンシング (バージョン 2.5 以降)

これには、ブローカーがバージョン 2.5 以降である必要があります。

モード V2 では、コンシューマーメタデータがオフセットとともにトランザクションに送信され、ブローカーは代わりにその情報を使用してプロデューサーがフェンスされているかどうかを判断できるため、各 group.id/topic/partition のプロデューサーを持つ必要はありません。

詳細については、KIP-447 [Apache] (英語) を参照してください。

V2 は以前は BETA でした。フレームワークを KIP-732 [Apache] (英語) に合わせるために EOSMode が変更されました。

4.1.15. Spring Bean をプロデューサー / コンシューマーインターセプターに接続する

Apache Kafka は、プロデューサーとコンシューマーにインターセプターを追加するメカニズムを提供します。これらのオブジェクトは、Spring ではなく Kafka によって管理されるため、通常の 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.16. Spring で管理される Producer Interceptor

バージョン 3.0.0 以降、プロデューサーインターセプターに関しては、インターセプターのクラス名を Apache Kafka プロデューサー構成に指定する代わりに、Spring がそれを Bean として直接管理できるようにすることができます。このアプローチを採用する場合は、このプロデューサーインターセプターを KafkaTemplate に設定する必要があります。以下は、上記と同じ MyProducerInterceptor を使用する例ですが、内部構成プロパティを使用しないように変更されています。

public class MyProducerInterceptor implements ProducerInterceptor<String, String> {

    private final SomeBean bean;

    public MyProducerInterceptor(SomeBean bean) {
        this.bean = bean;
    }

    @Override
    public void configure(Map<String, ?> configs) {

    }

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

}
@Bean
public MyProducerInterceptor myProducerInterceptor(SomeBean someBean) {
  return new MyProducerInterceptor(someBean);
}

@Bean
public KafkaTemplate<String, String> kafkaTemplate(ProducerFactory<String, String> pf, MyProducerInterceptor myProducerInterceptor) {
   KafkaTemplate<String, String> kafkaTemplate = new KafkaTemplate<String, String>(pf);
   kafkaTemplate.setProducerInterceptor(myProducerInterceptor);
}

レコードが送信される直前に、プロデューサーインターセプターの onSend メソッドが呼び出されます。サーバーがデータの公開に関する確認を送信すると、onAcknowledgement メソッドが呼び出されます。onAcknowledgement は、プロデューサーがユーザーコールバックを呼び出す直前に呼び出されます。

Spring を介して管理され、KafkaTemplate に適用する必要があるこのようなプロデューサーインターセプターが複数ある場合は、代わりに CompositeProducerInterceptor を使用する必要があります。CompositeProducerInterceptor では、個々のプロデューサーインターセプターを順番に追加できます。基盤となる ProducerInterceptor 実装のメソッドは、CompositeProducerInterceptor に追加された順序で呼び出されます。

4.1.17. リスナーコンテナーの一時停止と再開

バージョン 2.1.3 は、pause() および resume() メソッドをリスナーコンテナーに追加しました。以前は、ConsumerAwareMessageListener 内でコンシューマーを一時停止し、Consumer オブジェクトへのアクセスを提供する ListenerContainerIdleEvent をリッスンすることで再開できました。イベントリスナーを使用してアイドル状態のコンテナー内のコンシューマーを一時停止することはできますが、場合によっては、イベントリスナーがコンシューマースレッドで呼び出される保証がないため、これはスレッドセーフではありませんでした。コンシューマーを安全に一時停止および再開するには、リスナーコンテナーで pause および resume メソッドを使用する必要があります。pause() は、次の poll() の直前に有効になります。resume() は、現在の poll() が戻った直後に有効になります。コンテナーが一時停止されると、引き続きコンシューマーの poll() に進み、グループ管理が使用されている場合のリバランスを回避しますが、レコードは取得しません。詳細については、Kafka のドキュメントを参照してください。

バージョン 2.1.5 以降、isPauseRequested() を呼び出して、pause() が呼び出されたかどうかを確認できます。ただし、コンシューマーは実際にはまだ一時停止していない可能性があります。すべての Consumer インスタンスが実際に一時停止している場合、isConsumerPaused() は true を返します。

さらに (2.1.5 以降)、ConsumerPausedEvent および ConsumerResumedEvent インスタンスは、source プロパティとしてコンテナーとともに公開され、TopicPartition インスタンスは partitions プロパティに含まれます。

バージョン 2.9 以降、新しいコンテナープロパティ pauseImmediate を true に設定すると、現在のレコードが処理された後に一時停止が有効になります。デフォルトでは、前回のポーリングのすべてのレコードが処理されると一時停止が有効になります。[ 一時停止 ] を参照してください。

次の単純な 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.18. リスナーコンテナーのパーティションの一時停止と再開

バージョン 2.7 以降、リスナーコンテナーで pausePartition(TopicPartition topicPartition) および resumePartition(TopicPartition topicPartition) メソッドを使用することにより、そのコンシューマーに割り当てられた特定のパーティションの使用を一時停止および再開できます。一時停止と再開は、pause() と resume() の方法と同様に、poll() の前後にそれぞれ行われます。そのパーティションの一時停止がリクエストされた場合、isPartitionPauseRequested() メソッドは true を返します。isPartitionPaused() メソッドは、そのパーティションが実質的に一時停止されている場合に true を返します。

また、バージョン 2.7 以降、ConsumerPartitionPausedEvent および ConsumerPartitionResumedEvent インスタンスは、コンテナーを source プロパティおよび TopicPartition インスタンスとして公開されます。

4.1.19. 直列化、逆直列化、メッセージ変換

概要

Apache Kafka は、レコード値とそのキーを直列化およびデ直列化するための高レベル API を提供します。これは、いくつかの組み込み実装を備えた org.apache.kafka.common.serialization.Serializer<T> および org.apache.kafka.common.serialization.Deserializer<T> 抽象化に存在します。一方、Producer または Consumer 構成プロパティを使用して、シリアライザークラスとデシリアライザークラスを指定できます。次の例は、その方法を示しています。

props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
...
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

より複雑なケースや特殊なケースでは、KafkaConsumer (したがって KafkaProducer) は、keys および values のそれぞれの Serializer および Deserializer インスタンスを受け入れるオーバーロードされたコンストラクターを提供します。

この API を使用する場合、DefaultKafkaProducerFactory および DefaultKafkaConsumerFactory は (コンストラクターまたは setter メソッドを介して) プロパティを提供し、カスタム Serializer および Deserializer インスタンスをターゲット Producer または Consumer に挿入します。また、コンストラクターを介して Supplier<Serializer> または Supplier<Deserializer> インスタンスを渡すこともできます。これらの Supplier は、各 Producer または Consumer の作成時に呼び出されます。

文字列の直列化

バージョン 2.5 以降、Spring for Apache Kafka は、エンティティのストリング表現を使用する ToStringSerializer および ParseStringDeserializer クラスを提供します。それらは、メソッド toString といくつかの Function<String> または BiFunction<String, Headers> に依存して、文字列を解析し、インスタンスのプロパティを設定します。通常、これは parse などのクラスの静的メソッドを呼び出します。

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

デフォルトでは、ToStringSerializer は、レコード Headers 内のシリアライズされたエンティティに関する型情報を伝達するように構成されています。これを無効にするには、addTypeInfo プロパティを false に設定します。この情報は、受信側の ParseStringDeserializer で使用できます。

  • ToStringSerializer.ADD_TYPE_INFO_HEADERS (デフォルト true): これを false に設定して、ToStringSerializer でこの機能を無効にすることができます (addTypeInfo プロパティを設定します)。

ParseStringDeserializer<Object> deserializer = new ParseStringDeserializer<>((str, headers) -> {
    byte[] header = headers.lastHeader(ToStringSerializer.VALUE_TYPE).value();
    String entityType = new String(header);

    if (entityType.contains("Thing")) {
        return Thing.parse(str);
    }
    else {
        // ...parsing logic
    }
});

String を byte[] との間で変換するために使用される Charset を構成できます。デフォルトは UTF-8 です。

ConsumerConfig プロパティを使用して、パーサーメソッドの名前でデシリアライザーを構成できます。

  • ParseStringDeserializer.KEY_PARSER

  • ParseStringDeserializer.VALUE_PARSER

プロパティには、クラスの完全修飾名とそれに続くメソッド名をピリオド . で区切って含める必要があります。メソッドは静的で、(String, Headers) または (String) のいずれかの署名を持つ必要があります。

Kafka ストリームで使用するための ToFromStringSerde も提供されます。

JSON

Spring for Apache Kafka は、Jackson JSON オブジェクトマッパーに基づく JsonSerializer および JsonDeserializer 実装も提供します。JsonSerializer では、任意の Java オブジェクトを JSON byte[] として書き込むことができます。JsonDeserializer には、消費された byte[] を適切なターゲットオブジェクトに逆直列化できるように、追加の Class<?> targetType 引数が必要です。次の例は、JsonDeserializer を作成する方法を示しています。

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

JsonSerializer と JsonDeserializer の両方を ObjectMapper でカスタマイズできます。継承して、configure(Map<String, ?> configs, boolean isKey) メソッドで特定の構成ロジックを実装することもできます。

バージョン 2.3 以降、すべての JSON 対応コンポーネントはデフォルトで JacksonUtils.enhancedObjectMapper() インスタンスで構成され、MapperFeature.DEFAULT_VIEW_INCLUSION および DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 機能が無効になっています。また、このようなインスタンスには、Java 時間や Kotlin サポートなどのカスタムデータ型用のよく知られたモジュールが付属しています。詳細については、JacksonUtils.enhancedObjectMapper() JavaDocs を参照してください。このメソッドは、ネットワークを介したプラットフォーム間の互換性のために、org.springframework.util.MimeType オブジェクトの直列化用の org.springframework.kafka.support.JacksonMimeTypeModule をプレーン文字列に登録します。JacksonMimeTypeModule は、アプリケーションコンテキストで Bean として登録でき、Spring Boot ObjectMapper インスタンスに自動構成されます。

また、バージョン 2.3 以降、JsonDeserializer は、ターゲットジェネリクスコンテナー型の処理を改善するための TypeReference ベースのコンストラクターを提供します。

バージョン 2.1 以降、レコード Headers で型情報を伝達できるようになり、複数の型を処理できるようになりました。さらに、次の Kafka プロパティを使用してシリアライザーとデシリアライザーを構成できます。KafkaConsumer および KafkaProducer にそれぞれ Serializer および Deserializer インスタンスを提供した場合、効果がありません。

プロパティの構成
  • 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.utiljava.lang): 逆直列化が許可されているパッケージパターンのカンマ区切りのリスト。* は、すべてをデシリアライズすることを意味します。

  • JsonDeserializer.TYPE_MAPPINGS (デフォルト empty): マッピング型を参照してください。

  • JsonDeserializer.KEY_TYPE_METHOD (デフォルト empty): メソッドを使用して型を決定するを参照してください。

  • JsonDeserializer.VALUE_TYPE_METHOD (デフォルト empty): メソッドを使用して型を決定するを参照してください。

バージョン 2.2 以降、型情報ヘッダー(シリアライザーによって追加された場合)はデシリアライザーによって削除されます。removeTypeHeaders プロパティを false に設定することにより、デシリアライザで直接、前述の構成プロパティを使用して、以前の動作に戻すことができます。

バージョン 2.8 以降、プログラム構築に示すようにプログラムでシリアライザーまたはデシリアライザーを構築する場合、プロパティを明示的に設定していない限り(set*() メソッドまたは流れるような API を使用)、上記のプロパティがファクトリによって適用されます。以前は、プログラムで作成するときに、構成プロパティが適用されることはありませんでした。これは、オブジェクトにプロパティを直接明示的に設定した場合にも当てはまります。
マッピング型

バージョン 2.2 以降、JSON を使用する場合、前述のリストのプロパティを使用して型 マッピングを提供できるようになりました。以前は、シリアライザーとデシリアライザー内の型マッパーをカスタマイズする必要がありました。マッピングは、token:className ペアのコンマ区切りリストで構成されます。送信では、ペイロードのクラス名が対応するトークンにマッピングされます。受信では、型 ヘッダーのトークンが対応するクラス名にマップされます。

次の例では、一連のマッピングを作成します。

senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
senderProps.put(JsonSerializer.TYPE_MAPPINGS, "cat:com.mycat.Cat, hat:com.myhat.hat");
...
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
consumerProps.put(JsonDeSerializer.TYPE_MAPPINGS, "cat:com.yourcat.Cat, hat:com.yourhat.hat");
対応するオブジェクトには互換性が必要です。

Spring Boot を使用する場合、これらのプロパティを application.properties (または yaml) ファイルで提供できます。次の例は、その方法を示しています。

spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties.spring.json.type.mapping=cat:com.mycat.Cat,hat:com.myhat.Hat

プロパティを使用した簡単な構成のみを実行できます。より高度な構成 (シリアライザーとデシリアライザーでカスタム ObjectMapper を使用するなど) の場合は、事前に構築されたシリアライザーとデシリアライザーを受け入れるプロデューサーとコンシューマーファクトリコンストラクターを使用する必要があります。次の Spring 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 も提供されています。

バージョン 2.2 以降、ブール値 useHeadersIfPresent (デフォルトでは true ) を持つオーバーロードされたコンストラクターの 1 つを使用して、指定されたターゲット型を使用し、ヘッダー内の型情報を無視するようにデシリアライザーを明示的に構成できます。次の例は、その方法を示しています。

DefaultKafkaConsumerFactory<Integer, Cat1> cf = new DefaultKafkaConsumerFactory<>(props,
        new IntegerDeserializer(), new JsonDeserializer<>(Cat1.class, false));
メソッドを使用して型を決定する

バージョン 2.5 以降、プロパティを介してデシリアライザーを構成し、ターゲット型を決定するメソッドを呼び出すことができるようになりました。存在する場合、これは上で議論された他のテクニックのいずれかを上書きします。これは、データが Spring シリアライザーを使用しないアプリケーションによって公開され、データまたは他のヘッダーに応じて異なる型に逆直列化する必要がある場合に役立ちます。これらのプロパティをメソッド名に設定します。完全修飾クラス名の後にメソッド名が続き、ピリオド . で区切られます。メソッドは public static として宣言する必要があり、3 つの署名 (String topic, byte[] data, Headers headers)(byte[] data, Headers headers) または (byte[] data) のいずれかを持ち、Jackson JavaType を返します。

  • JsonDeserializer.KEY_TYPE_METHOD : spring.json.key.type.method

  • JsonDeserializer.VALUE_TYPE_METHOD : spring.json.value.type.method

任意のヘッダーを使用するか、データをインスペクションして型を判別できます。

サンプル
JavaType thing1Type = TypeFactory.defaultInstance().constructType(Thing1.class);

JavaType thing2Type = TypeFactory.defaultInstance().constructType(Thing2.class);

public static JavaType thingOneOrThingTwo(byte[] data, Headers headers) {
    // {"thisIsAFieldInThing1":"value", ...
    if (data[21] == '1') {
        return thing1Type;
    }
    else {
        return thing2Type;
    }
}

より高度なデータの場合、インスペクションは JsonPath などの使用を検討しますが、型を判別するためのテストが単純なほど、プロセスはより効率的になります。

以下は、デシリアライザーをプログラムで作成する例です (コンストラクターでコンシューマーファクトリにデシリアライザーを提供する場合)。

JsonDeserializer<Object> deser = new JsonDeserializer<>()
        .trustedPackages("*")
        .typeResolver(SomeClass::thing1Thing2JavaTypeForTopic);

...

public static JavaType thing1Thing2JavaTypeForTopic(String topic, byte[] data, Headers headers) {
    ...
}
プログラム構築

プロデューサー / コンシューマーファクトリで使用するためにシリアライザー / デシリアライザーをプログラムで構築する場合、バージョン 2.3 以降、流れるような 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);

または、流れるような API を使用してプロパティを構成したり、set*() メソッドを使用して設定したりしない限り、ファクトリは構成プロパティを使用してシリアライザー / デシリアライザーを構成します。プロパティの構成を参照してください。

シリアライザーとデシリアライザーの委譲
ヘッダーの使用

バージョン 2.3 は DelegatingSerializer および DelegatingDeserializer を導入しました。これにより、さまざまなキーおよび / または値型でレコードを生成および使用できます。プロデューサーは、ヘッダー DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR を、値に使用するシリアライザーとキーに DelegatingSerializer.KEY_SERIALIZATION_SELECTOR を選択するために使用されるセレクター値に設定する必要があります。一致するものが見つからない場合は、IllegalStateException がスローされます。

受信レコードの場合、デシリアライザーは同じヘッダーを使用して、使用するデシリアライザーを選択します。一致するものが見つからないか、ヘッダーが存在しない場合は、生の byte[] が返されます。

コンストラクターを介してセレクターのマップを Serializer / Deserializer に構成するか、キー DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG および DelegatingSerializer.KEY_SERIALIZATION_SELECTOR_CONFIG を使用して Kafka プロデューサー / コンシューマープロパティを介して構成できます。シリアライザーの場合、プロデューサープロパティは、キーがセレクターで値が Serializer インスタンス、シリアライザー Class、またはクラス名である Map<String, Object> にすることができます。プロパティは、次に示すように、カンマ区切りのマップエントリの文字列にすることもできます。

デシリアライザーの場合、コンシューマープロパティは、キーがセレクターで値が Deserializer インスタンス、デシリアライザー Class、またはクラス名である Map<String, Object> にすることができます。プロパティは、次に示すように、カンマ区切りのマップエントリの文字列にすることもできます。

プロパティを使用して構成するには、次の構文を使用します。

producerProps.put(DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG,
    "thing1:com.example.MyThing1Serializer, thing2:com.example.MyThing2Serializer")

consumerProps.put(DelegatingDeserializer.VALUE_SERIALIZATION_SELECTOR_CONFIG,
    "thing1:com.example.MyThing1Deserializer, thing2:com.example.MyThing2Deserializer")

その後、プロデューサーは DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR ヘッダーを thing1 または thing2 に設定します。

この手法は、異なる型を同じトピック (または異なるトピック) に送信することをサポートします。

バージョン 2.5.1 以降、型 (キーまたは値) が Serdes でサポートされている標準型 (LongInteger など) のいずれかである場合、セレクターヘッダーを設定する必要はありません。代わりに、シリアライザーはヘッダーを型のクラス名に設定します。これらの型のシリアライザーまたはデシリアライザーを構成する必要はありません。動的に (1 回だけ) 作成されます。

異なる型を異なるトピックに送信する別の手法については、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 以降では、マップキーがターゲットオブジェクトから割り当て可能かどうかを確認するようにシリアライザーを構成できます。これは、デリゲートシリアライザーがサブクラスをシリアライズできる場合に役立ちます。この場合、不一致の一致がある場合は、LinkedHashMap などの順序付けられた Map を提供する必要があります。

トピック別

バージョン 2.8 以降、DelegatingByTopicSerializer および DelegatingByTopicDeserializer では、トピック名に基づいてシリアライザー / デシリアライザーを選択できます。正規表現 Pattern は、使用するインスタンスを検索するために使用されます。マップは、コンストラクターを使用して、またはプロパティ(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 および 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));

再試行ポリシー、バックオフポリシーなどを使用した RetryTemplate の構成については、spring-retry [GitHub] (英語) プロジェクトを参照してください。

Spring メッセージングメッセージ変換

Serializer および Deserializer API は、低レベルの Kafka Consumer および Producer の観点からは非常にシンプルで柔軟ですが、@KafkaListener または Spring Integration の Apache Kafka サポートを使用する場合は、Spring メッセージングレベルでより柔軟性が必要になる場合があります。org.springframework.messaging.Message との間で簡単に変換できるようにするために、Spring for Apache Kafka は MessagingMessageConverter 実装とその JsonMessageConverter (およびサブクラス) のカスタマイズで MessageConverter 抽象化を提供します。@KafkaListener.containerFactory() プロパティの AbstractKafkaListenerContainerFactory Bean 定義を使用して、MessageConverter を KafkaTemplate インスタンスに直接挿入できます。次の例は、その方法を示しています。

@Bean
public KafkaListenerContainerFactory<?> kafkaJsonListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
        new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setRecordMessageConverter(new JsonMessageConverter());
    return factory;
}
...
@KafkaListener(topics = "jsonData",
                containerFactory = "kafkaJsonListenerContainerFactory")
public void jsonListener(Cat cat) {
...
}

Spring Boot を使用する場合は、コンバーターを @Bean として定義するだけで、Spring Boot の自動構成によって自動構成されたテンプレートとコンテナーファクトリに接続されます。

@KafkaListener を使用する場合、パラメーター型がメッセージコンバーターに提供され、変換を支援します。

この型推論は、@KafkaListener アノテーションがメソッドレベルで宣言されている場合にのみ実現できます。クラスレベルの @KafkaListener では、ペイロード型を使用して呼び出す @KafkaHandler メソッドを選択するため、メソッドを選択する前にすでに変換されている必要があります。

コンシューマー側では、JsonMessageConverter を構成できます。型 byte[]BytesString の ConsumerRecord 値を処理できるため、ByteArrayDeserializerBytesDeserializerStringDeserializer と組み合わせて使用する必要があります。(byte[] および Bytes は、不必要な byte[] から String への変換を回避するため、より効率的です)。必要に応じて、デシリアライザーに対応する JsonMessageConverter の特定のサブクラスを構成することもできます。

プロデューサー側では、Spring Integration または KafkaTemplate.send(Message<?> message) メソッド ( KafkaTemplate を使用するを参照) を使用する場合、構成済みの Kafka Serializer と互換性のあるメッセージコンバーターを構成する必要があります。

  • StringJsonMessageConverter と StringSerializer

  • BytesJsonMessageConverter と BytesSerializer

  • ByteArrayJsonMessageConverter と ByteArraySerializer

ここでも、byte[] または Bytes を使用すると、String から byte[] への変換が回避されるため、より効率的です。

便宜上、バージョン 2.3 以降、フレームワークは 3 つの値型すべてを直列化できる StringOrBytesSerializer も提供し、任意のメッセージコンバーターで使用できるようにします。

バージョン 2.7.1 以降、メッセージペイロードの変換を spring-messaging SmartMessageConverter に委譲できます。これにより、たとえば、MessageHeaders.CONTENT_TYPE ヘッダーに基づく変換が可能になります。

KafkaMessageConverter.fromMessage() メソッドは、ProducerRecord.value() プロパティにメッセージペイロードを含む ProducerRecord への送信変換のために呼び出されます。KafkaMessageConverter.toMessage() メソッドは、ペイロードが ConsumerRecord.value() プロパティである ConsumerRecord からの受信変換のために呼び出されます。SmartMessageConverter.toMessage() メソッドは、`fromMessage()` に渡された Message から新しい送信 Message<?> を作成するために呼び出されます(通常は KafkaTemplate.send(Message<?> msg) によって)。同様に、KafkaMessageConverter.toMessage() メソッドでは、コンバーターが ConsumerRecord から新しい Message<?> を作成した後、SmartMessageConverter.fromMessage() メソッドが呼び出され、新しく変換されたペイロードを使用して最終的な受信メッセージが作成されます。いずれの場合も、SmartMessageConverter が null を返す場合は、元のメッセージが使用されます。

デフォルトのコンバーターが KafkaTemplate およびリスナーコンテナーファクトリで使用される場合、テンプレートで setMessagingConverter() を呼び出し、@KafkaListener メソッドの contentMessageConverter プロパティを介して SmartMessageConverter を構成します。

例:

template.setMessagingConverter(mySmartConverter);
@KafkaListener(id = "withSmartConverter", topics = "someTopic",
    contentTypeConverter = "mySmartConverter")
public void smart(Thing thing) {
    ...
}
Spring Data 射影 インターフェースの使用

バージョン 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 を使用する

デシリアライザーがメッセージのデシリアライズに失敗すると、poll() が戻る前に問題が発生するため、Spring は問題を処理できません。この問題を解決するために、ErrorHandlingDeserializer が導入されました。このデシリアライザーは、実際のデシリアライザー (キーまたは値) に委譲します。デリゲートがレコードコンテンツの逆直列化に失敗した場合、ErrorHandlingDeserializer は、原因と生のバイトを含むヘッダーに null 値と DeserializationException を返します。レコードレベルの MessageListener を使用する場合、ConsumerRecord にキーまたは値の DeserializationException ヘッダーが含まれている場合、コンテナーの ErrorHandler は失敗した ConsumerRecord で呼び出されます。レコードはリスナーに渡されません。

または、Function<FailedDeserializationInfo, T> である failedDeserializationFunction を提供することにより、カスタム値を作成するように ErrorHandlingDeserializer を構成できます。この関数は、通常の方法でリスナーに渡される T のインスタンスを作成するために呼び出されます。すべてのコンテキスト情報を含む型 FailedDeserializationInfo のオブジェクトが関数に提供されます。ヘッダーに DeserializationException (シリアライズされた Java オブジェクトとして) があります。詳細については、ErrorHandlingDeserializer の Javadoc を参照してください。

キーと値の Deserializer オブジェクトを受け取り、適切なデリゲートで構成した適切な ErrorHandlingDeserializer インスタンスに接続する DefaultKafkaConsumerFactory コンストラクターを使用できます。または、コンシューマー構成プロパティ ( ErrorHandlingDeserializer によって使用される) を使用してデリゲートをインスタンス化することもできます。プロパティ名は ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS および ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS です。プロパティ値には、クラスまたはクラス名を指定できます。次の例は、これらのプロパティを設定する方法を示しています。

... // other props
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
props.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, JsonDeserializer.class);
props.put(JsonDeserializer.KEY_DEFAULT_TYPE, "com.example.MyKey")
props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.example.MyValue")
props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.example")
return new DefaultKafkaConsumerFactory<>(props);

次の例では、failedDeserializationFunction を使用しています。

public class BadFoo extends Foo {

  private final FailedDeserializationInfo failedDeserializationInfo;

  public BadFoo(FailedDeserializationInfo failedDeserializationInfo) {
    this.failedDeserializationInfo = failedDeserializationInfo;
  }

  public FailedDeserializationInfo getFailedDeserializationInfo() {
    return this.failedDeserializationInfo;
  }

}

public class FailedFooProvider implements Function<FailedDeserializationInfo, Foo> {

  @Override
  public Foo apply(FailedDeserializationInfo info) {
    return new BadFoo(info);
  }

}

前の例では、次の構成を使用しています。

...
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
consumerProps.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class);
consumerProps.put(ErrorHandlingDeserializer.VALUE_FUNCTION, FailedFooProvider.class);
...
コンシューマーが ErrorHandlingDeserializer で構成されている場合は、KafkaTemplate とそのプロデューサーを、通常のオブジェクトと、逆直列化の例外から生じる生の byte[] 値を処理できるシリアライザーで構成することが重要です。テンプレートのジェネリクス値型は Object である必要があります。1 つの手法は、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 を使用する場合は、メッセージヘッダーで逆直列化の例外を確認する必要があります。DefaultBatchErrorHandler とともに使用すると、そのヘッダーを使用して、例外が失敗したレコードを判別し、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());
    }
}
DeadLetterPublishingRecoverer も使用している場合、DeserializationException 用に公開されたレコードには、型 byte[] の record.value() が含まれます。これは直列化しないでください。byte[] には ByteArraySerializer を使用するように構成された DelegatingByTypeSerializer を使用し、他のすべての型には通常のシリアライザー (Json、Avro など) を使用することを検討してください。
バッチリスナーを使用したペイロード変換

バッチリスナーコンテナーファクトリを使用する場合、BatchMessagingMessageConverter 内で JsonMessageConverter を使用してバッチメッセージを変換することもできます。詳細については、直列化、逆直列化、メッセージ変換および Spring メッセージングメッセージ変換を参照してください。

デフォルトでは、変換の型はリスナー引数から推測されます。JsonMessageConverter を DefaultJackson2TypeMapper で構成し、その TypePrecedence が (デフォルトの INFERRED ではなく) TYPE_ID に設定されている場合、コンバーターはヘッダー (存在する場合) の型情報を代わりに使用します。これにより、たとえば、リスナーメソッドを具象クラスの代わりにインターフェースで宣言できます。また、型コンバーターはマッピングをサポートしているため、(データに互換性がある限り) ソースとは異なる型に逆直列化できます。これは、クラスレベルの @KafkaListener インスタンスを使用する場合にも役立ちます。この場合、呼び出すメソッドを決定するためにペイロードがすでに変換されている必要があります。次の例では、このメソッドを使用する Bean を作成します。

@Bean
public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setBatchListener(true);
    factory.setBatchMessageConverter(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 以降、リスナーメソッドの呼び出しのパラメーターを解決するためにデフォルトの o.s.messaging.handler.annotation.support.MessageHandlerMethodFactory によって使用される org.springframework.core.convert.ConversionService は、次のいずれかのインターフェースを実装するすべての Bean で提供されます。

  • org.springframework.core.convert.converter.Converter

  • org.springframework.core.convert.converter.GenericConverter

  • org.springframework.format.Formatter

これにより、ConsumerFactory および KafkaListenerContainerFactory のデフォルト構成を変更せずに、リスナーの逆直列化をさらにカスタマイズできます。

KafkaListenerConfigurer Bean を介して KafkaListenerEndpointRegistrar にカスタム MessageHandlerMethodFactory を設定すると、この機能が無効になります。
カスタム HandlerMethodArgumentResolver を @KafkaListener に追加する

バージョン 2.4.2 以降、独自の HandlerMethodArgumentResolver を追加し、カスタムメソッドパラメーターを解決できます。必要なのは KafkaListenerConfigurer を実装し、クラス KafkaListenerEndpointRegistrar のメソッド setCustomMethodArgumentResolvers() を使用することだけです。

@Configuration
class CustomKafkaConfig implements KafkaListenerConfigurer {

    @Override
    public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
        registrar.setCustomMethodArgumentResolvers(
            new HandlerMethodArgumentResolver() {

                @Override
                public boolean supportsParameter(MethodParameter parameter) {
                    return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType());
                }

                @Override
                public Object resolveArgument(MethodParameter parameter, Message<?> message) {
                    return new CustomMethodArgument(
                        message.getHeaders().get(KafkaHeaders.RECEIVED_TOPIC, String.class)
                    );
                }
            }
        );
    }

}

カスタム MessageHandlerMethodFactory を KafkaListenerEndpointRegistrar Bean に追加することで、フレームワークの引数解決を完全に置き換えることもできます。これを行う場合、アプリケーションが null value() (圧縮されたトピックなどから) を使用して tombstone レコードを処理する必要がある場合は、KafkaNullAwarePayloadArgumentResolver をファクトリに追加する必要があります。これは、すべての型をサポートし、@Payload アノテーションなしで引数と一致できるため、最後のリゾルバーである必要があります。DefaultMessageHandlerMethodFactory を使用している場合は、このリゾルバーを最後のカスタムリゾルバーとして設定します。ファクトリでは、このリゾルバーが KafkaNull ペイロードの知識を持たない標準 PayloadMethodArgumentResolver の前に使用されるようにします。

4.1.20. メッセージヘッダー

0.11.0.0 クライアントでは、メッセージ内のヘッダーのサポートが導入されました。バージョン 2.0 の時点で、Spring for Apache Kafka はこれらのヘッダーと spring-messaging MessageHeaders 間のマッピングをサポートするようになりました。

以前のバージョンでは、ConsumerRecord および ProducerRecord を spring-messaging Message<?> にマップしていました。ここで、値プロパティは payload との間でマップされ、他のプロパティ (topicpartition など) はヘッダーにマップされていました。これは引き続き当てはまりますが、追加の (任意の) ヘッダーをマップできるようになりました。

Apache Kafka ヘッダーには、次のインターフェース定義に示すようなシンプルな API があります。

public interface Header {

    String key();

    byte[] value();

}

KafkaHeaderMapper 戦略は、Kafka Headers と MessageHeaders の間でヘッダーエントリをマップするために提供されます。そのインターフェースの定義は次のとおりです。

public interface KafkaHeaderMapper {

    void fromHeaders(MessageHeaders headers, Headers target);

    void toHeaders(Headers source, Map<String, Object> target);

}

SimpleKafkaHeaderMapper は、生のヘッダーを byte[] としてマップし、String 値に変換するための構成オプションを備えています。

DefaultKafkaHeaderMapper はキーを MessageHeaders ヘッダー名にマップし、送信 メッセージの豊富なヘッダー型をサポートするために、JSON 変換が実行されます。「特別な」ヘッダー (キーが spring_json_header_types) には、<key>:<type> の JSON マップが含まれます。このヘッダーは、各ヘッダー値を元の型に適切に変換するために受信側で使用されます。

受信側では、すべての Kafka Header インスタンスが MessageHeaders にマップされます。送信側では、デフォルトで、idtimestampConsumerRecord プロパティにマッピングされるヘッダーを除くすべての MessageHeaders がマッピングされます。

マッパーにパターンを提供することで、送信 メッセージにマップするヘッダーを指定できます。次のリストは、多くのマッピング例を示しています。

public DefaultKafkaHeaderMapper() { (1)
    ...
}

public DefaultKafkaHeaderMapper(ObjectMapper objectMapper) { (2)
    ...
}

public DefaultKafkaHeaderMapper(String... patterns) { (3)
    ...
}

public DefaultKafkaHeaderMapper(ObjectMapper objectMapper, String... patterns) { (4)
    ...
}
1 例の前に説明したように、デフォルトの Jackson ObjectMapper を使用し、ほとんどのヘッダーをマップします。
2 例の前に説明したように、提供された Jackson ObjectMapper を使用し、ほとんどのヘッダーをマップします。
3 デフォルトの Jackson ObjectMapper を使用し、提供されたパターンに従ってヘッダーをマップします。
4 提供された Jackson ObjectMapper を使用し、提供されたパターンに従ってヘッダーをマッピングします。

パターンはかなり単純で、先頭のワイルドカード ()、末尾のワイルドカード、またはその両方 (たとえば、.cat.*) を含めることができます。先行する ! でパターンを否定できます。ヘッダー名 (正か負かにかかわらず) に一致する最初のパターンが勝ちます。

独自のパターンを提供する場合は、!id および !timestamp を含めることをお勧めします。これらのヘッダーは受信側で読み取り専用であるためです。

デフォルトでは、マッパーは java.lang および java.util のクラスのみをデシリアライズします。addTrustedPackages メソッドを使用して信頼できるパッケージを追加することにより、他の (またはすべての) パッケージを信頼できます。信頼できないソースからメッセージを受信した場合、信頼できるパッケージのみを追加することをお勧めします。すべてのパッケージを信頼するには、mapper.addTrustedPackages("*") を使用できます。
String ヘッダー値を未加工の形式でマッピングすると、マッパーの JSON 形式を認識していないシステムと通信する場合に役立ちます。

バージョン 2.2.5 以降、特定の文字列値のヘッダーを JSON を使用してマップするのではなく、生の byte[] との間でマップするように指定できます。AbstractKafkaHeaderMapper には新しいプロパティがあります。mapAllStringsOut が true に設定されている場合、すべての文字列値ヘッダーは charset プロパティ (デフォルトは UTF-8) を使用して byte[] に変換されます。さらに、header name : boolean のマップであるプロパティ rawMappedHeaders があります。マップにヘッダー名が含まれていて、ヘッダーに String 値が含まれている場合、文字セットを使用して生の byte[] としてマップされます。このマップは、マップ値のブール値が true である場合にのみ、文字セットを使用して生の受信 byte[] ヘッダーを String にマップするためにも使用されます。ブール値が false である場合、またはヘッダー名が true 値を持つマップにない場合、受信ヘッダーは単に生のマップされていないヘッダーとしてマップされます。

次のテストケースは、このメカニズムを示しています。

@Test
public void testSpecificStringConvert() {
    DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
    Map<String, Boolean> rawMappedHeaders = new HashMap<>();
    rawMappedHeaders.put("thisOnesAString", true);
    rawMappedHeaders.put("thisOnesBytes", false);
    mapper.setRawMappedHeaders(rawMappedHeaders);
    Map<String, Object> headersMap = new HashMap<>();
    headersMap.put("thisOnesAString", "thing1");
    headersMap.put("thisOnesBytes", "thing2");
    headersMap.put("alwaysRaw", "thing3".getBytes());
    MessageHeaders headers = new MessageHeaders(headersMap);
    Headers target = new RecordHeaders();
    mapper.fromHeaders(headers, target);
    assertThat(target).containsExactlyInAnyOrder(
            new RecordHeader("thisOnesAString", "thing1".getBytes()),
            new RecordHeader("thisOnesBytes", "thing2".getBytes()),
            new RecordHeader("alwaysRaw", "thing3".getBytes()));
    headersMap.clear();
    mapper.toHeaders(target, headersMap);
    assertThat(headersMap).contains(
            entry("thisOnesAString", "thing1"),
            entry("thisOnesBytes", "thing2".getBytes()),
            entry("alwaysRaw", "thing3".getBytes()));
}

デフォルトでは、両方のヘッダーマッパーがすべての受信ヘッダーをマップします。バージョン 2.8.8 以降、パターンは受信マッピングにも適用できます。受信マッピング用のマッパーを作成するには、それぞれのマッパーで静的メソッドの 1 つを使用します。

public static DefaultKafkaHeaderMapper forInboundOnlyWithMatchers(String... patterns) {
}

public static DefaultKafkaHeaderMapper forInboundOnlyWithMatchers(ObjectMapper objectMapper, String... patterns) {
}

public static SimpleKafkaHeaderMapper forInboundOnlyWithMatchers(String... patterns) {
}

例:

DefaultKafkaHeaderMapper inboundMapper = DefaultKafkaHeaderMapper.forInboundOnlyWithMatchers("!abc*", "*");

これにより、abc で始まるすべてのヘッダーが除外され、他のすべてのヘッダーが含まれます。

デフォルトでは、Jackson がクラスパス上にある限り、DefaultKafkaHeaderMapper は MessagingMessageConverter および BatchMessagingMessageConverter で使用されます。

バッチコンバーターを使用すると、変換されたヘッダーは KafkaHeaders.BATCH_CONVERTED_HEADERS で List<Map<String, Object>> として使用でき、リストの位置にあるマップはペイロードのデータ位置に対応します。

コンバーターがない場合 (Jackson が存在しないか、明示的に null に設定されているため)、コンシューマーレコードのヘッダーは KafkaHeaders.NATIVE_HEADERS ヘッダーに変換されずに提供されます。このヘッダーは Headers オブジェクト (バッチコンバーターの場合は List<Headers> ) であり、リスト内の位置はペイロード内のデータ位置に対応します)。

特定の型は JSON 直列化に適していないため、これらの型には単純な toString() 直列化が適している場合があります。DefaultKafkaHeaderMapper には addToStringClasses() というメソッドがあり、送信 マッピングでこの方法で処理する必要があるクラスの名前を指定できます。受信・マッピング中に、それらは String としてマッピングされます。デフォルトでは、org.springframework.util.MimeType と org.springframework.http.MediaType のみがこの方法でマッピングされます。
バージョン 2.3 以降、文字列値ヘッダーの処理が簡素化されています。このようなヘッダーは、デフォルトでは JSON エンコードされなくなりました (つまり、エンクロージング "…​" が追加されていません)。型は引き続き JSON_TYPES ヘッダーに追加されるため、受信側システムは ( byte[] から) String に変換し直すことができます。マッパーは、古いバージョンで生成されたヘッダーを処理 (デコード) できます (先頭の " をチェックします)。このようにして、2.3 を使用するアプリケーションは、古いバージョンのレコードを使用できます。
2.3 を使用するバージョンによって生成されたレコードが、以前のバージョンを使用するアプリケーションによって使用される可能性がある場合、以前のバージョンとの互換性を維持するには、encodeStrings を true に設定します。すべてのアプリケーションが 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 を自動構成された KafkaTemplate に自動構成します。それ以外の場合は、このコンバーターをテンプレートに追加する必要があります。

4.1.21. NULL ペイロードと "Tombstone" レコードのログ圧縮

ログ圧縮 [Apache] (英語) を使用すると、キーの削除を識別するために null ペイロードを含むメッセージを送受信できます。

値をデシリアライズできない場合に null を返す可能性のある Deserializer など、他の理由で null 値を受け取ることもできます。

KafkaTemplate を使用して null ペイロードを送信するには、send() メソッドの value 引数に null を渡すことができます。これに対する 1 つの例外は、send(Message<?> message) バリアントです。spring-messaging Message<?> は null ペイロードを持つことができないため、KafkaNull と呼ばれる特別なペイロード型を使用でき、フレームワークは null を送信します。便宜上、静的 KafkaNull.INSTANCE が提供されます。

メッセージリスナーコンテナーを使用する場合、受信した ConsumerRecord には null value() があります。

null ペイロードを処理するように @KafkaListener を構成するには、required = false とともに @Payload アノテーションを使用する必要があります。圧縮されたログの tombstone メッセージの場合は、通常、アプリケーションがどのキーが「削除」されたかを判断できるようにキーも必要になります。次の例は、そのような構成を示しています。

@KafkaListener(id = "deletableListener", topics = "myTopic")
public void listen(@Payload(required = false) String value, @Header(KafkaHeaders.RECEIVED_KEY) String key) {
    // value == null represents key deletion
}

複数の @KafkaHandler メソッドでクラスレベルの @KafkaListener を使用する場合は、追加の構成が必要です。具体的には、KafkaNull ペイロードを持つ @KafkaHandler メソッドが必要です。次の例は、1 つの設定方法を示しています。

@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_KEY) int key) {
        ...
    }

}

引数は KafkaNull ではなく null であることに注意してください。

この機能では、デフォルトの MessageHandlerMethodFactory を使用するときにフレームワークが構成する KafkaNullAwarePayloadArgumentResolver を使用する必要があります。カスタム MessageHandlerMethodFactory を使用する場合は、カスタム HandlerMethodArgumentResolver を @KafkaListener に追加するを参照してください。

4.1.22. 例外の処理

このセクションでは、Spring for Apache Kafka の使用時に発生する可能性のあるさまざまな例外の処理方法について説明します。

リスナーエラーハンドラー

バージョン 2.0 以降、@KafkaListener アノテーションには新しい属性 errorHandler があります。

errorHandler を使用して、KafkaListenerErrorHandler 実装の Bean 名を提供できます。次のように、この関数インターフェースには 1 つのメソッドがあります。

@FunctionalInterface
public interface KafkaListenerErrorHandler {

    Object handleError(Message<?> message, ListenerExecutionFailedException exception) throws Exception;

}

メッセージコンバーターによって生成された spring-messaging Message<?> オブジェクトと、ListenerExecutionFailedException にラップされたリスナーによってスローされた例外にアクセスできます。エラーハンドラーは、コンテナーにスローされる元の例外または新しい例外をスローできます。エラーハンドラーによって返されるものはすべて無視されます。

バージョン 2.7 以降、MessagingMessageConverter および BatchMessagingMessageConverter で rawRecordHeader プロパティを設定できます。これにより、生の ConsumerRecord が KafkaHeaders.RAW_DATA ヘッダーの変換された Message<?> に追加されます。これは、たとえば、リスナーエラーハンドラーで DeadLetterPublishingRecoverer を使用する場合に便利です。これは、デッドレタートピックで失敗したレコードをキャプチャーした後、何度か再試行した後、失敗結果を送信者に送信するリクエスト / 応答シナリオで使用される場合があります。

@Bean
KafkaListenerErrorHandler eh(DeadLetterPublishingRecoverer recoverer) {
    return (msg, ex) -> {
        if (msg.getHeaders().get(KafkaHeaders.DELIVERY_ATTEMPT, Integer.class) > 9) {
            recoverer.accept(msg.getHeaders().get(KafkaHeaders.RAW_DATA, ConsumerRecord.class), ex);
            return "FAILED";
        }
        throw ex;
    };
}

次のメソッドを介して、コンシューマーオブジェクトにアクセスできるサブインターフェース (ConsumerAwareListenerErrorHandler) があります。

Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer);

別のサブインターフェース (ManualAckListenerErrorHandler) は、手動 AckMode を使用するときに Acknowledgment オブジェクトへのアクセスを提供します。

Object handleError(Message<?> message, ListenerExecutionFailedException exception,
			Consumer<?, ?> consumer, @Nullable Acknowledgment ack);

どちらの場合でも、コンテナーはシークを認識しないため、コンシューマーに対してシークを実行しないでください。

コンテナーエラーハンドラー

バージョン 2.8 以降、従来の ErrorHandler および BatchErrorHandler インターフェースは新しい CommonErrorHandler に置き換えられました。これらのエラーハンドラーは、レコードリスナーとバッチリスナーの両方のエラーを処理できるため、単一のリスナーコンテナーファクトリで両方の種類のリスナーのコンテナーを作成できます。ほとんどのレガシーフレームワークエラーハンドラーの実装を置き換える CommonErrorHandler 実装が提供され、レガシーエラーハンドラーは非推奨になりました。従来のインターフェースは、リスナーコンテナーとリスナーコンテナーファクトリによって引き続きサポートされます。これらは将来のリリースで非推奨となりる予定です。

カスタムエラーハンドラーを CommonErrorHandler に移行する方法については、カスタムレガシーエラーハンドラーの実装を CommonErrorHandler に移行するを参照してください。

トランザクションが使用されている場合、デフォルトではエラーハンドラーは構成されていないため、例外によってトランザクションがロールバックされます。トランザクションコンテナーのエラー処理は AfterRollbackProcessor によって処理されます。トランザクションの使用時にカスタムエラーハンドラーを提供する場合、トランザクションをロールバックするには、例外をスローする必要があります。

このインターフェースには、コンテナーによって呼び出されるデフォルトのメソッド isAckAfterHandle() があり、エラーハンドラーが例外をスローせずに戻った場合に、オフセットをコミットする必要があるかどうかを判断します。デフォルトでは true を返します。

通常、フレームワークによって提供されるエラーハンドラーは、エラーが「処理」されていない場合(たとえば、シーク操作の実行後)に例外をスローします。デフォルトでは、このような例外は ERROR レベルでコンテナーによってログに記録されます。すべてのフレームワークエラーハンドラーは KafkaExceptionLogLevelAware を継承し、これらの例外がログに記録されるレベルを制御できるようにします。

/**
 * Set the level at which the exception thrown by this handler is logged.
 * @param logLevel the level (default ERROR).
 */
public void setLogLevel(KafkaException.Level logLevel) {
    ...
}

コンテナーファクトリのすべてのリスナーに使用されるグローバルエラーハンドラーを指定できます。次の例は、その方法を示しています。

@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
        kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    ...
    factory.setCommonErrorHandler(myErrorHandler);
    ...
    return factory;
}

デフォルトでは、アノテーション付きのリスナーメソッドが例外をスローすると、コンテナーにスローされ、メッセージはコンテナーの設定に従って処理されます。

コンテナーは、エラーハンドラーを呼び出す前に、保留中のオフセットコミットをすべてコミットします。

Spring Boot を使用している場合は、エラーハンドラーを @Bean として追加するだけで、Boot が自動構成されたファクトリに追加します。

バックオフハンドラー

DefaultErrorHandler などのエラーハンドラーは、BackOff を使用して、配信を再試行するまでの待機時間を決定します。バージョン 2.9 以降、カスタム BackOffHandler を構成できます。デフォルトのハンドラーは、バックオフ時間が経過するまで (またはコンテナーが停止するまで) スレッドを中断するだけです。フレームワークは、バックオフ時間が経過するまでリスナーコンテナーを一時停止し、その後コンテナーを再開する ContainerPausingBackOffHandler も提供します。これは、遅延が max.poll.interval.ms コンシューマープロパティよりも長い場合に役立ちます。実際のバックオフ時間の解決は、pollTimeout コンテナープロパティの影響を受けることに注意してください。

DefaultErrorHandler

この新しいエラーハンドラーは、現在いくつかのリリースでデフォルトのエラーハンドラーとなっている SeekToCurrentErrorHandler および RecoveringBatchErrorHandler に代わるものです。1 つの違いは、バッチリスナーのフォールバック動作(BatchListenerFailedException 以外の例外がスローされた場合)が完全なバッチの再試行と同等であることです。

バージョン 2.9 以降、DefaultErrorHandler は、以下で説明する未処理のレコードオフセットをシークするのと同じセマンティクスを提供するように構成できますが、実際にはシークしません。代わりに、レコードはリスナーコンテナーによって保持され、エラーハンドラーが終了した後 (および一時停止された poll() を 1 回実行した後、コンシューマーを存続させるためにリスナーに再送信されます。ノンブロッキング再試行または ContainerPausingBackOffHandler が使用されている場合、一時停止は複数の世論調査)。エラーハンドラーは、現在失敗しているレコードを再送信できるかどうか、回復された後はリスナーに再度送信されないかどうかを示す結果をコンテナーに返します。このモードを有効にするには、プロパティ seekAfterError を false に設定します。

エラーハンドラーは、失敗し続けるレコードを回復(スキップ)できます。デフォルトでは、10 回の失敗の後、失敗したレコードがログに記録されます(ERROR レベルで)。カスタムリカバリ(BiConsumer)と、配信の試行とそれぞれの間の遅延を制御する BackOff を使用してハンドラーを構成できます。FixedBackOff.UNLIMITED_ATTEMPTS とともに FixedBackOff を使用すると、(事実上)無限の再試行が発生します。次の例では、3 回試行した後のリカバリを構成します。

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

レコードリスナーの場合、これにより、デフォルト構成(FixedBackOff(0L, 9))の代わりに、1 秒のバックオフで最大 2 回(3 回の配信試行)の配信が再試行されます。再試行が終了した後、失敗は単にログに記録されます。

例として ; poll が 6 つのレコード (各パーティション 0, 1, 2 から 2 つ) を返し、リスナーが 4 番目のレコードで例外をスローした場合、コンテナーはオフセットをコミットすることで最初の 3 つのメッセージを確認します。DefaultErrorHandler は、パーティション 1 に対してオフセット 1 をシークし、パーティション 2 に対してオフセット 0 をシークします。次の poll() は、3 つの未処理レコードを返します。

AckMode が BATCH の場合、コンテナーはエラーハンドラーを呼び出す前に、最初の 2 つのパーティションのオフセットをコミットします。

バッチリスナーの場合、リスナーは、バッチ内のどのレコードが失敗したかを示す BatchListenerFailedException をスローする必要があります。

イベントのシーケンスは次のとおりです。

  • インデックスの前にレコードのオフセットをコミットします。

  • 再試行を繰り返しても実行されない場合は、シークを実行して、残りのすべてのレコード (失敗したレコードを含む) が再配信されるようにします。

  • 再試行回数に達した場合は、失敗したレコード (デフォルトのログのみ) の回復を試みてシークを実行し、残りのレコード (失敗したレコードを除く) が再配信されるようにします。回復されたレコードのオフセットがコミットされます

  • 再試行回数を超えて回復に失敗した場合、再試行回数に達していないかのようにシークが実行されます。

バージョン 2.9 から、DefaultErrorHandler は、上記で説明した未処理のレコードオフセットをシークするのと同じセマンティクスを提供するように構成できますが、実際にはシークしません。代わりに、エラーハンドラーは、未処理のレコードのみを含む新しい ConsumerRecords<?, ?> を作成します。これは、リスナーに送信されます (一時停止された poll() を 1 回実行した後、コンシューマーを存続させます)。このモードを有効にするには、プロパティ seekAfterError を false に設定します。

デフォルトの回復者は、再試行回数が尽きた後、失敗したレコードをログに記録します。カスタムリカバリツール、または 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

これらの例外は、再試行された配信では解決されそうにないためです。

再試行不可能なカテゴリに例外型を追加するか、分類された例外のマップを完全に置き換えることができます。詳細については、DefaultErrorHandler.addNotRetryableException() および DefaultErrorHandler.setClassifications() の Javadoc、および spring-retry BinaryExceptionClassifier の Javadoc を参照してください。

再試行できない例外に IllegalArgumentException を追加する例を次に示します。

@Bean
public DefaultErrorHandler errorHandler(ConsumerRecordRecoverer recoverer) {
    DefaultErrorHandler handler = new DefaultErrorHandler(recoverer);
    handler.addNotRetryableExceptions(IllegalArgumentException.class);
    return handler;
}

エラーハンドラーは、1 つ以上の RetryListener で構成でき、再試行と回復の進行状況の通知を受け取ります。バージョン 2.8.10 以降、バッチリスナーのメソッドが追加されました。

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

	default void failedDelivery(ConsumerRecords<?, ?> records, Exception ex, int deliveryAttempt) {
	}

	default void recovered(ConsumerRecords<?, ?> records, Exception ex) {
	}

	default void recoveryFailed(ConsumerRecords<?, ?> records, Exception original, Exception failure) {
	}

}

詳細については、javadoc を参照してください。

リカバリが失敗した場合(例外をスローした場合)、失敗したレコードはシークに含まれます。リカバリが失敗した場合、BackOff はデフォルトでリセットされ、リカバリが再試行される前に再配信が再度バックオフされます。リカバリが失敗した後の再試行をスキップするには、エラーハンドラーの resetStateOnRecoveryFailure を false に設定します。

エラーハンドラーに BiFunction<ConsumerRecord<?, ?>, Exception, BackOff> を提供して、失敗したレコードや例外に基づいて、使用する BackOff を決定できます。

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

関数が null を返す場合、ハンドラーのデフォルトの BackOff が使用されます。

resetStateOnExceptionChange を true に設定すると、再試行シーケンスが再開されます (そのように構成されている場合は、新しい BackOff の選択を含む) 失敗の間で例外型が変更された場合。false (バージョン 2.9 より前のデフォルト) の場合、例外型は考慮されません。

バージョン 2.9 から、これはデフォルトで true になりました。

配信試行ヘッダーも参照してください。

バッチエラーハンドラーによる変換エラー

バージョン 2.8 以降、バッチリスナーは ByteArrayDeserializerBytesDeserializerStringDeserializerDefaultErrorHandler とともに MessageConverter を使用する場合に、変換エラーを適切に処理できるようになりました。変換エラーが発生すると、ペイロードは null に設定され、ErrorHandlingDeserializer と同様に、逆直列化例外がレコードヘッダーに追加されます。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);
    }
}
完全なバッチの再試行

これは、リスナーが BatchListenerFailedException 以外の例外をスローするバッチリスナーの DefaultErrorHandler のフォールバック動作になりました。

バッチが再配信されるときに、バッチに同じ数のレコードがある、および / または再配信されたレコードが同じ順序であるという保証はありません。バッチの再試行状態を簡単に維持することは不可能です。FallbackBatchErrorHandler は次のアプローチを取ります。バッチリスナーが BatchListenerFailedException ではない例外をスローした場合、再試行はメモリ内のレコードのバッチから実行されます。延長された再試行シーケンス中のリバランスを回避するために、エラーハンドラーはコンシューマーを一時停止し、スリープする前に再試行ごとにポーリングして、リスナーを再度呼び出します。再試行が終了すると、バッチ内のレコードごとに ConsumerRecordRecoverer が呼び出されます。リカバリが例外をスローした場合、またはスレッドがスリープ中に中断された場合、レコードのバッチは次のポーリングで再配信されます。終了する前に、結果に関係なく、コンシューマーは再開されます。

このメカニズムはトランザクションでは使用できません。

BackOff 間隔を待機している間、エラーハンドラーは、コンテナーが停止しているかどうかを確認しながら、目的の遅延に達するまで短いスリープでループし、遅延を発生させるのではなく、stop() の直後にスリープを終了できるようにします。

コンテナー停止エラーハンドラー

リスナーが例外をスローした場合、CommonContainerStoppingErrorHandler はコンテナーを停止します。レコードリスナーの場合、AckMode が RECORD のときは、すでに処理されたレコードのオフセットがコミットされます。レコードリスナーの場合、AckMode が手動の値のときは、すでに確認されたレコードのオフセットがコミットされます。レコードリスナーの場合、wWhen は AckMode が BATCH であり、バッチリスナーの場合は、コンテナーが再起動されるとバッチ全体が再生されます。

コンテナーが停止した後、ListenerExecutionFailedException をラップする例外がスローされます。これにより、トランザクションがロールバックされます (トランザクションが有効になっている場合)。

エラーハンドラーの委譲

CommonDelegatingErrorHandler は、例外型に応じて、さまざまなエラーハンドラーに委譲できます。例: ほとんどの例外に対して DefaultErrorHandler を呼び出したり、他の例外に対して CommonContainerStoppingErrorHandler を呼び出したりすることができます。

ロギングエラーハンドラー

CommonLoggingErrorHandler は単に例外をログに記録します。レコードリスナーを使用すると、前のポーリングの残りのレコードがリスナーに渡されます。バッチリスナーの場合、バッチ内のすべてのレコードがログに記録されます。

レコードリスナーとバッチリスナーに異なる一般的なエラーハンドラーを使用する

レコードリスナーとバッチリスナーに異なるエラー処理戦略を使用する場合は、CommonMixedErrorHandler が提供されており、リスナー型ごとに特定のエラーハンドラーを構成できます。

一般的なエラーハンドラー Summery
  • 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 に移行する

CommonErrorHandler の javadoc を参照してください。

ErrorHandler または ConsumerAwareErrorHandler の実装を置き換えるには、handleOne() を実装し、seeksAfterHandle() をそのままにして false (デフォルト)を返す必要があります。また、handleOtherException() を実装して、レコード処理の範囲外で発生する例外(コンシューマーエラーなど)を処理する必要があります。

RemainingRecordsErrorHandler 実装を置き換えるには、handleRemaining() を実装し、seeksAfterHandle() をオーバーライドして true を返す必要があります (エラーハンドラーは必要なシークを実行する必要があります)。また、handleOtherException() を実装して、レコード処理の範囲外で発生する例外 (コンシューマーエラーなど) を処理する必要があります。

BatchErrorHandler 実装を置き換えるには、handleBatch() も実装する必要があります。handleOtherException() も実装する必要があります。これにより、レコード処理の範囲外で発生する例外(コンシューマーエラーなど)を処理できます。

ロールバック後のプロセッサー

トランザクションを使用しているときに、リスナーが例外をスローした場合 (およびエラーハンドラーが存在する場合は例外をスローした場合)、トランザクションはロールバックされます。デフォルトでは、未処理のレコード (失敗したレコードを含む) は次のポーリングで再フェッチされます。これは、DefaultAfterRollbackProcessor で seek 操作を実行することで実現されます。バッチリスナーを使用すると、レコードのバッチ全体が再処理されます (コンテナーは、バッチ内のどのレコードが失敗したかを認識しません)。この動作を変更するには、カスタム AfterRollbackProcessor を使用してリスナーコンテナーを構成します。例: レコードベースのリスナーを使用している場合、失敗したレコードを追跡し、何回か試行した後、おそらくそれをデッドレタートピックに公開することであきらめたいと思うかもしれません。

バージョン 2.2 以降、DefaultAfterRollbackProcessor は失敗し続けるレコードを回復 (スキップ) できるようになりました。デフォルトでは、10 回失敗すると、失敗したレコードがログに記録されます (ERROR レベルで)。カスタムリカバリ (BiConsumer) と最大の障害を使用してプロセッサーを構成できます。maxFailures プロパティを負の数に設定すると、無限の再試行が発生します。次の例では、3 回試行した後の回復を構成します。

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 を使用して失敗したレコードを公開している場合、プロセッサーは元のトピック / パーティション内の回復されたレコードのオフセットをトランザクションに送信します。この機能を有効にするには、DefaultAfterRollbackProcessor で commitRecovered および kafkaTemplate プロパティを設定します。

リカバリが失敗した場合 (例外をスローした場合)、失敗したレコードがシークに含まれます。バージョン 2.5.5 以降、回復者が失敗した場合、BackOff はデフォルトでリセットされ、回復が再試行される前に再配信が再びバックオフを通過します。以前のバージョンでは、BackOff はリセットされず、次の障害で回復が再試行されました。以前の動作に戻すには、プロセッサーの resetStateOnRecoveryFailure プロパティを false に設定します。

バージョン 2.6 以降、失敗したレコードや例外に基づいて、使用する BackOff を決定する BiFunction<ConsumerRecord<?, ?>, Exception, BackOff> をプロセッサーに提供できるようになりました。

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

関数が null を返す場合、プロセッサーのデフォルトの BackOff が使用されます。

バージョン 2.6.3 以降、resetStateOnExceptionChange を true に設定すると、失敗の間に例外型が変更された場合に、再試行シーケンスが再開されます(構成されている場合は、新しい BackOff の選択を含む)。デフォルトでは、例外型は考慮されません。

バージョン 2.3.1 以降、DefaultErrorHandler と同様に、DefaultAfterRollbackProcessor は特定の例外を致命的とみなし、そのような例外の再試行はスキップされます。リカバリは最初の失敗時に呼び出されます。デフォルトで致命的と見なされる例外は次のとおりです。

  • DeserializationException

  • MessageConversionException

  • ConversionException

  • MethodArgumentResolutionException

  • NoSuchMethodException

  • ClassCastException

これらの例外は、再試行された配信では解決されそうにないためです。

再試行不可能なカテゴリにさらに例外の種類を追加するか、分類された例外のマップを完全に置き換えることができます。詳細については、DefaultAfterRollbackProcessor.setClassifications() の Javadoc および spring-retry BinaryExceptionClassifier の Javadoc を参照してください。

再試行できない例外に IllegalArgumentException を追加する例を次に示します。

@Bean
public DefaultAfterRollbackProcessor errorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
    DefaultAfterRollbackProcessor processor = new DefaultAfterRollbackProcessor(recoverer);
    processor.addNotRetryableException(IllegalArgumentException.class);
    return processor;
}

配信試行ヘッダーも参照してください。

現在の kafka-clients では、コンテナーは ProducerFencedException がリバランスによって引き起こされたのか、プロデューサーの transactional.id がタイムアウトまたは期限切れのために取り消されたのかを検出できません。ほとんどの場合、これはリバランスによって引き起こされるため、コンテナーは AfterRollbackProcessor を呼び出しません (パーティションが割り当てられなくなったため、パーティションをシークするのは適切ではないためです)。タイムアウトが各トランザクションを処理し、定期的に (たとえば ListenerContainerIdleEvent を介して) 「空の」トランザクションを実行するのに十分な大きさであることを確認すると、タイムアウトと期限切れによるフェンシングを回避できます。または、stopContainerWhenFenced コンテナープロパティを true に設定すると、コンテナーが停止し、レコードの損失を回避できます。ConsumerStoppedEvent を使用し、FENCED の Reason プロパティをチェックして、この状態を検出できます。イベントにはコンテナーへの参照も含まれているため、このイベントを使用してコンテナーを再起動できます。

バージョン 2.7 以降、BackOff 間隔を待機している間、エラーハンドラーは、コンテナーが停止したかどうかを確認しながら、必要な遅延に達するまで短いスリープでループし、stop() ではなく、stop() の直後にスリープを終了できるようにします。遅延を引き起こしています。

バージョン 2.7 以降、プロセッサーは 1 つ以上の RetryListener で構成でき、再試行および回復の進行状況の通知を受け取ります。

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

}

詳細については、javadoc を参照してください。

配信試行ヘッダー

以下は、バッチリスナーではなく、レコードリスナーにのみ適用されます。

バージョン 2.5 以降、DeliveryAttemptAware を実装する ErrorHandler または AfterRollbackProcessor を使用する場合、レコードへの KafkaHeaders.DELIVERY_ATTEMPT ヘッダー (kafka_deliveryAttempt) の追加を有効にすることができます。このヘッダーの値は、1 から始まる増分整数です。生の ConsumerRecord<?, ?> を受信する場合、整数は byte[4] にあります。

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

@KafkaListener を DefaultKafkaHeaderMapper または SimpleKafkaHeaderMapper とともに使用する場合、リスナーメソッドに @Header(KafkaHeaders.DELIVERY_ATTEMPT) int delivery をパラメーターとして追加することで取得できます。

このヘッダーの設定を有効にするには、コンテナープロパティ deliveryAttemptHeader を true に設定します。各レコードの状態を調べてヘッダーを追加する (小さな) オーバーヘッドを回避するために、デフォルトでは無効になっています。

DefaultErrorHandler および DefaultAfterRollbackProcessor は、この機能をサポートしています。

リスナー情報ヘッダー

場合によっては、リスナーが実行されているコンテナーを知ることができると便利です。

バージョン 2.8.4 以降、リスナーコンテナーで listenerInfo プロパティを設定したり、@KafkaListener アノテーションで info 属性を設定したりできるようになりました。次に、コンテナーはこれを 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) {
...
}

RecordInterceptor または RecordFilterStrategy 実装で使用される場合、ヘッダーは、KafkaListenerAnnotationBeanPostProcessor の charSet プロパティを使用して変換されたバイト配列としてコンシューマーレコードにあります。

ヘッダーマッパーは、コンシューマーレコードから MessageHeaders を作成するときにも String に変換し、このヘッダーを送信レコードにマップすることはありません。

バージョン 2.8.6 以降の POJO バッチリスナーの場合、ヘッダーはバッチの各メンバーにコピーされ、変換後に単一の String パラメーターとしても使用できます。

@KafkaListener(id = "list2", topics = "someTopic", containerFactory = "batchFactory",
        info = "info for batch")
public void listen(List<Thing> list,
        @Header(KafkaHeaders.RECEIVED_KEY) List<Integer> keys,
        @Header(KafkaHeaders.RECEIVED_PARTITION) 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>> を受け取った場合、情報は各 Message<?> の KafkaHeaders.LISTENER_INFO ヘッダーにあります。

バッチの消費の詳細については、バッチリスナーを参照してください。

デッドレターレコードの公開

レコードの障害の最大数に達したときに、レコードリカバリ装置を使用して DefaultErrorHandler および 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: 例外クラス名(通常は ListenerExecutionFailedException ですが、他の名前にすることもできます)。

  • KafkaHeaders.DLT_EXCEPTION_CAUSE_FQCN: 例外の原因となるクラス名(存在する場合)(バージョン 2.8 以降)。

  • KafkaHeaders.DLT_EXCEPTION_STACKTRACE: 例外スタックトレース。

  • KafkaHeaders.DLT_EXCEPTION_MESSAGE: 例外メッセージ。

  • KafkaHeaders.DLT_KEY_EXCEPTION_FQCN: 例外クラス名 (キーの逆直列化エラーのみ)。

  • KafkaHeaders.DLT_KEY_EXCEPTION_STACKTRACE: 例外スタックトレース (キーの逆直列化エラーのみ)。

  • KafkaHeaders.DLT_KEY_EXCEPTION_MESSAGE: 例外メッセージ (キーの逆直列化エラーのみ)。

  • KafkaHeaders.DLT_ORIGINAL_TOPIC: 元ネタ。

  • KafkaHeaders.DLT_ORIGINAL_PARTITION: 元のパーティション。

  • KafkaHeaders.DLT_ORIGINAL_OFFSET: 元のオフセット。

  • KafkaHeaders.DLT_ORIGINAL_TIMESTAMP: 元のタイムスタンプ。

  • KafkaHeaders.DLT_ORIGINAL_TIMESTAMP_TYPE: 元のタイムスタンプ型。

  • KafkaHeaders.DLT_ORIGINAL_CONSUMER_GROUP: レコードの処理に失敗した元のコンシューマーグループ(バージョン 2.8 以降)。

重要な例外は DeserializationException によってのみ発生するため、DLT_KEY_EXCEPTION_CAUSE_FQCN はありません。

さらにヘッダーを追加するには、2 つのメカニズムがあります。

  1. 回復者をサブクラス化して createProducerRecord() をオーバーライドします - super.createProducerRecord() を呼び出して、さらにヘッダーを追加します。

  2. BiFunction を提供して、コンシューマーレコードと例外を受け取り、Headers オブジェクトを返します。そこからのヘッダーは、最終的なプロデューサーレコードにコピーされます。デッドレターレコードヘッダーの管理も参照してください。setHeadersFunction() を使用して BiFunction を設定します。

2 番目の方法は実装が簡単ですが、最初の方法には、すでに組み立てられている標準ヘッダーなど、より多くの情報が含まれています。

バージョン 2.3 以降、ErrorHandlingDeserializer と組み合わせて使用すると、発行者は、デッドレタープロデューサーレコード内のレコード value() を、逆直列化に失敗した元の値に復元します。以前は、value() は null であり、ユーザーコードはメッセージヘッダーから DeserializationException をデコードする必要がありました。さらに、パブリッシャーに複数の KafkaTemplate を提供できます。これは、たとえば、DeserializationException から byte[] を公開する場合や、正常に逆直列化されたレコードから別のシリアライザーを使用する値を公開する場合に必要になる場合があります。String および byte[] シリアライザーを使用する KafkaTemplate でパブリッシャーを構成する例を次に示します。

@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() に適したテンプレートを見つけます。キーが順番に検査されるように、LinkedHashMap が推奨されます。

null 値を公開するときに、複数のテンプレートがある場合、回復者は Void クラスのテンプレートを探します。存在しない場合は、values().iterator() の最初のテンプレートが使用されます。

2.7 以降は、setFailIfSendResultIsError メソッドを使用して、メッセージの公開が失敗したときに例外がスローされるようにすることができます。setWaitForSendResultTimeout を使用した送信者の成功の検証にタイムアウトを設定することもできます。

リカバリが失敗した場合 (例外をスローした場合)、失敗したレコードがシークに含まれます。バージョン 2.5.5 以降、回復者が失敗した場合、BackOff はデフォルトでリセットされ、回復が再試行される前に再配信が再びバックオフを通過します。以前のバージョンでは、BackOff はリセットされず、次の障害で回復が再試行されました。以前の動作に戻すには、エラーハンドラーの resetStateOnRecoveryFailure プロパティを false に設定します。

バージョン 2.6.3 以降、resetStateOnExceptionChange を true に設定すると、失敗の間に例外型が変更された場合に、再試行シーケンスが再開されます(構成されている場合は、新しい BackOff の選択を含む)。デフォルトでは、例外型は考慮されません。

バージョン 2.3 以降、recoverer は Kafka ストリームでも使用できます - 詳細については、デシリアライズ例外からの回復を参照してください。

ErrorHandlingDeserializer は、ヘッダー ErrorHandlingDeserializer.VALUE_DESERIALIZER_EXCEPTION_HEADER および ErrorHandlingDeserializer.KEY_DESERIALIZER_EXCEPTION_HEADER (java 直列化を使用) に逆直列化例外を追加します。デフォルトでは、これらのヘッダーは配信不能トピックに公開されたメッセージに保持されません。バージョン 2.7 以降、キーと値の両方が逆直列化に失敗した場合、両方の元の値が DLT に送信されるレコードに入力されます。

受信レコードが互いに依存しているが、順不同で到着する可能性がある場合は、失敗したレコードをデッドレタートピックに直接送信するのではなく、元のトピックの末尾に (数回) 再発行すると便利な場合があります。例については、このスタックオーバーフローの質問 (英語) を参照してください。

次のエラーハンドラー構成は、まさにそれを行います。

@Bean
public ErrorHandler eh(KafkaOperations<String, String> template) {
    return new DefaultErrorHandler(new DeadLetterPublishingRecoverer(template,
            (rec, ex) -> {
                org.apache.kafka.common.header.Header retries = rec.headers().lastHeader("retries");
                if (retries == null) {
                    retries = new RecordHeader("retries", new byte[] { 1 });
                    rec.headers().add(retries);
                }
                else {
                    retries.value()[0]++;
                }
                return retries.value()[0] > 5
                        ? new TopicPartition("topic.DLT", rec.partition())
                        : new TopicPartition("topic", rec.partition());
            }), new FixedBackOff(0L, 0L));
}

バージョン 2.7 以降、回復者は、宛先リゾルバーによって選択されたパーティションが実際に存在することを確認します。パーティションが存在しない場合、ProducerRecord 内のパーティションは null に設定され、KafkaProducer がパーティションを選択できるようになります。このチェックを無効にするには、verifyPartition プロパティを false に設定します。

デッドレターレコードヘッダーの管理

上記のデッドレターレコードの公開を参照すると、DeadLetterPublishingRecoverer には、ヘッダーがすでに存在する場合(ノンブロッキング再試行の使用を含む、失敗したデッドレターレコードの再処理時など)にヘッダーを管理するために使用される 2 つのプロパティがあります。

  • appendOriginalHeaders (デフォルト true)

  • stripPreviousExceptionHeaders (バージョン 2.8 以降のデフォルト true )

Apache Kafka は同じ名前を持つ複数のヘッダーをサポートします。「最新」の値を取得するには、headers.lastHeader(headerName) を使用できます。複数のヘッダーの反復子を取得するには、headers.headers(headerName).iterator() を使用します。

失敗したレコードを繰り返し再発行すると、これらのヘッダーが大きくなる可能性があります(最終的には、RecordTooLargeException が原因で発行が失敗します)。これは、例外ヘッダー、特にスタックトレースヘッダーに特に当てはまります。

2 つのプロパティの理由は、最後の例外情報のみを保持したい場合でも、失敗ごとにレコードが通過したトピックの履歴を保持したい場合があるためです。

appendOriginalHeaders は ORIGINAL という名前のすべてのヘッダーに適用され、stripPreviousExceptionHeaders は EXCEPTION という名前のすべてのヘッダーに適用されます。

バージョン 2.8.4 以降、出力レコードに追加する標準ヘッダーを制御できるようになりました。デフォルトで追加される(現在)10 個の標準ヘッダーの総称名については enum HeadersToAdd を参照してください(これらは実際のヘッダー名ではなく、単なる抽象化です。実際のヘッダー名は、サブクラスがオーバーライドできる 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 メソッドを介して複数のヘッダー機能を提供できるようになりました。これにより、たとえばノンブロッキング再試行を使用している場合など、別の機能がすでに登録されている場合でも、追加の機能を適用できます。

ExponentialBackOffWithMaxRetries の実装

Spring Framework は、多数の BackOff 実装を提供します。デフォルトでは、ExponentialBackOff は無期限に再試行します。何度か再試行した後にあきらめるには、maxElapsedTime を計算する必要があります。バージョン 2.7.3 以降、Spring for Apache Kafka は、maxRetries プロパティを受け取り、maxElapsedTime を自動的に計算するサブクラスである ExponentialBackOffWithMaxRetries を提供します。これは、もう少し便利です。

@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.23. JAAS と Kerberos

バージョン 2.0 から、Kerberos 構成を支援するために KafkaJaasLoginModuleInitializer クラスが追加されました。この 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", "[email protected] (英語)  ");
    jaasConfig.setOptions(options);
    return jaasConfig;
}

4.2. ノンブロッキング再試行

バージョン 2.9 では、メカニズムがインフラストラクチャ Bean をブートストラップするように変更されました。この機能をブートストラップするために現在必要とされている 2 つのメカニズムについては、構成を参照してください。

Kafka でノンブロッキング再試行 /dlt 機能を実現するには、通常、追加のトピックを設定し、対応するリスナーを作成および構成する必要があります。2.7 以降、Spring for Apache Kafka は @RetryableTopic アノテーションと RetryTopicConfiguration クラスを介してそのサポートを提供し、ブートストラップを簡素化します。

4.2.1. パターンのしくみ

メッセージ処理が失敗した場合、メッセージはバックオフタイムスタンプで再試行トピックに転送されます。次に、再試行トピックコンシューマーはタイムスタンプをチェックし、期限が到来していない場合は、そのトピックのパーティションの消費を一時停止します。期限が来ると、パーティションの消費が再開され、メッセージが再び消費されます。メッセージ処理が再び失敗した場合、メッセージは次の再試行トピックに転送され、処理が成功するか、試行回数が尽きるまでパターンが繰り返され、メッセージはデッドレタートピック (構成されている場合) に送信されます。

説明のために、「メイントピック」トピックがあり、1000 ミリ秒の指数バックオフで 2 および 4 回の最大試行の乗数でノンブロッキングリトライを設定する場合、メイントピック - リトライ -1000 が作成されます。main-topic-retry-2000、main-topic-retry-4000、main-topic-dlt トピックを作成し、それぞれのコンシューマーを構成します。フレームワークは、トピックの作成と、リスナーの設定と構成も行います。

この戦略を使用すると、そのトピックに対する Kafka の順序の保証が失われます。
好みの AckMode モードを設定できますが、RECORD が推奨されます。
現時点では、この機能はクラスレベルの @KafkaListener アノテーションをサポートしていません。

4.2.2. バックオフ遅延精度

概要と保証

すべてのメッセージ処理とバックオフはコンシューマースレッドによって処理されるため、遅延の精度はベストエフォートベースで保証されます。1 つのメッセージの処理に、そのコンシューマーの次のメッセージのバックオフ期間よりも長い時間がかかる場合、次のメッセージの遅延は予想よりも長くなります。また、遅延が短い場合 (約 1 秒以下)、オフセットのコミットなど、スレッドが行う必要があるメンテナンス作業によって、メッセージ処理の実行が遅れる場合があります。再試行トピックのコンシューマーが複数のパーティションを処理している場合も、精度が影響を受ける可能性があります。これは、タイミング調整を行うために、ポーリングからコンシューマーを起動し、完全な pollTimeouts を持つことに依存しているためです。

そうは言っても、単一のパーティションを処理するコンシューマーの場合、メッセージの処理は、ほとんどの状況でほぼ正確な時間に発生するはずです。

メッセージは期限までに処理されないことが保証されています。

4.2.3. 構成

バージョン 2.9 以降、デフォルト設定では、@EnableKafkaRetryTopic アノテーションを @Configuration アノテーション付きクラスで使用する必要があります。これにより、機能が適切にブートストラップできるようになり、機能のコンポーネントの一部を注入して実行時に検索できるようになります。

@EnableKafkaRetryTopic は @EnableKafka でメタアノテーションが付けられているため、このアノテーションを追加する場合、@EnableKafka も追加する必要はありません。

また、そのバージョン以降、機能のコンポーネントとグローバル機能のより高度な構成のために、RetryTopicConfigurationSupport クラスを @Configuration クラスで拡張し、適切なメソッドをオーバーライドする必要があります。詳細については、グローバル設定と機能の構成を参照してください。

既定では、再試行トピックのコンテナーは、メインコンテナーと同じ同時実行性を持ちます。バージョン 3.0 以降では、再試行コンテナーに別の concurrency を設定できます (アノテーションまたは RetryConfigurationBuilder で)。

上記の手法の 1 つだけを使用でき、RetryTopicConfigurationSupport を継承できる @Configuration クラスは 1 つだけです。
@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
}

同じクラス内のメソッドを指定して、@DltHandler アノテーションでアノテーションを付けることにより、dlt メッセージを処理できます。DltHandler メソッドが指定されていない場合、消費のみを記録するデフォルトのコンシューマーが作成されます。

@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}
kafkaTemplate 名を指定しない場合は、defaultRetryTopicKafkaTemplate という名前の Bean が検索されます。Bean が見つからない場合は、例外がスローされます。

バージョン 3.0 以降では、@RetryableTopic アノテーションをカスタムアノテーションのメタアノテーションとして使用できます。例:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RetryableTopic
static @interface MetaAnnotatedRetryableTopic {

    @AliasFor(attribute = "concurrency", annotation = RetryableTopic.class)
    String parallelism() default "3";

}
RetryTopicConfiguration Bean の使用

@Configuration アノテーション付きクラスで RetryTopicConfiguration Bean を作成することにより、ノンブロッキング再試行サポートを構成することもできます。

@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)
            .concurrency(1)
            .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 のコンシューマーは、トピックのサフィックスを持つ @KafkaListener アノテーションの groupId パラメーターで指定したものとの組み合わせであるグループ ID を持つコンシューマーグループに割り当てられます。何も提供しない場合、すべて同じグループに属し、再試行トピックで再調整すると、メイントピックで不要な再調整が発生します。
コンシューマーが ErrorHandlingDeserializer で構成されている場合、非シリアライズ例外を処理するには、KafkaTemplate とそのプロデューサーを、通常のオブジェクトと、逆直列化例外の結果である生の byte[] 値を処理できるシリアライザーで構成することが重要です。テンプレートのジェネリクス値型は Object である必要があります。1 つの手法は、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 アノテーションは、手動のパーティション割り当ての有無にかかわらず、ノンブロッキング再試行とともに同じトピックに使用できますが、特定のトピックに使用される構成は 1 つだけです。このようなトピックの構成には、単一の RetryTopicConfiguration Bean を使用するのが最適です。同じトピックに複数の @RetryableTopic アノテーションが使用されている場合、すべて同じ値である必要があります。そうでない場合、それらの 1 つがそのトピックのすべてのリスナーに適用され、他のアノテーションの値は無視されます。
グローバル設定と機能の構成

2.9 以降、コンポーネントを構成するための以前の Bean オーバーライドアプローチは削除されました (前述の API の実験的な性質のため、非推奨ではありません)。これは、RetryTopicConfiguration Bean のアプローチを変更するものではなく、インフラストラクチャコンポーネントの構成のみを変更します。ここで、RetryTopicConfigurationSupport クラスを (単一の) @Configuration クラスに拡張し、適切なメソッドをオーバーライドする必要があります。以下に例を示します。

@EnableKafka
@Configuration
public class MyRetryTopicConfiguration extends RetryTopicConfigurationSupport {

    @Override
    protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
        blockingRetries
                .retryOn(MyBlockingRetriesException.class, MyOtherBlockingRetriesException.class)
                .backOff(new FixedBackOff(3000, 3));
    }

    @Override
    protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
        nonBlockingFatalExceptions.add(MyNonBlockingException.class);
    }

    @Override
    protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
        // Use the new 2.9 mechanism to avoid re-fetching the same records after a pause
        customizersConfigurer.customizeErrorHandler(eh -> {
            eh.setSeekAfterError(false);
        });
    }

}
この構成アプローチを使用する場合、Bean の重複によるコンテキストの開始の失敗を防ぐために @EnableKafkaRetryTopic アノテーションを使用しないでください。代わりに単純な @EnableKafka アノテーションを使用してください。

autoCreateTopics が true の場合、メイントピックとリトライトピックは、指定された数のパーティションとレプリケーションファクターで作成されます。バージョン 3.0 以降、デフォルトのレプリケーションファクターは -1 であり、ブローカーのデフォルトを使用することを意味します。ブローカーのバージョンが 2.4 より前の場合は、明示的な値を設定する必要があります。特定のトピック (メイントピックや DLT など) のこれらの値を上書きするには、必要なプロパティを指定して NewTopic @Bean を追加するだけです。自動作成プロパティをオーバーライドします。

デフォルトでは、受信したレコードの元のパーティションを使用して、レコードが再試行トピックに発行されます。再試行トピックのパーティションがメイントピックよりも少ない場合は、フレームワークを適切に構成する必要があります。例を次に示します。
@EnableKafka
@Configuration
public class Config extends RetryTopicConfigurationSupport {

    @Override
    protected Consumer<DeadLetterPublishingRecovererFactory> configureDeadLetterPublishingContainerFactory() {
        return dlprf -> dlprf.setPartitionResolver((cr, nextTopic) -> null);
    }

    ...

}

関数のパラメーターは、コンシューマーレコードと次のトピックの名前です。特定のパーティション番号、または null を返して、KafkaProducer がパーティションを決定する必要があることを示すことができます。

デフォルトでは、レコードが再試行トピックを通過するときに、再試行ヘッダーのすべての値 (試行回数、タイムスタンプ) が保持されます。バージョン 2.9.6 以降、これらのヘッダーの最後の値のみを保持する場合は、上記の configureDeadLetterPublishingContainerFactory() メソッドを使用して、ファクトリの retainAllRetryHeaderValues プロパティを false に設定します。

4.2.4. プログラム構築

この機能は @KafkaListener で使用するように設計されています。ただし、何人かのユーザーは、ノンブロッキングの再試行をプログラムで構成する方法についての情報をリクエストしています。次の Spring Boot アプリケーションは、その方法の例を示しています。

@SpringBootApplication
public class Application extends RetryTopicConfigurationSupport {

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

    @Bean
    RetryTopicConfiguration retryConfig(KafkaTemplate<String, String> template) {
        return RetryTopicConfigurationBuilder.newInstance()
                .maxAttempts(4)
                .autoCreateTopicsWith(2, (short) 1)
                .create(template);
    }

    @Bean
    TaskScheduler scheduler() {
        return new ThreadPoolTaskScheduler();
    }

    @Bean
    @Order(0)
    SmartInitializingSingleton dynamicRetry(RetryTopicConfigurer configurer, RetryTopicConfiguration config,
            KafkaListenerAnnotationBeanPostProcessor<?, ?> bpp, KafkaListenerContainerFactory<?> factory,
            Listener listener, KafkaListenerEndpointRegistry registry) {

        return () -> {
            KafkaListenerEndpointRegistrar registrar = bpp.getEndpointRegistrar();
            MethodKafkaListenerEndpoint<String, String> mainEndpoint = new MethodKafkaListenerEndpoint<>();
            EndpointProcessor endpointProcessor = endpoint -> {
                // customize as needed (e.g. apply attributes to retry endpoints).
                if (!endpoint.equals(mainEndpoint)) {
                    endpoint.setConcurrency(1);
                }
                // these are required
                endpoint.setMessageHandlerMethodFactory(bpp.getMessageHandlerMethodFactory());
                endpoint.setTopics("topic");
                endpoint.setId("id");
                endpoint.setGroupId("group");
            };
            mainEndpoint.setBean(listener);
            try {
                mainEndpoint.setMethod(Listener.class.getDeclaredMethod("onMessage", ConsumerRecord.class));
            }
            catch (NoSuchMethodException | SecurityException ex) {
                throw new IllegalStateException(ex);
            }
            mainEndpoint.setConcurrency(2);
            mainEndpoint.setTopics("topic");
            mainEndpoint.setId("id");
            mainEndpoint.setGroupId("group");
            configurer.processMainAndRetryListeners(endpointProcessor, mainEndpoint, config, registrar, factory,
                    "kafkaListenerContainerFactory");
        };
    }


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

}

@Component
class Listener implements MessageListener<String, String> {

    @Override
    public void onMessage(ConsumerRecord<String, String> record) {
        System.out.println(KafkaUtils.format(record));
        throw new RuntimeException("test");
    }

}
トピックの自動作成は、上記の例のように、アプリケーションコンテキストがリフレッシュされる前に構成が処理される場合にのみ発生します。実行時にコンテナーを構成するには、他の手法を使用してトピックを作成する必要があります。

4.2.5. 機能

ほとんどの機能は、@RetryableTopic アノテーションと RetryTopicConfiguration Bean の両方で使用できます。

BackOff の設定

BackOff 構成は、Spring Retry プロジェクトの BackOffPolicy インターフェースに依存しています。

以下が含まれます:

  • 固定バックオフ

  • 指数バックオフ

  • ランダム指数バックオフ

  • 均一ランダムバックオフ

  • ノーバックオフ

  • カスタムバックオフ

@RetryableTopic(attempts = 5,
    backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .fixedBackoff(3000)
            .maxAttempts(4)
            .create(template);
}

Spring Retry の SleepingBackOffPolicy インターフェースのカスタム実装を提供することもできます。

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .customBackOff(new MyCustomBackOffPolicy())
            .maxAttempts(5)
            .create(template);
}
デフォルトのバックオフポリシーは FixedBackOffPolicy で、最大 3 回の試行と 1000 ミリ秒の間隔があります。
ExponentialBackOffPolicy のデフォルトの最大遅延は 30 秒です。バックオフポリシーでこれより大きい値の遅延が必要な場合は、それに応じて maxDelay プロパティを調整します。
最初の試行は maxAttempts にカウントされるため、maxAttempts 値を 4 に指定すると、元の試行に 3 回の再試行が加えられます。
グローバルタイムアウト

再試行プロセスのグローバルタイムアウトを設定できます。その時間に達した場合、次にコンシューマーが例外をスローしたときに、メッセージは 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)
            .create(template);
}
デフォルトではタイムアウトが設定されていません。これは、タイムアウト値として -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 を参照してください。RetryTopicConfigurationSupport を継承する @Configuration クラスで configureNonBlockingRetries メソッドをオーバーライドすることにより、このリストに例外を追加または削除できます。詳細については、グローバル設定と機能の構成を参照してください。

@Override
protected void manageNonBlockingRetriesFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
    nonBlockingFatalExceptions.add(MyNonBlockingException.class);
}
致命的な例外の分類を無効にするには、提供されたリストをクリアするだけです。
トピックを含めたり除外したりする

.includeTopic(String topic)、.includeTopics(Collection<String> Topics) .excludeTopic(String topic) および .excludeTopics(Collection<String> topic) メソッドを介して、RetryTopicConfiguration 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);
}
デフォルトの動作では、すべてのトピックが含まれます。
トピック AutoCreation

特に指定がない限り、フレームワークは、KafkaAdmin Bean によって消費される NewTopic Bean を使用して、必要なトピックを自動作成します。トピックが作成されるパーティションの数とレプリケーション係数を指定でき、この機能をオフにすることができます。バージョン 3.0 以降、デフォルトのレプリケーションファクターは -1 であり、ブローカーのデフォルトを使用することを意味します。ブローカーのバージョンが 2.4 より前の場合は、明示的な値を設定する必要があります。

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);
}
デフォルトでは、トピックは 1 つのパーティションと -1 のレプリケーション係数で自動作成されます (ブローカーのデフォルトを使用することを意味します)。ブローカーのバージョンが 2.4 より前の場合は、明示的な値を設定する必要があります。
障害ヘッダー管理

障害ヘッダー(元のヘッダーと例外ヘッダー)の管理方法を検討する場合、フレームワークは DeadLetterPublishingRecover に委譲して、ヘッダーを追加するか置き換えるかを決定します。

デフォルトでは、appendOriginalHeaders を false に明示的に設定し、stripPreviousExceptionHeaders を DeadLetterPublishingRecover で使用されるデフォルトのままにします。

これは、最初の「元の」ヘッダーと最後の例外ヘッダーのみがデフォルト構成で保持されることを意味します。これは、多くの再試行手順が含まれる場合に、(スタックトレースヘッダーなどが原因で)過度に大きなメッセージが作成されないようにするためです。

詳細については、デッドレターレコードヘッダーの管理を参照してください。

これらのプロパティに異なる設定を使用するようにフレームワークを再構成するには、RetryTopicConfigurationSupport を継承する @Configuration クラスで configureCustomizers メソッドをオーバーライドして、DeadLetterPublishingRecoverer カスタマイザーを構成します。詳細については、グローバル設定と機能の構成を参照してください。

@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
    customizersConfigurer.customizeDeadLetterPublishingRecoverer(dlpr -> {
        dlpr.setAppendOriginalHeaders(true);
        dlpr.setStripPreviousExceptionHeaders(false);
    });
}

バージョン 2.8.4 以降、カスタムヘッダーを追加する場合(ファクトリによって追加された再試行情報ヘッダーに加えて、ファクトリに headersFunction を追加できます。factory.setHeadersFunction((rec, ex) → { …​ })

デフォルトでは、追加されたヘッダーはすべて累積されます。Kafka ヘッダーには複数の値を含めることができます。バージョン 2.9.5 以降、関数によって返される Headers に型 DeadLetterPublishingRecoverer.SingleRecordHeader のヘッダーが含まれている場合、そのヘッダーの既存の値はすべて削除され、新しい単一の値のみが残ります。

4.2.6. ブロッキングとノンブロッキングの再試行の組み合わせ

2.8.4 以降では、ブロッキングとノンブロッキングの両方の再試行を組み合わせて使用するようにフレームワークを構成できます。例: DatabaseAccessException など、次のレコードでもエラーをトリガーする可能性のある一連の例外が発生する可能性があるため、同じレコードを再試行トピックに送信する前に、または DLT に直接送信する前に、同じレコードを数回再試行できます。

ブロック再試行を構成するには、RetryTopicConfigurationSupport を継承する @Configuration クラスで configureBlockingRetries メソッドをオーバーライドし、使用する BackOff と共に再試行する例外を追加します。デフォルトの BackOff は、遅延がなく 9 回試行される FixedBackOff です。詳細については、グローバル設定と機能の構成を参照してください。

@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
    blockingRetries
            .retryOn(MyBlockingRetryException.class, MyOtherBlockingRetryException.class)
            .backOff(new FixedBackOff(3000, 5));
}
グローバルな再試行可能なトピックの致命的な例外分類と組み合わせて、一部の例外でブロック再試行と非ブロック再試行の両方をトリガーしたり、いずれかの種類のみをトリガーしたり、直接アクセスしたりするなど、任意の動作に合わせてフレームワークを構成できます。いかなる種類の再試行も行わない DLT。

両方の構成が連携して機能する例を次に示します。

@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
    blockingRetries
            .retryOn(ShouldRetryOnlyBlockingException.class, ShouldRetryViaBothException.class)
            .backOff(new FixedBackOff(50, 3));
}

@Override
protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
    nonBlockingFatalExceptions.add(ShouldSkipBothRetriesException.class);
}

この例では:

  • ShouldRetryOnlyBlockingException.class はブロッキングを介してのみ再試行し、すべての再試行が失敗した場合、DLT に直接進みます。

  • ShouldRetryViaBothException.class はブロッキングを介して再試行し、すべてのブロッキング再試行が失敗した場合、別の一連の試行のために次の再試行トピックに転送されます。

  • ShouldSkipBothRetriesException.class は決して再試行されることはなく、最初の処理の試行が失敗した場合は DLT に直接移動します。

再試行のブロック動作は allowlist であることに注意してください。この方法で再試行する例外を追加します。ノンブロッキング再試行の分類は致命的な例外を対象としているため、拒否リストです。ノンブロッキング再試行を実行したくない例外を追加しますが、代わりに DLT に直接送信します。
ノンブロッキング例外分類の動作は、特定のトピックの構成によっても異なります。

4.2.7. トピックの命名

トピックの再試行と DLT は、メイントピックに提供された値またはデフォルト値をサフィックスとして付け、そのトピックの遅延またはインデックスを追加することで名前が付けられます。

例:

"my-topic" → "my-topic-retry-0"、"my-topic-retry-1"、…、"my-topic-dlt"

"my-other-topic" → "my-topic-myRetrySuffix-1000"、"my-topic-myRetrySuffix-2000"、…、"my-topic-myDltSuffix"。

デフォルトの動作では、試行ごとに個別の再試行トピックが作成され、インデックス値が追加されます: retry-0、retry-1、…、retry-n。デフォルトでは、再試行トピックの数は、構成された maxAttempts から 1 を引いた数になります。
トピックと 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);
    }
デフォルトの動作では、複数のトピックでの固定遅延構成を除き、遅延値を末尾に付けます。この場合、トピックの末尾にはトピックのインデックスが付きます。
固定遅延再試行の単一トピック

FixedBackOffPolicy や NoBackOffPolicy などの固定遅延ポリシーを使用している場合は、単一のトピックを使用してノンブロッキング再試行を実行できます。このトピックには、提供されたサフィックスまたはデフォルトのサフィックスがサフィックスとして付けられ、インデックスまたは遅延値はアペンドされません。

以前の FixedDelayStrategy は非推奨となり、SameIntervalTopicReuseStrategy に置き換えることができます。
@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()
            .create(template);
}
デフォルトの動作では、試行ごとに個別の再試行トピックが作成され、インデックス値が追加されます: retry-0、retry-1、…
maxInterval 指数遅延の単一トピック

指数バックオフポリシー (ExponentialBackOffPolicy) を使用している場合は、単一の再試行トピックを使用して、遅延が構成された maxInterval である試行の非ブロック再試行を実行できます。

この「最終」再試行トピックには、指定されたサフィックスまたはデフォルトのサフィックスが付加され、インデックスまたは maxInterval 値が付加されます。

maxInterval 遅延を使用した再試行に単一のトピックを使用することを選択することで、長時間再試行を続ける指数再試行ポリシーを構成することがより実行可能になる可能性があります。このアプローチでは大量のトピックが必要ないためです。

デフォルトの動作は、構成された maxAttempts から 1 を引いた数の再試行トピックで動作することです。指数バックオフを使用する場合、再試行トピックには遅延値が末尾に付けられ、最後の再試行トピック (maxInterval 遅延に対応) が末尾に付けられます。追加インデックス付き。

たとえば、initialInterval=1000multiplier=2maxInterval=16000 で指数バックオフを構成する場合、1 時間試行し続けるには、maxAttempts を 229 として構成する必要があり、デフォルトで必要な再試行トピックは次のようになります。

  • - 再試行 -1000

  • - 再試行 -2000

  • - 再試行 -4000

  • - 再試行 -8000

  • - 再試行 -16000-0

  • - 再試行 -16000-1

  • - 再試行 -16000-2

  • …​

  • - 再試行 -16000-224

同じ間隔で再試行トピックを再利用する戦略を使用する場合、上記の同じ構成で、必要な再試行トピックは次のようになります。

  • - 再試行 -1000

  • - 再試行 -2000

  • - 再試行 -4000

  • - 再試行 -8000

  • - 再試行 -16000

これは、将来のリリースではデフォルトになります。

@RetryableTopic(attempts = 230,
    backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 16000),
    sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.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()
            .exponentialBackoff(1000, 2, 16000)
            .maxAttempts(230)
            .useSingleTopicForSameIntervals()
            .create(template);
}
カスタム命名戦略

RetryTopicNamesProviderFactory を実装する Bean を登録することで、より複雑な命名戦略を実現できます。デフォルトの実装は SuffixingRetryTopicNamesProviderFactory であり、次の方法で別の実装を登録できます。

@Override
protected RetryTopicComponentFactory createComponentFactory() {
    return new RetryTopicComponentFactory() {
        @Override
        public RetryTopicNamesProviderFactory retryTopicNamesProviderFactory() {
            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.2.8. 複数のリスナー、同じトピック

バージョン 3.0 から、同じトピックで複数のリスナーを構成できるようになりました。これを行うには、カスタムトピック命名を使用して、再試行トピックを互いに分離する必要があります。これは、例で最もよく示されています。

@RetryableTopic(...
        retryTopicSuffix = "-listener1", dltTopicSuffix = "-listener1-dlt",
        topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(id = "listener1", groupId = "group1", topics = TWO_LISTENERS_TOPIC, ...)
void listen1(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic) {
    ...
}

@RetryableTopic(...
        retryTopicSuffix = "-listener2", dltTopicSuffix = "-listener2-dlt",
        topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(id = "listener2", groupId = "group2", topics = TWO_LISTENERS_TOPIC, ...)
void listen2(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic) {
    ...
}

topicSuffixingStrategy はオプションです。フレームワークは、リスナーごとに個別の再試行トピックのセットを構成して使用します。

4.2.9. Dlt 戦略

このフレームワークは、DLT を操作するためのいくつかの戦略を提供します。DLT 処理の方法を提供するか、デフォルトのログ記録方法を使用するか、DLT をまったく使用しないことができます。また、DLT 処理が失敗した場合の動作を選択することもできます。

Dlt 処理方法

トピックの DLT を処理するために使用する方法と、その処理が失敗した場合の動作を指定できます。

これを行うには、@RetryableTopic アノテーションを持つクラスのメソッドで @DltHandler アノテーションを使用できます。そのクラス内のすべての @RetryableTopic アノテーション付きメソッドで同じメソッドが使用されることに注意してください。

@RetryableTopic
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}

DLT ハンドラーメソッドは、RetryTopicConfigurationBuilder.dltHandlerMethod(String、String)メソッドを介して提供することもでき、DLT のメッセージを処理する必要がある Bean 名とメソッド名を引数として渡します。

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
            .create(template);
}

@Component
public class MyCustomDltProcessor {

    private final MyDependency myDependency;

    public MyCustomDltProcessor(MyDependency myDependency) {
        this.myDependency = myDependency;
    }

    public void processDltMessage(MyPojo message) {
       // ... message processing, persistence, etc
    }
}
DLT ハンドラーが提供されない場合、デフォルトの RetryTopicConfigurer.LoggingDltListenerHandlerMethod が使用されます。

バージョン 2.8 以降、デフォルトのハンドラーを含め、このアプリケーションで DLT から消費したくない場合(または消費を延期したい場合)、DLT コンテナーを開始するかどうかを制御できます。コンテナーファクトリの autoStartup プロパティ。

@RetryableTopic アノテーションを使用する場合は、autoStartDltHandler プロパティを false に設定します。構成ビルダーを使用する場合は、autoStartDltHandler(false) を使用してください。

後で KafkaListenerEndpointRegistry を介して DLT ハンドラーを開始できます。

DLT 障害動作

DLT 処理が失敗した場合、ALWAYS_RETRY_ON_ERROR と FAIL_ON_ERROR の 2 つの可能な動作が利用可能です。

前者では、レコードは 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()
            .dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
            .doNotRetryOnDltFailure()
            .create(template);
}
デフォルトの動作は ALWAYS_RETRY_ON_ERROR です。
バージョン 2.8.3 以降、DeserializationException などの致命的な例外がスローされる場合、通常、このような例外は常にスローされるため、ALWAYS_RETRY_ON_ERROR はレコードを DLT にルーティングしません。

致命的と見なされる例外は次のとおりです。

  • 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.2.10. ListenerContainerFactory の指定

デフォルトでは、RetryTopic 構成は @KafkaListener アノテーションから提供されたファクトリを使用しますが、再試行トピックおよび dlt リスナーコンテナーの作成に使用する別のファクトリを指定できます。

@RetryableTopic アノテーションの場合、ファクトリの Bean 名を指定でき、RetryTopicConfiguration Bean を使用して、Bean 名またはインスタンス自体を指定できます。

@RetryableTopic(listenerContainerFactory = "my-retry-topic-factory")
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template,
        ConcurrentKafkaListenerContainerFactory<Integer, MyPojo> factory) {

    return RetryTopicConfigurationBuilder
            .newInstance()
            .listenerFactory(factory)
            .create(template);
}

@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .listenerFactory("my-retry-topic-factory")
            .create(template);
}
2.8.3 以降、再試行可能なトピックと再試行不可能なトピックに同じファクトリを使用できます。

ファクトリ設定の動作を以前の 2.8.3 に戻す必要がある場合は、グローバル設定と機能の構成に従って RetryTopicConfigurationSupport を継承する @Configuration クラスの configureRetryTopicConfigurer メソッドをオーバーライドし、次のように useLegacyFactoryConfigurer を true に設定できます。

@Override
protected Consumer<RetryTopicConfigurer> configureRetryTopicConfigurer() {
    return rtc -> rtc.useLegacyFactoryConfigurer(true);
}

4.2.11. 実行時にトピックの情報にアクセスする

2.9 以降、提供された DestinationTopicContainer Bean を注入することにより、実行時にトピックチェーンに関する情報にアクセスできます。このインターフェースは、トピックの名前、遅延、型などの便利なプロパティだけでなく、トピックの チェーンまたは DLT (構成されている場合) で次のトピックを検索するメソッドを提供します。

実際のユースケースの例として、このような情報を使用して、コンソールアプリケーションが DLT から、失敗した処理の原因の後で チェーンの最初の再試行トピックにレコードを再送信できるようにすることができます。バグ / 矛盾した状態、解決されました。

DestinationTopicContainer#getNextDestinationTopicFor() メソッドで提供される DestinationTopic は、入力トピックの チェーンに登録された次のトピックに対応します。メッセージが転送される実際のトピックは、例外分類、試行回数、単一トピックの固定遅延戦略などのさまざまな要因により異なる場合があります。これらの要因を考慮する必要がある場合は、DestinationTopicResolver インターフェースを使用してください。

4.2.12. KafkaBackOffException ログレベルの変更

再試行トピックのメッセージが消費される予定がない場合、KafkaBackOffException がスローされます。このような例外は、デフォルトで DEBUG レベルでログに記録されますが、@Configuration クラスの ListenerContainerFactoryConfigurer でエラーハンドラーカスタマイザを設定することにより、この動作を変更できます。

例: ログレベルを WARN に変更するには、次を追加します。

@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
    customizersConfigurer.customizeErrorHandler(defaultErrorHandler ->
            defaultErrorHandler.setLogLevel(KafkaException.Level.WARN))
}

4.3. Apache Kafka ストリームのサポート

バージョン 1.1.4 以降、Spring for Apache Kafka は Kafka ストリーム [Apache] (英語) のファーストクラスのサポートを提供します。Spring アプリケーションから使用するには、kafka-streams jar がクラスパスに存在する必要があります。これは Spring for Apache Kafka プロジェクトのオプションの依存関係であり、推移的にダウンロードされることはありません。

4.3.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();

2 つの主要なコンポーネントがあります。

  • StreamsBuilderKStream (または KTable) インスタンスを構築するための API を使用します。

  • KafkaStreams: それらのインスタンスのライフサイクルを管理するため。

単一の StreamsBuilder によって KafkaStreams インスタンスに公開されるすべての KStream インスタンスは、ロジックが異なっていても、同時に開始および停止されます。つまり、StreamsBuilder によって定義されるすべてのストリームは、単一のライフサイクルコントロールに関連付けられます。KafkaStreams インスタンスが streams.close() によって閉じられると、再起動することはできません。代わりに、ストリーム処理を再開するための新しい KafkaStreams インスタンスを作成する必要があります。

4.3.2. Spring 管理

Spring アプリケーションコンテキストの観点から Kafka ストリームの使用を簡素化し、コンテナーを介したライフサイクル管理を使用するために、Spring for Apache Kafka には StreamsBuilderFactoryBean が導入されています。これは、StreamsBuilder シングルトンインスタンスを Bean として公開する AbstractFactoryBean 実装です。次の例では、そのような Bean を作成します。

@Bean
public FactoryBean<StreamsBuilder> myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) {
    return new StreamsBuilderFactoryBean(streamsConfig);
}
バージョン 2.2 以降、ストリーム設定は StreamsConfig ではなく KafkaStreamsConfiguration オブジェクトとして提供されるようになりました。

StreamsBuilderFactoryBean は、内部 KafkaStreams インスタンスのライフサイクルを管理するために SmartLifecycle も実装します。Kafka ストリーム API と同様に、KafkaStreams を開始する前に KStream インスタンスを定義する必要があります。これは、Kafka ストリームの Spring API にも当てはまります。StreamsBuilderFactoryBean でデフォルトの autoStartup = true を使用する場合、アプリケーションコンテキストがリフレッシュされる前に StreamsBuilder で KStream インスタンスを宣言する必要があります。例: KStream は通常の Bean 定義にすることができますが、Kafka ストリーム API は影響なしに使用されます。次の例は、その方法を示しています。

@Bean
public KStream<?, ?> kStream(StreamsBuilder kStreamBuilder) {
    KStream<Integer, String> stream = kStreamBuilder.stream(STREAMING_TOPIC1);
    // Fluent KStream API
    return stream;
}

ライフサイクルを手動で制御する場合 (たとえば、何らかの条件による停止と開始)、ファクトリ Bean (&) プレフィックスを使用して StreamsBuilderFactoryBean Bean を直接参照できます。StreamsBuilderFactoryBean は内部 KafkaStreams インスタンスを使用するため、停止して再起動しても安全です。各 start() に新しい KafkaStreams が作成されます。KStream インスタンスのライフサイクルを個別に制御したい場合は、別の StreamsBuilderFactoryBean インスタンスの使用を検討することもできます。

StreamsBuilderFactoryBean で KafkaStreams.StateListenerThread.UncaughtExceptionHandlerStateRestoreListener オプションを指定することもできます。これは、内部 KafkaStreams インスタンスに委譲されます。また、これらのオプションを StreamsBuilderFactoryBean で間接的に設定する以外に、バージョン 2.1.5 以降、KafkaStreamsCustomizer コールバックインターフェースを使用して内部 KafkaStreams インスタンスを構成できます。KafkaStreamsCustomizer は、StreamsBuilderFactoryBean によって提供されるオプションをオーバーライドすることに注意してください。一部の KafkaStreams 操作を直接実行する必要がある場合は、StreamsBuilderFactoryBean.getKafkaStreams() を使用してその内部 KafkaStreams インスタンスにアクセスできます。StreamsBuilderFactoryBean Bean を型別にオートワイヤーできますが、次の例に示すように、Bean 定義で完全な型を使用する必要があります。

@Bean
public StreamsBuilderFactoryBean myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) {
    return new StreamsBuilderFactoryBean(streamsConfig);
}
...
@Autowired
private StreamsBuilderFactoryBean myKStreamBuilderFactoryBean;

または、インターフェース Bean 定義を使用する場合は、名前で @Qualifier をインジェクション用に追加できます。次の例は、その方法を示しています。

@Bean
public FactoryBean<StreamsBuilder> myKStreamBuilder(KafkaStreamsConfiguration streamsConfig) {
    return new StreamsBuilderFactoryBean(streamsConfig);
}
...
@Autowired
@Qualifier("&myKStreamBuilder")
private StreamsBuilderFactoryBean myKStreamBuilderFactoryBean;

バージョン 2.4.1 以降、ファクトリ Bean には型 KafkaStreamsInfrastructureCustomizer の新しいプロパティ infrastructureCustomizer があります。これにより、ストリームが作成される前に、StreamsBuilder (状態ストアを追加するなど) および / または Topology のカスタマイズが可能になります。

public interface KafkaStreamsInfrastructureCustomizer {

	void configureBuilder(StreamsBuilder builder);

	void configureTopology(Topology topology);

}

デフォルトの no-op 実装は、どちらかが必要でない場合に両方のメソッドを実装する必要がないように提供されています。

複数のカスタマイザを適用する必要がある場合のために、CompositeKafkaStreamsInfrastructureCustomizer が提供されます。

4.3.3. KafkaStreams Micrometer サポート

バージョン 2.5.3 で導入され、ファクトリ Bean によって管理される KafkaStreams オブジェクトの micrometer メーターを自動的に登録するように KafkaStreamsMicrometerListener を構成できます。

streamsBuilderFactoryBean.addListener(new KafkaStreamsMicrometerListener(meterRegistry,
        Collections.singletonList(new ImmutableTag("customTag", "customTagValue"))));

4.3.4. JSON の直列化と逆直列化をストリーミングします

JSON 形式でトピックまたは状態ストアを読み書きするときにデータをシリアライズおよびデシリアライズするために、Spring for Apache Kafka は、JSON を使用する JsonSerde 実装を提供し、直列化、逆直列化、メッセージ変換で説明されている JsonSerializer および JsonDeserializer に委譲します。JsonSerde 実装は、コンストラクター (ターゲット型または ObjectMapper) を通じて同じ構成オプションを提供します。次の例では、JsonSerde を使用して Kafka ストリームの Cat ペイロードをシリアライズおよびデシリアライズします (インスタンスが必要な場合はいつでも JsonSerde を同様の方法で使用できます)。

stream.through(Serdes.Integer(), new JsonSerde<>(Cat.class), "cats");

プロデューサー / コンシューマーファクトリで使用するためにシリアライザー / デシリアライザーをプログラムで構築する場合、バージョン 2.3 以降、流れるような API を使用できるため、構成が簡単になります。

stream.through(new JsonSerde<>(MyKeyType.class)
        .forKeys()
        .noTypeInfo(),
    new JsonSerde<>(MyValueType.class)
        .noTypeInfo(),
    "myTypes");

4.3.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.3.6. 構成

Kafka ストリーム環境を構成するには、StreamsBuilderFactoryBean に KafkaStreamsConfiguration インスタンスが必要です。すべての可能なオプションについては、Apache Kafka のドキュメント [Apache] (英語) を参照してください。

バージョン 2.2 以降、ストリーム設定は StreamsConfig ではなく KafkaStreamsConfiguration オブジェクトとして提供されるようになりました。

特にマイクロサービスを開発する場合、ほとんどの場合、定型コードを回避するために、Spring for Apache Kafka は @EnableKafkaStreams アノテーションを提供します。これは、@Configuration クラスに配置する必要があります。必要なのは、defaultKafkaStreamsConfig という名前の KafkaStreamsConfiguration Bean を宣言することだけです。defaultKafkaStreamsBuilder という名前の StreamsBuilderFactoryBean Bean は、アプリケーションコンテキストで自動的に宣言されます。追加の StreamsBuilderFactoryBean Bean を宣言して使用することもできます。StreamsBuilderFactoryBeanConfigurer を実装する Bean を提供することにより、その Bean の追加のカスタマイズを実行できます。そのような Bean が複数ある場合、Ordered.order プロパティに従って適用されます。

デフォルトでは、ファクトリ Bean が停止すると、KafkaStreams.cleanUp() メソッドが呼び出されます。バージョン 2.1.2 以降、ファクトリ Bean には追加のコンストラクターがあり、start() または stop() 中に cleanUp() メソッドを呼び出すかどうかを制御できるプロパティを持つ CleanupConfig オブジェクトを取得します。バージョン 2.7 以降、デフォルトではローカル状態をクリーンアップしません。

4.3.7. ヘッダーエンリッチャー

バージョン 3.0 は ContextualProcessor の HeaderEnricherProcessor 拡張を追加しました。非推奨の Transformer インターフェースを実装した非推奨の HeaderEnricher と同じ機能を提供します。これは、ストリーム処理内でヘッダーを追加するために使用できます。ヘッダー値は SpEL 式です。式評価のルートオブジェクトには 3 つのプロパティがあります。

  • record - org.apache.kafka.streams.processor.api.Record (keyvaluetimestampheaders)

  • key - 現在のレコードのキー

  • value - 現在のレコードの値

  • context - ProcessorContext、現在のレコードのメタデータへのアクセスを許可します

式は byte[] または String ( UTF-8 を使用して byte[] に変換される) を返す必要があります。

ストリーム内でエンリッチャーを使用するには:

.process(() -> new HeaderEnricherProcessor(expressions))

プロセッサーは key または value を変更しません。ヘッダーを追加するだけです。

レコードごとに新しいインスタンスが必要です。
.process(() -> new HeaderEnricherProcessor<..., ...>(expressionMap))

これは簡単な例で、1 つのリテラルヘッダーと 1 つの変数を追加します。

Map<String, Expression> headers = new HashMap<>();
headers.put("header1", new LiteralExpression("value1"));
SpelExpressionParser parser = new SpelExpressionParser();
headers.put("header2", parser.parseExpression("record.timestamp() + ' @' + record.offset()"));
ProcessorSupplier supplier = () -> new HeaderEnricher<String, String> enricher = new HeaderEnricher<>(headers);
KStream<String, String> stream = builder.stream(INPUT);
stream
        .process(() -> supplier)
        .to(OUTPUT);

4.3.8. MessagingProcessor

バージョン 3.0 は ContextualProcessor の MessagingProcessor 拡張を追加しました。非推奨の Transformer インターフェースを実装した非推奨の MessagingTransformer と同じ機能を提供します。これにより、Kafka ストリームトポロジが Spring Integration フローなどの Spring メッセージングコンポーネントと対話できるようになります。トランスフォーマーには MessagingFunction の実装が必要です。

@FunctionalInterface
public interface MessagingFunction {

	Message<?> exchange(Message<?> message);

}

Spring Integration は、その GatewayProxyFactoryBean を使用した実装を自動的に提供します。また、キー、値、メタデータ (ヘッダーを含む) を Spring メッセージング Message<?> との間で変換するために MessagingMessageConverter が必要です。詳細については、【 KStream から Spring Integration フローを呼び出す ] を参照してください。

4.3.9. デシリアライズ例外からの回復

バージョン 2.3 では、逆直列化例外が発生したときに何らかのアクションを実行できる RecoveringDeserializationExceptionHandler が導入されました。RecoveringDeserializationExceptionHandler が実装である DeserializationExceptionHandler については、Kafka のドキュメントを参照してください。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.3.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.4. アプリケーションのテスト

spring-kafka-test jar には、アプリケーションのテストに役立つ便利なユーティリティがいくつか含まれています。

4.4.1. KafkaTestUtils

o.s.kafka.test.utils.KafkaTestUtils は、レコードを消費したり、さまざまなレコードオフセットを取得したりするための多くの静的ヘルパーメソッドを提供します。詳細については、Javadoc を参照してください。

4.4.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_CONFIG を earliest に設定します。これは、ほとんどの場合、テストケースで送信されたメッセージをコンシューマーが消費する必要があるためです。ConsumerConfig のデフォルトは latest です。つまり、コンシューマーが開始する前にテストによって送信されたメッセージは、それらのレコードを受信しません。以前の動作に戻すには、メソッドを呼び出した後にプロパティを latest に設定します。

組み込みブローカーを使用する場合、クロストークを防ぐために、テストごとに異なるトピックを使用するのが一般的なベストプラクティスです。何らかの理由でこれが不可能な場合、consumeFromEmbeddedTopics メソッドのデフォルトの動作は、割り当て後に割り当てられたパーティションを先頭にシークすることであることに注意してください。コンシューマープロパティにアクセスできないため、seekToEnd ブール値パラメーターを受け取るオーバーロードされたメソッドを使用して、最初ではなく最後までシークする必要があります。

組み込み Kafka サーバーおよび組み込み Zookeeper サーバーを作成するために、EmbeddedKafkaBroker 用の JUnit 4 @Rule ラッパーが提供されています。(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) が提供されています。

デフォルトの spring.embedded.kafka.brokers システムプロパティの代わりに、Kafka ブローカーのアドレスを任意の便利なプロパティに公開できます。この目的のために、組み込み Kafka を開始する前に spring.embedded.kafka.brokers.property (EmbeddedKafkaBroker.BROKER_LIST_PROPERTY) システムプロパティを設定できます。例: Spring Boot では、Kafka クライアントの自動構成用に spring.kafka.bootstrap-servers 構成プロパティがそれぞれ設定されることが期待されます。ランダムなポートに埋め込まれた Kafka でテストを実行する前に、システムプロパティとして spring.embedded.kafka.brokers.property=spring.kafka.bootstrap-servers を設定できます。EmbeddedKafkaBroker はそれを使用してブローカーアドレスを公開します。

EmbeddedKafkaBroker.brokerProperties(Map<String, String>) を使用すると、Kafka サーバーに追加のプロパティを提供できます。可能なブローカープロパティの詳細については、Kafka 構成 [Apache] (英語) を参照してください。

4.4.3. トピックの構成

次の構成例では、5 つのパーティションを持つ cat および hat というトピック、10 のパーティションを持つ thing1 というトピック、および 15 のパーティションを持つ thing2 というトピックを作成します。

public class MyTests {

    @ClassRule
    private static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, false, 5, "cat", "hat");

    @Test
    public void test() {
        embeddedKafkaRule.getEmbeddedKafka()
              .addTopics(new NewTopic("thing1", 10, (short) 1), new NewTopic("thing2", 15, (short) 1));
        ...
      }

}

デフォルトでは、addTopics は問題が発生した場合 (すでに存在するトピックの追加など) に例外をスローします。バージョン 2.6 は、Map<String, Exception> を返すそのメソッドの新しいバージョンを追加しました。キーはトピック名で、値は成功の場合は null、失敗の場合は Exception です。

4.4.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 デーモンでテストを実行する場合、これは問題になる可能性があります。このような状況ではこの手法を使用しないでください。または、テストが完了したら、何かを使用して EmbeddedKafkaBroker で destroy() を呼び出す必要があります。

バージョン 3.0 以降、フレームワークは JUnit プラットフォームの GlobalEmbeddedKafkaTestExecutionListener を公開します。デフォルトでは無効になっています。これには、JUnit プラットフォーム 1.8 以降が必要です。このリスナーの目的は、テスト計画全体に対して 1 つのグローバル EmbeddedKafkaBroker を開始し、計画の最後で停止することです。このリスナーを有効にして、プロジェクト内のすべてのテストに対して 1 つのグローバル組み込み Kafka クラスターを使用するには、システムプロパティまたは JUnit プラットフォーム構成を介して spring.kafka.global.embedded.enabled プロパティを true に設定する必要があります。さらに、次のプロパティを提供できます。

  • spring.kafka.embedded.count - 管理する Kafka ブローカーの数。

  • spring.kafka.embedded.ports - Kafka ブローカーごとに起動するポート (コンマ区切り値)。ランダムポートが優先される場合は 0。値の数は、上記の count と等しくなければなりません。

  • spring.kafka.embedded.topics - 開始された Kafka クラスターで作成するトピック (コンマ区切り値)。

  • spring.kafka.embedded.partitions - 作成されたトピック用にプロビジョニングするパーティションの数。

  • spring.kafka.embedded.broker.properties.location - 追加の Kafka ブローカー構成プロパティのファイルの場所。このプロパティの値は、Spring リソース抽象化パターンに従う必要があります。

基本的に、これらのプロパティは @EmbeddedKafka 属性の一部を模倣しています。

構成プロパティの詳細と、JUnit 5 ユーザーガイド (英語) で提供する方法を参照してください。例: spring.embedded.kafka.brokers.property=spring.kafka.bootstrap-servers エントリ (Spring Boot アプリケーションでのテスト用) は、テストクラスパスの junit-platform.properties ファイルに追加できます。

グローバルな組み込み Kafka とテストごとのクラスを 1 つのテストスイートに結合しないことをお勧めします。どちらも同じシステムプロパティを共有しているため、予期しない動作が発生する可能性が非常に高くなります。
spring-kafka-test は、junit-jupiter-api および junit-platform-launcher (後者はグローバル組み込みブローカーをサポートするため) に推移的な依存関係があります。組み込みブローカーを使用したいが、JUnit を使用していない場合は、これらの依存関係を除外することをお勧めします。

4.4.5. @EmbeddedKafka アノテーション

一般に、ルールを @ClassRule として使用して、テスト間でブローカーを開始および停止しないようにすることをお勧めします (テストごとに異なるトピックを使用します)。バージョン 2.0 以降、Spring のテストアプリケーションコンテキストキャッシングを使用する場合、EmbeddedKafkaBroker Bean も宣言できるため、単一のブローカーを複数のテストクラスで使用できます。便宜上、EmbeddedKafkaBroker Bean を登録するための @EmbeddedKafka というテストクラスレベルのアノテーションを提供します。次の例は、その使用方法を示しています。

@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 ポートプロパティを指定することもできます。

次の例では、@EmbeddedKafka サポートプロパティプレースホルダー解決の topicsbrokerPropertiesbrokerPropertiesLocation 属性を設定します。

@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 から解決されます。さらに、ブローカーのプロパティは、brokerPropertiesLocation によって指定された broker.properties クラスパスリソースからロードされます。プロパティプレースホルダーは、brokerPropertiesLocation URL およびリソース内で見つかったすべてのプロパティプレースホルダーに対して解決されます。brokerProperties によって定義されたプロパティは、brokerPropertiesLocation にあるプロパティをオーバーライドします。

@EmbeddedKafka アノテーションは JUnit 4 または JUnit 5 で使用できます。

4.4.6. JUnit5 を使用した @EmbeddedKafka アノテーション

バージョン 2.3 以降、JUnit5 で @EmbeddedKafka アノテーションを使用するには 2 つの方法があります。@SpringJunitConfig アノテーションとともに使用すると、組み込みブローカーがテストアプリケーションコンテキストに追加されます。クラスまたはメソッドレベルでブローカーをテストにオートワイヤーして、ブローカーのアドレスリストを取得できます。

Spring Test コンテキストを使用しない場合、EmbdeddedKafkaCondition はブローカーを作成します。条件にはパラメーターリゾルバーが含まれているため、テストメソッドでブローカーにアクセスできます。…

@EmbeddedKafka
public class EmbeddedKafkaConditionTests {

    @Test
    public void test(EmbeddedKafkaBroker broker) {
        String brokerList = broker.getBrokersAsString();
        ...
    }

}

@EmbeddedBroker のアノテーションが付けられたクラスが ExtendedWith(SpringExtension.class) のアノテーションも付けられていない (またはメタのアノテーションが付けられていない) 場合、スタンドアロン (Spring Test コンテキストではない) ブローカーが作成されます。@SpringJunitConfig および @SpringBootTest はメタアノテーションが付けられており、これらのアノテーションのいずれかが存在する場合にもコンテキストベースのブローカーが使用されます。

使用可能な Spring Test アプリケーションコンテキストがある場合、トピックとブローカープロパティにはプロパティプレースホルダーを含めることができ、プロパティがどこかで定義されている限り解決されます。使用可能な Spring コンテキストがない場合、これらのプレースホルダーは解決されません。

4.4.7. @SpringBootTest アノテーションに埋め込まれたブローカー

Spring Initializr は、テストスコープの spring-kafka-test 依存関係をプロジェクト構成に自動的に追加するようになりました。

アプリケーションが spring-cloud-stream で Kafka バインダーを使用し、テストに組み込みブローカーを使用する場合は、spring-cloud-stream-test-support 依存関係を削除する必要があります。これは、実際のバインダーがテストケースのテストバインダーに置き換わるためです。一部のテストでテストバインダーを使用し、一部のテストで埋め込みブローカーを使用する場合、実際のバインダーを使用するテストでは、テストクラスのバインダー自動構成を除外して、テストバインダーを無効にする必要があります。次の例は、その方法を示しています。

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

Spring Boot アプリケーションテストで組み込みブローカーを使用するには、いくつかの方法があります。

それらには次のものが含まれます。

JUnit4 クラスルール

次の例は、JUnit4 クラスルールを使用して組み込みブローカーを作成する方法を示しています。

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyApplicationTests {

    @ClassRule
    public static EmbeddedKafkaRule broker = new EmbeddedKafkaRule(1,
        false, "someTopic")
            .brokerListProperty("spring.kafka.bootstrap-servers");
    }

    @Autowired
    private KafkaTemplate<String, String> template;

    @Test
    public void test() {
        ...
    }

}

これは Spring Boot アプリケーションであるため、ブローカーリストプロパティをオーバーライドして Boot のプロパティを設定していることに注意してください。

@EmbeddedKafka アノテーションまたは EmbeddedKafkaBroker Bean

次の例は、@EmbeddedKafka アノテーションを使用して組み込みブローカーを作成する方法を示しています。

@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.4.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.4.9. AssertJ 条件

次の 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.4.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.11. コンシューマーとプロデューサーのモックアップ

kafka-clients ライブラリは、テスト目的で MockConsumer クラスと MockProducer クラスを提供します。

バージョン 3.0.7 以降、リスナーコンテナーまたは KafkaTemplate を使用してテストの一部でこれらのクラスを使用したい場合、フレームワークは MockConsumerFactory および MockProducerFactory 実装を提供するようになりました。

これらのファクトリは、実行中の (または組み込みの) ブローカーを必要とするデフォルトのファクトリの代わりに、リスナーコンテナーおよびテンプレートで使用できます。

単一のコンシューマーを返す単純な実装の例を次に示します。

@Bean
ConsumerFactory<String, String> consumerFactory() {
    MockConsumer<String, String> consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST);
    TopicPartition topicPartition0 = new TopicPartition("topic", 0);
    List<TopicPartition> topicPartitions = Arrays.asList(topicPartition0);
    Map<TopicPartition, Long> beginningOffsets = topicPartitions.stream().collect(Collectors
            .toMap(Function.identity(), tp -> 0L));
    consumer.updateBeginningOffsets(beginningOffsets);
    consumer.schedulePollTask(() -> {
        consumer.addRecord(
                new ConsumerRecord<>("topic", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "test1",
                        new RecordHeaders(), Optional.empty()));
        consumer.addRecord(
                new ConsumerRecord<>("topic", 0, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "test2",
                        new RecordHeaders(), Optional.empty()));
    });
    return new MockConsumerFactory(() -> consumer);
}

同時実行でテストしたい場合は、ファクトリのコンストラクター内の Supplier ラムダで毎回新しいインスタンスを作成する必要があります。

MockProducerFactory には 2 つのコンストラクターがあります。1 つは単純なファクトリを作成するもの、もう 1 つはトランザクションをサポートするファクトリを作成するものです。

以下に例を示します。

@Bean
ProducerFactory<String, String> nonTransFactory() {
    return new MockProducerFactory<>(() ->
            new MockProducer<>(true, new StringSerializer(), new StringSerializer()));
}

@Bean
ProducerFactory<String, String> transFactory() {
    MockProducer<String, String> mockProducer =
            new MockProducer<>(true, new StringSerializer(), new StringSerializer());
    mockProducer.initTransactions();
    return new MockProducerFactory<String, String>((tx, id) -> mockProducer, "defaultTxId");
}

2 番目のケースでは、ラムダは BiFunction<Boolean, String> であることに注意してください。呼び出し元がトランザクションプロデューサーを必要とする場合、最初のパラメーターは true になります。オプションの 2 番目のパラメーターにはトランザクション ID が含まれます。これは、(コンストラクターで提供される) デフォルトにすることも、KafkaTransactionManager (またはローカルトランザクションの場合は KafkaTemplate ) によってオーバーライドすることもできます (そのように構成されている場合)。トランザクション ID は、この値に基づいて別の MockProducer を使用する場合に提供されます。

マルチスレッド環境でプロデューサーを使用している場合、BiFunction は複数のプロデューサーを返す必要があります (おそらく ThreadLocal を使用してスレッドバインドされている)。

トランザクション MockProducer は、initTransaction() を呼び出してトランザクション用に初期化する必要があります。

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 と組み合わせて使用すると、アプリケーションが起動されるたびにすべてのレコードがロードされます。また、コンテナーが null コンシューマーグループのオフセットをコミットしないように、コンテナーの AckMode を MANUAL に設定する必要があります。ただし、バージョン 2.5.5 以降では、上に示したように、すべてのパーティションに初期オフセットを適用できます。詳細については、"明示的なパーティション割り当て" を参照してください。

5.2. 他のトランザクションマネージャーとの Kafka トランザクションの例

次の Spring Boot アプリケーションは、データベースと Kafka トランザクションを連鎖させる例です。リスナーコンテナーは Kafka トランザクションを開始し、@Transactional アノテーションは DB トランザクションを開始します。DB トランザクションが最初にコミットされます。Kafka トランザクションがコミットに失敗した場合、レコードは再配信されるため、DB 更新はべき等である必要があります。

@SpringBootApplication
public class Application {

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

    @Bean
    public ApplicationRunner runner(KafkaTemplate<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 はそのトランザクションを DB トランザクションと同期し、データベースの後にコミット / ロールバックが発生します。

Kafka トランザクションを最初にコミットし、Kafka トランザクションが成功した場合にのみ DB トランザクションをコミットする場合は、ネストされた @Transactional メソッドを使用します。

@Transactional("dstm")
public void someMethod(String in) {
    this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')");
    sendToKafka(in);
}

@Transactional("kafkaTransactionManager")
public void sendToKafka(String in) {
    this.kafkaTemplate.send("topic2", in.toUpperCase());
}

5.3. JsonSerializer および JsonDeserializer のカスタマイズ

シリアライザーとデシリアライザーは、プロパティを使用した多数のカスタマイズをサポートします。詳細については、JSON を参照してください。Spring ではなく kafka-clients コードは、これらのオブジェクトをコンシューマーおよびプロデューサーのファクトリに直接注入しない限り、これらのオブジェクトをインスタンス化します。プロパティを使用して(デ)シリアライザーを構成したいが、たとえばカスタム 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-clients または kafka-streams の異なるバージョンを使用し、組み込みの kafka ブローカーをテストに使用する場合は、Spring Boot の依存関係管理で使用されるバージョンをオーバーライドし、kafka.version プロパティを設定する必要があります。

または、サポートされている Spring Boot バージョンで別の Spring for Apache Kafka バージョンを使用するには、spring-kafka.version プロパティを設定します。

Maven
<properties>
    <kafka.version>3.4.0</kafka.version>
    <spring-kafka.version>3.0.7</spring-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>
Gradle
ext['kafka.version'] = '3.4.0'
ext['spring-kafka.version'] = '3.0.7'

dependencies {
    implementation 'org.springframework.kafka:spring-kafka'
    implementation 'org.apache.kafka:kafka-streams' // optional - only needed when using kafka-streams
    testImplementation 'org.springframework.kafka:spring-kafka-test'
}

テストスコープの依存関係は、組み込みの Kafka ブローカーをテストで使用している場合にのみ必要です。

付録 B: Micrometer 観測資料

B.1. 可観測性 - メトリクス

以下に、このプロジェクトで宣言されたすべての指標のリストを示します。

B.1.1. リスナーの観測

Apache Kafka リスナーの観測。

指標名  spring.kafka.listener (規約クラス KafkaListenerObservation$DefaultKafkaListenerObservationConvention で定義)。タイプ  timer.

指標名  spring.kafka.listener.active (規約クラス KafkaListenerObservation$DefaultKafkaListenerObservationConvention で定義)。タイプ  long task timer.

観測の開始後に追加された KeyValues は、*.active メトリクスから欠落している可能性があります。
Micrometer は、ベースユニットに nanoseconds を内部的に使用します。ただし、各バックエンドが実際のベースユニットを決定します。(つまり、Prometheus は秒を使用します)

包含クラス KafkaListenerObservation の名前。

すべてのタグには、spring.kafka.listener プレフィックスを付ける必要があります。
表 5: カーディナリティの低いキー

名前

説明

spring.kafka.listener.id (必須)

リスナー ID (またはリスナーコンテナー Bean 名)。

B.1.2. テンプレート観測

KafkaTemplates の観測。

指標名  spring.kafka.template (規約クラス KafkaTemplateObservation$DefaultKafkaTemplateObservationConvention で定義)。タイプ  timer.

指標名  spring.kafka.template.active (規約クラス KafkaTemplateObservation$DefaultKafkaTemplateObservationConvention で定義)。タイプ  long task timer.

観測の開始後に追加された KeyValues は、*.active メトリクスから欠落している可能性があります。
Micrometer は、ベースユニットに nanoseconds を内部的に使用します。ただし、各バックエンドが実際のベースユニットを決定します。(つまり、Prometheus は秒を使用します)

包含クラス KafkaTemplateObservation の名前。

すべてのタグには、spring.kafka.template プレフィックスを付ける必要があります。
表 6: カーディナリティの低いキー

名前

説明

spring.kafka.template.name (必須)

テンプレートの Bean 名。

B.2. 可観測性 - スパン

以下に、このプロジェクトで宣言されたすべてのスパンのリストを示します。

B.2.1. リスナーの観測スパン

Apache Kafka リスナーの観測。

スパン名  spring.kafka.listener (規約クラス KafkaListenerObservation$DefaultKafkaListenerObservationConvention で定義)。

包含クラス KafkaListenerObservation の名前。

すべてのタグには、spring.kafka.listener プレフィックスを付ける必要があります。
表 7: タグキー

名前

説明

spring.kafka.listener.id (required)

Listener id (or listener container bean name).

B.2.2. Template Observation Span

Observation for KafkaTemplates.

Span name spring.kafka.template (defined by convention class KafkaTemplateObservation$DefaultKafkaTemplateObservationConvention).

Name of the enclosing class KafkaTemplateObservation.

All tags must be prefixed with spring.kafka.template prefix!
Table 8. Tag Keys

Name

Description

spring.kafka.template.name (required)

Bean name of the template.

B.3. Observability - Conventions

Below you can find a list of all GlobalObservationConvention and ObservationConvention declared by this project.

Table 9. ObservationConvention implementations

ObservationConvention Class Name

Applicable ObservationContext Class Name

KafkaListenerObservation

KafkaRecordReceiverContext

KafkaListenerObservation.DefaultKafkaListenerObservationConvention

KafkaRecordReceiverContext

KafkaListenerObservationConvention

KafkaRecordReceiverContext

KafkaTemplateObservation

KafkaRecordSenderContext

KafkaTemplateObservation.DefaultKafkaTemplateObservationConvention

KafkaRecordSenderContext

KafkaTemplateObservationConvention

KafkaRecordSenderContext

付録 C: ネイティブイメージ

Spring AOT ネイティブヒントは、@KafkaListener で使用される AVRO 生成クラスのヒントを含む、Spring for Apache Kafka を使用する Spring アプリケーションのネイティブイメージの開発を支援するために提供されます。

spring-kafka-test (および特にその EmbeddedKafkaBroker) は、ネイティブイメージではサポートされていません。

spring-aot-smoke-tests GitHub リポジトリ (英語) でいくつかの例を見ることができます。

付録 D: 変更履歴

D.1. 2.8 以降の 2.9 の新機能

D.1.1. Kafka クライアントバージョン

このバージョンには、3.2.0 kafka-clients が必要です。

D.1.2. エラーハンドラーの変更

DefaultErrorHandler は、残りのレコードのオフセットをシークする代わりに、コンテナーを 1 つのポーリングで一時停止し、前回のポーリングからの残りの結果を使用するように構成できるようになりました。詳細については、DefaultErrorHandler を参照してください。

DefaultErrorHandler に BackOffHandler プロパティが追加されました。詳細については、バックオフハンドラーを参照してください。

D.1.3. リスナーコンテナーの変更

interceptBeforeTx は、すべてのトランザクションマネージャーで動作するようになりました (以前は、KafkaAwareTransactionManager が使用されている場合にのみ適用されていました)。[interceptBeforeTx] を参照してください。

以前のポーリングからのすべてのレコードが処理された後ではなく、現在のレコードが処理された後にコンテナーがコンシューマーを一時停止できるようにする、新しいコンテナープロパティ pauseImmediate が提供されます。[ 一時停止 ] を参照してください。

コンシューマーの認証と認可に関連するイベント

D.1.4. ヘッダーマッパーの変更点

どの受信 ヘッダーをマップするかを構成できるようになりました。2.8.8 以降のバージョンでも利用できます。詳細については、メッセージヘッダーを参照してください。

D.1.5. KafkaTemplate の変更

3.0 では、このクラスによって返される先物は ListenableFuture ではなく CompletableFuture になります。このリリースを使用する場合の移行の支援については、KafkaTemplate の使用を参照してください。

D.1.6. ReplyingKafkaTemplate の変更

テンプレートは、応答コンテナーが初期化される前にリクエストを送信する際の競合を回避するために、応答コンテナーでの割り当てを待機するメソッドを提供するようになりました。2.8.8 以降のバージョンでも利用できます。ReplyingKafkaTemplate を使用するを参照してください。

3.0 では、このクラスによって返される先物は ListenableFuture ではなく CompletableFuture になります。このリリースを使用する場合の移行の支援については、ReplyingKafkaTemplate の使用および Message<?> でのリクエスト / リプライを参照してください。

D.2. 2.7 以降の 2.8 の新機能

このセクションでは、バージョン 2.7 からバージョン 2.8 に加えられた変更について説明します。以前のバージョンでの変更については、変更履歴を参照してください。

D.2.1. Kafka クライアントバージョン

このバージョンには 3.0.0 kafka-clients が必要です

D.2.2. パッケージの変更

型マッピングに関連するクラスとインターフェースは、…​support.converter から …​support.mapping に移動されました。

  • AbstractJavaTypeMapper

  • ClassMapper

  • DefaultJackson2JavaTypeMapper

  • Jackson2JavaTypeMapper

D.2.3. 故障した手動コミット

リスナーコンテナーは、手動のオフセットコミットを順不同で(通常は非同期に)受け入れるように構成できるようになりました。コンテナーは、欠落しているオフセットが確認されるまでコミットを延期します。詳細については、オフセットの手動コミットを参照してください。

D.2.4. @KafkaListener の変更

リスナーメソッドがメソッド自体のバッチリスナーであるかどうかを指定できるようになりました。これにより、レコードリスナーとバッチリスナーの両方に同じコンテナーファクトリを使用できます。

詳細については、バッチリスナーを参照してください。

バッチリスナーは、変換例外を処理できるようになりました。

詳細については、バッチエラーハンドラーによる変換エラーを参照してください。

RecordFilterStrategy をバッチリスナーで使用すると、1 回の呼び出しでバッチ全体をフィルタリングできるようになりました。詳細については、バッチリスナーの最後にある注記を参照してください。

@KafkaListener アノテーションに filter 属性が追加され、このリスナーのみのコンテナーファクトリの RecordFilterStrategy をオーバーライドできるようになりました。

@KafkaListener アノテーションに info 属性が追加されました。これは、新しいリスナーコンテナープロパティ listenerInfo にデータを入力するために使用されます。次に、これを使用して、RecordInterceptorRecordFilterStrategy、リスナー自体で使用できる各レコードの KafkaHeaders.LISTENER_INFO ヘッダーにデータを入力します。詳細については、リスナー情報ヘッダーおよび抽象リスナーコンテナーのプロパティを参照してください。

D.2.5. KafkaTemplate の変更

これで、トピック、パーティション、オフセットを指定して、単一のレコードを受け取ることができます。詳細については、KafkaTemplate を使用した受信を参照してください。

D.2.6. CommonErrorHandler が追加されました

バッチリスナーを記録するためのレガシー GenericErrorHandler とそのサブインターフェース階層は、GenericErrorHandler のほとんどのレガシー実装に対応する実装を持つ新しいシングルインターフェース CommonErrorHandler に置き換えられました。詳細については、コンテナーエラーハンドラーおよびカスタムレガシーエラーハンドラーの実装を CommonErrorHandler に移行するを参照してください。

D.2.7. リスナーコンテナーの変更

interceptBeforeTx コンテナープロパティは、デフォルトで true になりました。

authorizationExceptionRetryInterval プロパティは authExceptionRetryInterval に名前が変更され、以前の AuthorizationException に加えて AuthenticationException にも適用されるようになりました。このプロパティが設定されていない限り、両方の例外は致命的と見なされ、コンテナーはデフォルトで停止します。

詳細については、KafkaMessageListenerContainer を使用するおよびリスナーコンテナーのプロパティを参照してください。

D.2.8. シリアライザー / デシリアライザーの変更

DelegatingByTopicSerializer と DelegatingByTopicDeserializer が提供されるようになりました。詳細については、シリアライザーとデシリアライザーの委譲を参照してください。

D.2.9. DeadLetterPublishingRecover の変更

プロパティ stripPreviousExceptionHeaders は、デフォルトで true になりました。

現在、出力レコードに追加されるヘッダーをカスタマイズするためのいくつかの手法があります。

詳細については、デッドレターレコードヘッダーの管理を参照してください。

D.2.10. 再試行可能なトピックの変更

これで、再試行可能なトピックと再試行不可能なトピックに同じファクトリを使用できます。詳細については、ListenerContainerFactory の指定を参照してください。

失敗したレコードを DLT に直接送信する、致命的な例外の管理可能なグローバルリストが追加されました。管理方法については、例外分類子を参照してください。

ブロッキングとノンブロッキングの再試行を組み合わせて使用できるようになりました。詳細については、ブロッキングとノンブロッキングの再試行の組み合わせを参照してください。

再試行可能なトピック機能を使用するときにスローされる KafkaBackOffException は、DEBUG レベルでログに記録されるようになりました。ロギングレベルを WARN に戻すか、他のレベルに設定する必要がある場合は、KafkaBackOffException ログレベルの変更を参照してください。

D.3. 2.6 および 2.7 間の変更

D.3.1. Kafka クライアントバージョン

このバージョンには、2.7.0 kafka-clients が必要です。バージョン 2.7.1 以降、2.8.0 クライアントとも互換性があります。Spring Boot の依存関係を上書きを参照してください。

D.3.2. トピックを使用したノンブロッキング遅延再試行

この重要な新機能は、このリリースで追加されています。厳密な順序付けが重要でない場合、失敗した配信を別のトピックに送信して、後で使用することができます。このような一連の再試行トピックは、遅延を増やしながら構成できます。詳細については、ノンブロッキング再試行を参照してください。

D.3.3. リスナーコンテナーの変更

onlyLogRecordMetadata コンテナープロパティは、デフォルトで true になりました。

新しいコンテナープロパティ stopImmediate が利用可能になりました。

詳細については、リスナーコンテナーのプロパティを参照してください。

配信試行の間に BackOff を使用するエラーハンドラー (例: SeekToCurrentErrorHandler と DefaultAfterRollbackProcessor) は、停止を遅らせるのではなく、コンテナーの停止後すぐにバックオフ間隔を終了します。

FailedRecordProcessor を継承するエラーハンドラーおよびアフターロールバックプロセッサーを 1 つ以上の RetryListener で構成して、再試行およびリカバリの進行状況に関する情報を受け取ることができるようになりました。

RecordInterceptor には、リスナーが戻った後に(通常、または例外をスローすることによって)呼び出される追加のメソッドが含まれるようになりました。また、サブインターフェース ConsumerAwareRecordInterceptor もあります。さらに、バッチリスナー用の BatchInterceptor が追加されました。詳細については、メッセージリスナコンテナーを参照してください。

D.3.4. @KafkaListener の変更

@KafkaHandler メソッド(クラスレベルのリスナー)のペイロードパラメーターを検証できるようになりました。詳細については、@KafkaListener @Payload 検証を参照してください。

これで、MessagingMessageConverter および BatchMessagingMessageConverter に rawRecordHeader プロパティを設定できます。これにより、変換された Message<?> に生の ConsumerRecord が追加されます。これは、たとえば、リスナーエラーハンドラーで DeadLetterPublishingRecoverer を使用する場合に役立ちます。詳細については、リスナーエラーハンドラーを参照してください。

アプリケーションの初期化中に @KafkaListener アノテーションを変更できるようになりました。詳細については、@KafkaListener 属性の変更を参照してください。

D.3.5. DeadLetterPublishingRecover の変更

これで、キーと値の両方が逆直列化に失敗した場合、元の値が DLT に公開されます。以前は、値が入力されていましたが、キー DeserializationException はヘッダーに残っていました。リカバリ装置をサブクラス化し、createProducerRecord メソッドをオーバーライドした場合、API に重大な変更があります。

さらに、リカバリ機能は、宛先リゾルバーによって選択されたパーティションが実際に存在することを確認してから公開します。

詳細については、デッドレターレコードの公開を参照してください。

D.3.6. ChainedKafkaTransactionManager は非推奨

詳細については、トランザクションを参照してください。

D.3.7. ReplyingKafkaTemplate の変更

現在、何らかの条件が存在する場合に、応答を調べて将来を例外的に失敗させるメカニズムがあります。

spring-messaging の送受信のサポート Message<?> が追加されました。

詳細については、ReplyingKafkaTemplate を使用するを参照してください。

D.3.8. Kafka ストリームの変更

デフォルトでは、StreamsBuilderFactoryBean はローカル状態をクリーンアップしないように構成されています。詳細については、構成を参照してください。

D.3.9. KafkaAdmin の変更

新しいメソッド createOrModifyTopics および describeTopics が追加されました。KafkaAdmin.NewTopics が追加され、単一の Bean で複数のトピックを簡単に構成できるようになりました。詳細については、トピックの構成を参照してください。

D.3.10. MessageConverter の変更

spring-messaging SmartMessageConverter を MessagingMessageConverter に追加できるようになり、contentType ヘッダーに基づいたコンテンツネゴシエーションが可能になりました。詳細については、Spring メッセージングメッセージ変換を参照してください。

D.3.11. @KafkaListener のシーケンス

詳細については、@KafkaListener を順番に開始するを参照してください。

D.3.12. ExponentialBackOffWithMaxRetries

新しい BackOff 実装が提供され、最大再試行回数の構成がより便利になります。詳細については、ExponentialBackOffWithMaxRetries の実装を参照してください。

D.3.13. 条件付き委譲エラーハンドラー

これらの新しいエラーハンドラーは、例外の種類に応じて、さまざまなエラーハンドラーに委譲するように構成できます。詳細については、エラーハンドラーの委譲を参照してください。

D.4. 2.5 および 2.6 間の変更

D.4.1. Kafka クライアントバージョン

このバージョンには、2.6.0 kafka-clients が必要です。

D.4.2. リスナーコンテナーの変更

デフォルトの EOSMode は BETA になりました。詳細については、正確に一度セマンティクスを参照してください。

さまざまなエラーハンドラー ( FailedRecordProcessor を継承する) と DefaultAfterRollbackProcessor は、回復が失敗した場合に BackOff をリセットするようになりました。さらに、失敗したレコードや例外に基づいて、使用する BackOff を選択できるようになりました。

コンテナーのプロパティで adviceChain を設定できるようになりました。詳細については、リスナーコンテナーのプロパティを参照してください。

コンテナーが ListenerContainerIdleEvent を公開するように構成されている場合、アイドルイベントの公開後にレコードを受信すると、ListenerContainerNoLongerIdleEvent を公開するようになりました。詳細については、アプリケーションイベントおよびアイドル状態のコンシューマーと無反応なコンシューマーの検出を参照してください。

D.4.3. @KafkaListener の変更

手動のパーティション割り当てを使用する場合、どのパーティションを初期オフセットにリセットするかを決定するためのワイルドカードを指定できるようになりました。さらに、リスナーが ConsumerSeekAware を実装している場合、onPartitionsAssigned() は手動割り当ての後に呼び出されます。(バージョン 2.5.5 でも追加されました)。詳細については、明示的なパーティション割り当てを参照してください。

AbstractConsumerSeekAware に便利なメソッドが追加され、検索が簡単になりました。詳細については、特定のオフセットを求めてを参照してください。

D.4.4. ErrorHandler の変更

FailedRecordProcessor のサブクラス (例: SeekToCurrentErrorHandlerDefaultAfterRollbackProcessorRecoveringBatchErrorHandler) は、例外がこのレコードで以前に発生したものとは異なる型である場合、再試行状態をリセットするように構成できるようになりました。

D.4.5. プロデューサーファクトリの変更

プロデューサーの最大年齢を設定できるようになりました。その後、プロデューサーは閉じられて再作成されます。詳細については、トランザクションを参照してください。

DefaultKafkaProducerFactory の作成後に構成マップを更新できるようになりました。これは、たとえば、資格情報が変更された後に SSL キー / トラストストアの場所を更新する必要がある場合に役立ちます。詳細については、DefaultKafkaProducerFactory を使用するを参照してください。

D.5. 2.4 および 2.5 間の変更

このセクションでは、バージョン 2.4 からバージョン 2.5 に加えられた変更について説明します。以前のバージョンでの変更については、変更履歴を参照してください。

D.5.1. コンシューマー / プロデューサーファクトリの変更

デフォルトのコンシューマーおよびプロデューサーファクトリは、コンシューマーまたはプロデューサーが作成またはクローズされるたびにコールバックを呼び出すことができるようになりました。ネイティブ Micrometer メトリクスの実装が提供されます。詳細については、ファクトリリスナーを参照してください。

実行時にブートストラップサーバーのプロパティを変更できるようになり、別の Kafka クラスターへのフェイルオーバーが可能になりました。詳細については、Kafka への接続を参照してください。

D.5.2. StreamsBuilderFactoryBean の変更

ファクトリ Bean は、KafkaStreams が作成または破棄されるたびにコールバックを呼び出すことができるようになりました。ネイティブ Micrometer メトリクスの実装が提供されます。詳細については、KafkaStreams Micrometer サポートを参照してください。

D.5.3. Kafka クライアントバージョン

このバージョンには、2.5.0 kafka-clients が必要です。

D.5.4. クラス / パッケージの変更

SeekUtils が o.s.k.support パッケージから o.s.k.listener に移動しました。

D.5.5. 配信試行ヘッダー

特定のエラーハンドラーの使用時およびロールバックプロセッサー後の配信試行を追跡するヘッダーを追加するオプションが追加されました。詳細については、配信試行ヘッダーを参照してください。

D.5.6. @KafkaListener の変更

@KafkaListener の戻り値の型が Message<?> の場合、必要に応じてデフォルトの応答ヘッダーが自動的に入力されるようになりました。詳細については、返信型 Message<?> を参照してください。

受信レコードに null キーがある場合、KafkaHeaders.RECEIVED_MESSAGE_KEY には null 値が設定されなくなりました。ヘッダーは完全に省略されます。

@KafkaListener メソッドは、トピック、パーティションなどのメタデータに個別のヘッダーを使用する代わりに、ConsumerRecordMetadata パラメーターを指定できるようになりました。詳細については、コンシューマーレコードのメタデータを参照してください。

D.5.7. リスナーコンテナーの変更

assignmentCommitOption コンテナープロパティは、デフォルトで LATEST_ONLY_NO_TX になりました。詳細については、リスナーコンテナーのプロパティを参照してください。

トランザクションを使用する場合、subBatchPerPartition コンテナープロパティはデフォルトで true になりました。詳細については、トランザクションを参照してください。

新しい RecoveringBatchErrorHandler が提供されるようになりました。

静的グループメンバーシップがサポートされるようになりました。詳細については、メッセージリスナコンテナーを参照してください。

増分 / 協調リバランスが構成されている場合、オフセットが致命的でない RebalanceInProgressException でコミットに失敗した場合、コンテナーはリバランスが完了した後、このインスタンスに割り当てられたままのパーティションのオフセットを再コミットしようとします。

デフォルトのエラーハンドラーは、レコードリスナーの場合は SeekToCurrentErrorHandler、バッチリスナーの場合は RecoveringBatchErrorHandler になりました。詳細については、コンテナーエラーハンドラーを参照してください。

標準エラーハンドラーによって意図的にスローされた例外がログに記録されるレベルを制御できるようになりました。詳細については、コンテナーエラーハンドラーを参照してください。

getAssignmentsByClientId() メソッドが追加され、同時コンテナー内のどのコンシューマーにどのパーティションが割り当てられているかを簡単に判断できるようになりました。詳細については、リスナーコンテナーのプロパティを参照してください。

エラー、デバッグログなどの ConsumerRecord 全体のログ記録を抑制できるようになりました。リスナーコンテナーのプロパティの onlyLogRecordMetadata を参照してください。

D.5.8. KafkaTemplate の変更

KafkaTemplate は micrometer タイマーを維持できるようになりました。詳細については、モニターを参照してください。

KafkaTemplate を ProducerConfig プロパティで構成して、プロデューサーファクトリのプロパティをオーバーライドできるようになりました。詳細については、KafkaTemplate の使用を参照してください。

RoutingKafkaTemplate が提供されるようになりました。詳細については、RoutingKafkaTemplate を使用するを参照してください。

ListenerFutureCallback の代わりに KafkaSendCallback を使用してより狭い例外を取得できるようになり、失敗した ProducerRecord を抽出しやすくなりました。詳細については、KafkaTemplate の使用を参照してください。

D.5.9. Kafka 文字列シリアライザー / デシリアライザー

新しい ToStringSerializer/StringDeserializer および関連する SerDe が提供されるようになりました。詳細については、文字列の直列化を参照してください。

D.5.10. JsonDeserializer

JsonDeserializer は、デシリアライゼーション型を決定するための柔軟性が向上しました。詳細については、メソッドを使用して型を決定するを参照してください。

D.5.11. Serializer/Deserializer の委譲

送信 レコードにヘッダーがない場合、DelegatingSerializer は「標準」型を処理できるようになりました。詳細については、シリアライザーとデシリアライザーの委譲を参照してください。

D.5.12. 変更のテスト

KafkaTestUtils.consumerProps() ヘルパーレコードは、デフォルトで ConsumerConfig.AUTO_OFFSET_RESET_CONFIG を earliest に設定するようになりました。詳細については、JUnit を参照してください。

D.6. 2.3 および 2.4 間の変更

D.6.1. Kafka クライアントバージョン

このバージョンには 2.4.0 kafka-clients 以降が必要であり、新しい増分リバランス機能をサポートしています。

D.6.2. ConsumerAwareRebalanceListener

ConsumerRebalanceListener と同様に、このインターフェースには onPartitionsLost メソッドが追加されました。詳細については、Apache Kafka のドキュメントを参照してください。

ConsumerRebalanceListener とは異なり、デフォルトの実装では onPartitionsRevoked を呼び出しません。代わりに、リスナーコンテナーは onPartitionsLost を呼び出した後にそのメソッドを呼び出します。ConsumerAwareRebalanceListener を実装するときに同じことをすべきではありません。

詳細については、リスナーのリバランスの最後にある重要な注記を参照してください。

D.6.3. GenericErrorHandler

isAckAfterHandle() のデフォルトの実装は、デフォルトで true を返すようになりました。

D.6.4. KafkaTemplate

KafkaTemplate は、トランザクションと共に非トランザクションパブリッシングをサポートするようになりました。詳細については、KafkaTemplate トランザクションおよび非トランザクションパブリッシングを参照してください。

D.6.5. AggregatingReplyingKafkaTemplate

releaseStrategy が BiConsumer になりました。これは、タイムアウト後に (およびレコードが到着したときに) 呼び出されるようになりました。タイムアウト後の呼び出しの場合、2 番目のパラメーターは true です。

詳細については、複数の返信を集約するを参照してください。

D.6.6. リスナーコンテナー

ContainerProperties は、AuthorizationException が KafkaConsumer によってスローされた後にリスナーコンテナーが再試行できるようにする authorizationExceptionRetryInterval オプションを提供します。詳細については、JavaDocs および KafkaMessageListenerContainer を使用するを参照してください。

D.6.7. @KafkaListener

@KafkaListener アノテーションには、新しいプロパティ splitIterables があります。デフォルトは真。応答リスナーが Iterable を返す場合、このプロパティは、返される結果を単一のレコードとして送信するか、各要素のレコードとして送信するかを制御します。詳細については、@SendTo を使用したリスナー結果の転送を参照してください。

バッチリスナーを BatchToRecordAdapter で構成できるようになりました。これにより、たとえば、リスナーが一度に 1 つのレコードを取得しながら、トランザクションでバッチを処理できます。デフォルトの実装では、バッチ全体の処理を停止することなく、ConsumerRecordRecoverer を使用してバッチ内のエラーを処理できます。これは、トランザクションを使用する場合に便利です。詳細については、バッチリスナーとのトランザクションを参照してください。

D.6.8. Kafka ストリーム

StreamsBuilderFactoryBean は、新しいプロパティ KafkaStreamsInfrastructureCustomizer を受け入れます。これにより、ストリームが作成される前にビルダーおよび / またはトポロジーを構成できます。詳細については、Spring 管理を参照してください。

D.7. 2.2 および 2.3 間の変更

このセクションでは、バージョン 2.2 からバージョン 2.3 に加えられた変更について説明します。

D.7.1. ヒント、コツ、例

新しい章ヒント、コツ、例が追加されました。GitHub の課題および / またはその章の追加エントリのプルリクエストを送信してください。

D.7.2. Kafka クライアントバージョン

このバージョンには 2.3.0 kafka-clients 以降が必要です。

D.7.3. クラス / パッケージの変更

TopicPartitionInitialOffset は TopicPartitionOffset を推奨して非推奨になりました。

D.7.4. 構成変更

バージョン 2.3.4 以降、missingTopicsFatal コンテナープロパティはデフォルトで false です。これが true の場合、ブローカーがダウンしている場合、アプリケーションは起動に失敗します。多くのユーザーがこの変更の影響を受けました。Kafka が高可用性プラットフォームであることを考えると、アクティブなブローカーなしでアプリケーションを開始することが一般的な使用例になるとは予想していませんでした。

D.7.5. プロデューサーとコンシューマーファクトリの変更

DefaultKafkaProducerFactory は、スレッドごとにプロデューサーを作成するように設定できるようになりました。構成されたクラス (引数なしのコンストラクターが必要) または Serializer インスタンスを使用して構築する代わりに、コンストラクターに Supplier<Serializer> インスタンスを提供することもできます。詳細については、DefaultKafkaProducerFactory の使用を参照してください。

DefaultKafkaConsumerFactory の Supplier<Deserializer> インスタンスでも同じオプションを使用できます。詳細については、KafkaMessageListenerContainer の使用を参照してください。

D.7.6. リスナーコンテナーの変更

以前は、リスナーがリスナーアダプター ( @KafkaListener など) を使用して呼び出されたときに、エラーハンドラーは ListenerExecutionFailedException (実際のリスナー例外は cause) を受け取りました。ネイティブ GenericMessageListener によってスローされた例外は、変更されずにエラーハンドラーに渡されました。ListenerExecutionFailedException は常に引数であり (実際のリスナー例外は cause として)、コンテナーの group.id プロパティへのアクセスを提供します。

リスナーコンテナーにはオフセットをコミットするための独自のメカニズムがあるため、Kafka ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG が false であることが優先されます。コンシューマーファクトリまたはコンテナーのコンシューマープロパティオーバーライドで特に設定されていない限り、自動的に false に設定されるようになりました。

ackOnError プロパティは、デフォルトで false になりました。

リスナーメソッドでコンシューマーの group.id プロパティを取得できるようになりました。詳細については、コンシューマー group.id の取得を参照してください。

コンテナーには新しいプロパティ recordInterceptor があり、リスナーを呼び出す前にレコードをインスペクションまたは変更できます。複数のインターセプターを呼び出す必要がある場合に備えて、CompositeRecordInterceptor も提供されます。詳細については、メッセージリスナコンテナーを参照してください。

ConsumerSeekAware には、開始位置、終了位置、現在の位置を基準にしてシークを実行し、タイムスタンプ以上の最初のオフセットをシークできる新しいメソッドがあります。詳細については、特定のオフセットを求めてを参照してください。

シークを簡素化するために、便利なクラス AbstractConsumerSeekAware が提供されるようになりました。詳細については、特定のオフセットを求めてを参照してください。

ContainerProperties は、idleBetweenPolls オプションを提供して、リスナーコンテナーのメインループを KafkaConsumer.poll() 呼び出し間でスリープさせます。詳細については、JavaDocs および KafkaMessageListenerContainer の使用を参照してください。

AckMode.MANUAL (または MANUAL_IMMEDIATE) を使用する場合、Acknowledgment で nack を呼び出すことで再配信を行うことができるようになりました。詳細については、オフセットのコミットを参照してください。

リスナーのパフォーマンスは、Micrometer Timer を使用して監視できるようになりました。詳細については、モニターを参照してください。

コンテナーは、起動に関連する追加のコンシューマーライフサイクルイベントを公開するようになりました。詳細については、アプリケーションイベントを参照してください。

トランザクションバッチリスナーがゾンビフェンシングをサポートできるようになりました。詳細については、トランザクションを参照してください。

リスナーコンテナーファクトリは、作成および設定後に各コンテナーをさらに設定するために ContainerCustomizer を使用して設定できるようになりました。詳細については、コンテナーファクトリを参照してください。

D.7.7. ErrorHandler の変更

SeekToCurrentErrorHandler は現在、特定の例外を致命的なものとして扱い、それらの再試行を無効にして、最初の失敗時に回復プログラムを呼び出します。

SeekToCurrentErrorHandler および SeekToCurrentBatchErrorHandler は、配信試行の間に BackOff (スレッドスリープ) を適用するように設定できるようになりました。

バージョン 2.3.2 以降、失敗したレコードの回復後にエラーハンドラーが戻るときに、回復されたレコードのオフセットがコミットされます。

DeadLetterPublishingRecoverer は、ErrorHandlingDeserializer と組み合わせて使用されると、デッドレタートピックに送信されたメッセージのペイロードを、デシリアライズできなかった元の値に設定するようになりました。以前は、null であり、メッセージヘッダーから DeserializationException を抽出するために必要なユーザーコードでした。詳細については、デッドレターレコードの公開を参照してください。

D.7.8. TopicBuilder

自動トピックプロビジョニング用の NewTopic @Bean をより便利に作成するために、新しいクラス TopicBuilder が提供されています。詳細については、トピックの構成を参照してください。

D.7.9. Kafka ストリームの変更

@EnableKafkaStreams によって作成された StreamsBuilderFactoryBean の追加構成を実行できるようになりました。詳細については、ストリーム構成を参照してください。

逆直列化エラーのあるレコードを回復できる RecoveringDeserializationExceptionHandler が提供されるようになりました。DeadLetterPublishingRecoverer と組み合わせて使用して、これらのレコードをデッドレタートピックに送信できます。詳細については、デシリアライズ例外からの回復を参照してください。

SpEL を使用してヘッダー値を生成する HeaderEnricher トランスフォーマーが提供されています。詳細については、ヘッダーエンリッチャーを参照してください。

MessagingTransformer が提供されました。これにより、Kafka ストリームトポロジが Spring Integration フローなどの spring-messaging コンポーネントと対話できるようになります。詳細については、MessagingProcessor および【 KStream から Spring Integration フローを呼び出すを参照してください。

D.7.10. JSON コンポーネントの変更

現在、すべての JSON 対応コンポーネントは、デフォルトで JacksonUtils.enhancedObjectMapper() によって生成された Jackson ObjectMapper で構成されています。JsonDeserializer は、ターゲットジェネリクスコンテナー型の処理を改善するために、TypeReference ベースのコンストラクターを提供するようになりました。また、org.springframework.util.MimeType をプレーン文字列にシリアライズするために JacksonMimeTypeModule が導入されました。詳細については、JavaDocs および直列化、逆直列化、メッセージ変換を参照してください。

ByteArrayJsonMessageConverter と、すべての Json コンバーター用の新しいスーパークラス JsonMessageConverter が提供されています。また、StringOrBytesSerializer が利用可能になりました。ProducerRecord で byte[]BytesString 値を直列化できます。詳細については、Spring メッセージングメッセージ変換を参照してください。

JsonSerializerJsonDeserializerJsonSerde には、プログラムによる構成をより簡単にするための流れるような API が含まれるようになりました。詳細については、javadoc、直列化、逆直列化、メッセージ変換JSON の直列化と逆直列化をストリーミングしますを参照してください。

D.7.11. ReplyingKafkaTemplate

応答がタイムアウトすると、フューチャーは例外的に KafkaException ではなく KafkaReplyTimeoutException で完了します。

また、オーバーロードされた sendAndReceive メソッドが提供され、メッセージごとに応答タイムアウトを指定できるようになりました。

D.7.12. AggregatingReplyingKafkaTemplate

複数の受信者からの応答を集約することにより、ReplyingKafkaTemplate を拡張します。詳細については、複数の返信を集約するを参照してください。

D.7.13. トランザクションの変更

KafkaTemplate および KafkaTransactionManager でプロデューサーファクトリの transactionIdPrefix をオーバーライドできるようになりました。詳細については、transactionIdPrefix を参照してください。

D.7.14. 新しい委譲シリアライザー / デシリアライザー

フレームワークは、ヘッダーを使用して複数のキー / 値型でレコードを生成および使用できるようにする、委譲 Serializer および Deserializer を提供するようになりました。詳細については、シリアライザーとデシリアライザーの委譲を参照してください。

D.7.15. 新しい再試行デシリアライザー

フレームワークは、ネットワークの問題などの一時的なエラーが発生した場合に直列化を再試行するために、委譲 RetryingDeserializer を提供するようになりました。詳細については、デシリアライザーの再試行を参照してください。

D.8. 2.1 および 2.2 間の変更

D.8.1. Kafka クライアントバージョン

このバージョンには 2.0.0 kafka-clients 以降が必要です。

D.8.2. クラスとパッケージの変更

ContainerProperties クラスは org.springframework.kafka.listener.config から org.springframework.kafka.listener に移動しました。

AckMode 列挙型が AbstractMessageListenerContainer から ContainerProperties に移動されました。

setBatchErrorHandler() および setErrorHandler() メソッドは、ContainerProperties から AbstractMessageListenerContainer および AbstractKafkaListenerContainerFactory の両方に移動されました。

D.8.3. ロールバック処理後

新しい AfterRollbackProcessor 戦略が提供されます。詳細については、ロールバック後のプロセッサーを参照してください。

D.8.4. ConcurrentKafkaListenerContainerFactory の変更

ConcurrentKafkaListenerContainerFactory を使用して、@KafkaListener アノテーションだけでなく、任意の ConcurrentMessageListenerContainer を作成および構成できるようになりました。詳細については、コンテナーファクトリを参照してください。

D.8.5. リスナーコンテナーの変更

新しいコンテナープロパティ (missingTopicsFatal) が追加されました。詳細については、KafkaMessageListenerContainer を使用するを参照してください。

コンシューマーが停止したときに ConsumerStoppedEvent が発行されるようになりました。詳細については、スレッドセーフを参照してください。

バッチリスナーは、オプションで List<ConsumerRecord<?, ?> の代わりに完全な ConsumerRecords<?, ?> オブジェクトを受け取ることができます。詳細については、バッチリスナーを参照してください。

DefaultAfterRollbackProcessor および SeekToCurrentErrorHandler は、失敗し続けるレコードを回復 (スキップ) できるようになりました。デフォルトでは、10 回の失敗後に回復します。失敗したレコードを配信不能トピックに公開するように構成できます。

バージョン 2.2.4 以降、デッドレターのトピック名を選択する際に、コンシューマーのグループ ID を使用できるようになりました。

ConsumerStoppingEvent が追加されました。詳細については、アプリケーションイベントを参照してください。

コンテナーが AckMode.MANUAL_IMMEDIATE で構成されている場合、SeekToCurrentErrorHandler は、復元されたレコードのオフセットをコミットするように構成できるようになりました (2.2.4 以降)。

D.8.6. @KafkaListener の変更

アノテーションにプロパティを設定することで、リスナーコンテナーファクトリの concurrency および autoStartup プロパティをオーバーライドできるようになりました。構成を追加して、応答メッセージにコピーするヘッダー (存在する場合) を決定できるようになりました。詳細については、@KafkaListener アノテーションを参照してください。

@KafkaListener を独自のアノテーションのメタアノテーションとして使用できるようになりました。詳細については、メタアノテーションとしての @KafkaListener  を参照してください。

@Payload 検証用に Validator を構成する方が簡単になりました。詳細については、@KafkaListener @Payload 検証を参照してください。

アノテーションで kafka コンシューマープロパティを直接指定できるようになりました。これらは、コンシューマーファクトリで定義された同じ名前のプロパティをオーバーライドします (バージョン 2.2.4 以降)。詳細については、アノテーションプロパティを参照してください。

D.8.7. ヘッダーマッピングの変更

型 MimeType および MediaType のヘッダーは、RecordHeader 値の単純な文字列としてマップされるようになりました。以前は、これらは JSON としてマッピングされ、MimeType のみがデコードされていました。MediaType をデコードできませんでした。これらは、相互運用性のための単純な文字列になりました。

また、DefaultKafkaHeaderMapper には新しい addToStringClasses メソッドがあり、JSON の代わりに toString() を使用してマップする必要のある型を指定できます。詳細については、メッセージヘッダーを参照してください。

D.8.8. 埋め込まれた Kafka の変更

KafkaEmbedded クラスとその KafkaRule インターフェースは非推奨となり、EmbeddedKafkaBroker とその JUnit 4 EmbeddedKafkaRule ラッパーが優先されました。@EmbeddedKafka アノテーションは、非推奨の KafkaEmbedded の代わりに EmbeddedKafkaBroker Bean を設定するようになりました。この変更により、JUnit 5 テストで @EmbeddedKafka を使用できるようになりました。@EmbeddedKafka アノテーションには、EmbeddedKafkaBroker を取り込むポートを指定する属性 ports が含まれるようになりました。詳細については、アプリケーションのテストを参照してください。

D.8.9. JsonSerializer/ デシリアライザーの機能強化

プロデューサーとコンシューマーのプロパティを使用して、型 マッピング情報を提供できるようになりました。

新しいコンストラクターがデシリアライザーで使用可能になり、提供されたターゲット型で型ヘッダー情報をオーバーライドできます。

JsonDeserializer は、デフォルトで型情報ヘッダーを削除するようになりました。

Kafka プロパティ (2.2.3 以降) を使用して、型情報ヘッダーを無視するように JsonDeserializer を構成できるようになりました。

詳細については、直列化、逆直列化、メッセージ変換を参照してください。

D.8.10. Kafka ストリームの変更

ストリーム構成 Bean は、StreamsConfig オブジェクトではなく KafkaStreamsConfiguration オブジェクトである必要があります。

StreamsBuilderFactoryBean は、パッケージ …​core から …​config に移動されました。

KafkaStreamBrancher は、条件付き ブランチが KStream インスタンス上に構築されている場合のエンドユーザーエクスペリエンスを向上させるために導入されました。

詳細については、Apache Kafka ストリームのサポートおよび構成を参照してください。

D.8.11. トランザクション ID

リスナーコンテナーによってトランザクションが開始されると、transactional.id は transactionIdPrefix に <group.id>.<topic>.<partition> が付加されます。この変更により、ここで説明するよう (英語) に、ゾンビの適切なフェンシングが可能になります。

D.9. 2.0 および 2.1 間の変更

D.9.1. Kafka クライアントバージョン

このバージョンには 1.0.0 kafka-clients 以降が必要です。

1.1.x クライアントは、バージョン 2.2 でネイティブにサポートされています。

D.9.2. JSON の改善

StringJsonMessageConverter および JsonSerializer は Headers に型情報を追加するようになり、コンバーターと JsonDeserializer は、固定された構成型ではなく、メッセージ自体に基づいて、受信時に特定の型を作成できるようになりました。詳細については、直列化、逆直列化、メッセージ変換を参照してください。

D.9.3. コンテナー停止エラーハンドラー

コンテナーエラーハンドラーは、レコードリスナーとバッチリスナーの両方に提供されるようになりました。リスナーによってスローされた例外を致命的なものとして処理します。/ コンテナーを停止します。詳細については、例外の処理を参照してください。

D.9.4. コンテナーの一時停止と再開

リスナーコンテナーに pause() および resume() メソッドが追加されました (バージョン 2.1.3 以降)。詳細については、リスナーコンテナーの一時停止と再開を参照してください。

D.9.5. ステートフルリトライ

バージョン 2.1.3 以降、ステートフル再試行を構成できます。詳細については、ステートフルリトライを参照してください。

D.9.6. クライアント ID

バージョン 2.1.1 以降、@KafkaListener に client.id プレフィックスを設定できるようになりました。以前は、クライアント ID をカスタマイズするには、リスナーごとに個別のコンシューマーファクトリ (およびコンテナーファクトリ) が必要でした。プレフィックスには -n のサフィックスが付いており、同時実行を使用する場合に一意のクライアント ID を提供します。

D.9.7. オフセットコミットのロギング

デフォルトでは、トピックオフセットコミットのロギングは DEBUG ロギングレベルで実行されます。バージョン 2.1.2 以降、commitLogLevel と呼ばれる ContainerProperties の新しいプロパティを使用して、これらのメッセージのログレベルを指定できます。詳細については、KafkaMessageListenerContainer の使用を参照してください。

D.9.8. デフォルトの @KafkaHandler

バージョン 2.1.3 以降、クラスレベルの @KafkaListener で @KafkaHandler アノテーションの 1 つをデフォルトとして指定できます。詳細については、クラスでの @KafkaListener  を参照してください。

D.9.9. ReplyingKafkaTemplate

バージョン 2.1.3 以降、リクエスト / 応答セマンティクスをサポートするために KafkaTemplate のサブクラスが提供されます。詳細については、ReplyingKafkaTemplate を使用するを参照してください。

D.9.10. ChainedKafkaTransactionManager

バージョン 2.1.3 は ChainedKafkaTransactionManager を導入しました。(現在は非推奨です)。

D.9.11. 2.0 からの移行ガイド

2.0 から 2.1 への移行 [GitHub] (英語) ガイドを参照してください。

D.10. 1.3 および 2.0 間の変更

D.10.1. Spring Framework および Java バージョン

Spring for Apache Kafka プロジェクトには、Spring Framework 5.0 および Java 8 が必要になりました。

D.10.2. @KafkaListener の変更

@KafkaListener メソッド (およびクラスと @KafkaHandler メソッド) に @SendTo でアノテーションを付けることができるようになりました。メソッドが結果を返す場合、指定されたトピックに転送されます。詳細については、@SendTo を使用したリスナー結果の転送を参照してください。

D.10.3. メッセージリスナー

メッセージリスナーは、Consumer オブジェクトを認識できるようになりました。詳細については、メッセージリスナーを参照してください。

D.10.4. ConsumerAwareRebalanceListener を使用する

リバランスリスナーは、リバランス通知中に Consumer オブジェクトにアクセスできるようになりました。詳細については、リスナーのリバランスを参照してください。

D.11. 1.2 および 1.3 間の変更

D.11.1. トランザクションのサポート

0.11.0.0 クライアントライブラリでは、トランザクションのサポートが追加されました。KafkaTransactionManager およびその他のトランザクションのサポートが追加されました。詳細については、"トランザクション" を参照してください。

D.11.2. ヘッダーのサポート

0.11.0.0 クライアントライブラリでは、メッセージヘッダーのサポートが追加されました。これらは、spring-messaging MessageHeaders との間でマッピングできるようになりました。詳細については、"メッセージヘッダー" を参照してください。

D.11.3. トピックの作成

0.11.0.0 クライアントライブラリは、トピックの作成に使用できる AdminClient を提供します。KafkaAdmin は、このクライアントを使用して、@Bean インスタンスとして定義されたトピックを自動的に追加します。

D.11.4. Kafka タイムスタンプのサポート

KafkaTemplate は、タイムスタンプ付きのレコードを追加する API をサポートするようになりました。timestamp のサポートに関して、新しい KafkaHeaders が導入されました。また、新しい KafkaConditions.timestamp() および KafkaMatchers.hasTimestamp() テストユーティリティが追加されました。詳細については、KafkaTemplate の使用@KafkaListener アノテーションアプリケーションのテストを参照してください。

D.11.5. @KafkaListener の変更

例外を処理するように KafkaListenerErrorHandler を構成できるようになりました。詳細については、例外の処理を参照してください。

デフォルトでは、@KafkaListener id プロパティが group.id プロパティとして使用され、コンシューマーファクトリで設定されたプロパティ (存在する場合) をオーバーライドします。さらに、アノテーションで groupId を明示的に構成できます。以前は、リスナーに異なる group.id 値を使用するには、個別のコンテナーファクトリ (およびコンシューマーファクトリ) が必要でした。提供時に構成された group.id を使用する以前の動作を復元するには、アノテーションの idIsGroup プロパティを false に設定します。

D.11.6. @EmbeddedKafka アノテーション

便宜上、KafkaEmbedded を Bean として登録するために、テストクラスレベルの @EmbeddedKafka アノテーションが提供されています。詳細については、アプリケーションのテストを参照してください。

D.11.7. Kerberos 構成

Kerberos の構成がサポートされるようになりました。詳細については、JAAS と Kerberos を参照してください。

D.12. 1.1 および 1.2 間の変更

このバージョンでは、0.10.2.x クライアントが使用されます。

D.13. 1.0 および 1.1 間の変更

D.13.1. Kafka クライアント

このバージョンでは、Apache Kafka 0.10.x.x クライアントを使用します。

D.13.2. バッチリスナー

リスナーは、一度に 1 つずつではなく、consumer.poll() 操作によって返されるメッセージのバッチ全体を受信するように構成できます。

D.13.3. null ペイロード

Null ペイロードは、ログ圧縮を使用するときにキーを「削除」するために使用されます。

D.13.4. 初期オフセット

パーティションを明示的に割り当てるときに、現在のエンドからの絶対または相対ではなく、コンシューマーグループの現在の位置に対して相対的な初期オフセットを構成できるようになりました。

D.13.5. シーク

各トピックまたはパーティションの位置をシークできるようになりました。グループ管理を使用していて、Kafka がパーティションを割り当てるときに、これを使用して初期化中に初期位置を設定できます。アイドル状態のコンテナーが検出されたとき、またはアプリケーションの実行中の任意の時点でシークすることもできます。詳細については、特定のオフセットを求めてを参照してください。