Springを使ったApache Kafkaの紹介

SpringでのApache Kafkaの紹介

1. 概要

Apache Kafkaは、分散型でフォールトトレラントなストリーム処理システムです。

この記事では、KafkaのSpringサポートと、ネイティブのKafkaJavaクライアントAPIに対して提供される抽象化のレベルについて説明します。

Spring Kafkaは、KafkaTemplateおよび@KafkaListenerアノテーションを介したメッセージ駆動型POJOを備えたシンプルで典型的なSpringテンプレートプログラミングモデルをもたらします。

参考文献:

FlinkとKafkaでストリームデータを処理する方法を学ぶ

MQTTおよびMongoDBを使用したKafka Connectの例

Kafkaコネクタを使用した実用的な例をご覧ください。

2. インストールとセットアップ

Kafkaをダウンロードしてインストールするには、公式ガイドhereを参照してください。

また、pom.xml:spring-kafka依存関係を追加する必要があります


    org.springframework.kafka
    spring-kafka
    2.2.7.RELEASE

このアーティファクトの最新バージョンはhereにあります。

サンプルアプリケーションは、Spring Bootアプリケーションになります。

この記事では、サーバーがデフォルト構成を使用して起動され、サーバーポートが変更されていないことを前提としています。

3. トピックの構成

以前は、コマンドラインツールを実行してKafkaで次のようなトピックを作成していました。

$ bin/kafka-topics.sh --create \
  --zookeeper localhost:2181 \
  --replication-factor 1 --partitions 1 \
  --topic mytopic

しかし、KafkaにAdminClientが導入されたことで、プログラムでトピックを作成できるようになりました。

KafkaAdmin Spring Beanを追加する必要があります。これにより、タイプNewTopic:のすべてのBeanのトピックが自動的に追加されます。

@Configuration
public class KafkaTopicConfig {

    @Value(value = "${kafka.bootstrapAddress}")
    private String bootstrapAddress;

    @Bean
    public KafkaAdmin kafkaAdmin() {
        Map configs = new HashMap<>();
        configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        return new KafkaAdmin(configs);
    }

    @Bean
    public NewTopic topic1() {
         return new NewTopic("example", 1, (short) 1);
    }
}

4. メッセージの作成

メッセージを作成するには、最初に、KafkaProducerインスタンスを作成するための戦略を設定するProducerFactoryを構成する必要があります。

次に、Producerインスタンスをラップし、Kafkaトピックにメッセージを送信するための便利なメソッドを提供するKafkaTemplateが必要です。

Producerインスタンスはスレッドセーフであるため、アプリケーションコンテキスト全体で単一のインスタンスを使用すると、パフォーマンスが向上します。 したがって、KakfaTemplateインスタンスもスレッドセーフであり、1つのインスタンスを使用することをお勧めします。

4.1. プロデューサー構成

@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory producerFactory() {
        Map configProps = new HashMap<>();
        configProps.put(
          ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
          bootstrapAddress);
        configProps.put(
          ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
          StringSerializer.class);
        configProps.put(
          ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
          StringSerializer.class);
        return new DefaultKafkaProducerFactory<>(configProps);
    }

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

4.2. メッセージの公開

KafkaTemplateクラスを使用してメッセージを送信できます。

@Autowired
private KafkaTemplate kafkaTemplate;

public void sendMessage(String msg) {
    kafkaTemplate.send(topicName, msg);
}

The send API returns a ListenableFuture object.送信スレッドをブロックし、送信されたメッセージに関する結果を取得する場合は、ListenableFutureオブジェクトのget APIを呼び出すことができます。 スレッドは結果を待機しますが、プロデューサーの速度が低下します。

Kafkaは、高速ストリーム処理プラットフォームです。 したがって、結果を非同期で処理して、後続のメッセージが前のメッセージの結果を待たないようにすることをお勧めします。 コールバックを介してこれを行うことができます。

public void sendMessage(String message) {

    ListenableFuture> future =
      kafkaTemplate.send(topicName, message);

    future.addCallback(new ListenableFutureCallback>() {

        @Override
        public void onSuccess(SendResult result) {
            System.out.println("Sent message=[" + message +
              "] with offset=[" + result.getRecordMetadata().offset() + "]");
        }
        @Override
        public void onFailure(Throwable ex) {
            System.out.println("Unable to send message=["
              + message + "] due to : " + ex.getMessage());
        }
    });
}

5. メッセージの消費

5.1. 消費者構成

メッセージを消費するには、ConsumerFactoryKafkaListenerContainerFactoryを構成する必要があります。 これらのBeanがSpringBeanファクトリで使用可能になると、@KafkaListenerアノテーションを使用してPOJOベースのコンシューマーを構成できます。

SpringマネージドBeanで@KafkaListenerアノテーションを検出できるようにするには、構成クラスで@EnableKafkaアノテーションが必要です。

@EnableKafka
@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory consumerFactory() {
        Map props = new HashMap<>();
        props.put(
          ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
          bootstrapAddress);
        props.put(
          ConsumerConfig.GROUP_ID_CONFIG,
          groupId);
        props.put(
          ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
          StringDeserializer.class);
        props.put(
          ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
          StringDeserializer.class);
        return new DefaultKafkaConsumerFactory<>(props);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory
      kafkaListenerContainerFactory() {

        ConcurrentKafkaListenerContainerFactory factory =
          new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
}

5.2. メッセージの消費

@KafkaListener(topics = "topicName", groupId = "foo")
public void listen(String message) {
    System.out.println("Received Messasge in group foo: " + message);
}

Multiple listeners can be implemented for a topic、それぞれ異なるグループIDを持ちます。 さらに、ある消費者はさまざまなトピックからのメッセージを聞くことができます。

@KafkaListener(topics = "topic1, topic2", groupId = "foo")

Springは、リスナーで@Headerアノテーションを使用した1つ以上のメッセージヘッダーの取得もサポートしています。

@KafkaListener(topics = "topicName")
public void listenWithHeaders(
  @Payload String message,
  @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
      System.out.println(
        "Received Message: " + message"
        + "from partition: " + partition);
}

5.3. 特定のパーティションからのメッセージの消費

お気づきかもしれませんが、1つのパーティションのみでトピックexampleを作成しました。 ただし、複数のパーティションを持つトピックの場合、@KafkaListenerは、初期オフセットを使用してトピックの特定のパーティションに明示的にサブスクライブできます。

@KafkaListener(
  topicPartitions = @TopicPartition(topic = "topicName",
  partitionOffsets = {
    @PartitionOffset(partition = "0", initialOffset = "0"),
    @PartitionOffset(partition = "3", initialOffset = "0")
}))
public void listenToParition(
  @Payload String message,
  @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
      System.out.println(
        "Received Messasge: " + message"
        + "from partition: " + partition);
}

このリスナーではinitialOffsetが0に送信されているため、このリスナーが初期化されるたびに、パーティション0および3から以前に消費されたすべてのメッセージが再消費されます。 オフセットを設定する必要がない場合は、@TopicPartitionアノテーションのpartitionsプロパティを使用して、オフセットのないパーティションのみを設定できます。

@KafkaListener(topicPartitions
  = @TopicPartition(topic = "topicName", partitions = { "0", "1" }))

5.4. リスナー用のメッセージフィルターの追加

リスナーは、カスタムフィルターを追加することにより、特定の種類のメッセージを消費するように構成できます。 これは、RecordFilterStrategyKafkaListenerContainerFactoryに設定することで実行できます。

@Bean
public ConcurrentKafkaListenerContainerFactory
  filterKafkaListenerContainerFactory() {

    ConcurrentKafkaListenerContainerFactory factory =
      new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setRecordFilterStrategy(
      record -> record.value().contains("World"));
    return factory;
}

次に、このコンテナファクトリを使用するようにリスナーを設定できます。

@KafkaListener(
  topics = "topicName",
  containerFactory = "filterKafkaListenerContainerFactory")
public void listen(String message) {
    // handle message
}

このリスナーでは、すべてのmessages matching the filter will be discarded

6. カスタムメッセージコンバーター

これまでのところ、文字列をメッセージとして送受信する方法のみを扱ってきました。 ただし、カスタムJavaオブジェクトを送受信することもできます。 これには、ProducerFactoryで適切なシリアライザーを構成し、ConsumerFactoryでデシリアライザーを構成する必要があります。

メッセージとして送信する単純なBeanクラス,を見てみましょう。

public class Greeting {

    private String msg;
    private String name;

    // standard getters, setters and constructor
}

6.1. カスタムメッセージの作成

この例では、JsonSerializerを使用します。 ProducerFactoryKafkaTemplateのコードを見てみましょう。

@Bean
public ProducerFactory greetingProducerFactory() {
    // ...
    configProps.put(
      ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      JsonSerializer.class);
    return new DefaultKafkaProducerFactory<>(configProps);
}

@Bean
public KafkaTemplate greetingKafkaTemplate() {
    return new KafkaTemplate<>(greetingProducerFactory());
}

この新しいKafkaTemplateは、Greetingメッセージを送信するために使用できます。

kafkaTemplate.send(topicName, new Greeting("Hello", "World"));

6.2. カスタムメッセージの消費

同様に、ConsumerFactoryKafkaListenerContainerFactoryを変更して、Greetingメッセージを正しく逆シリアル化します。

@Bean
public ConsumerFactory greetingConsumerFactory() {
    // ...
    return new DefaultKafkaConsumerFactory<>(
      props,
      new StringDeserializer(),
      new JsonDeserializer<>(Greeting.class));
}

@Bean
public ConcurrentKafkaListenerContainerFactory
  greetingKafkaListenerContainerFactory() {

    ConcurrentKafkaListenerContainerFactory factory =
      new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(greetingConsumerFactory());
    return factory;
}

spring-kafka JSONシリアライザーおよびデシリアライザーはJacksonライブラリーを使用します。これは、spring-kafkaプロジェクトのオプションのMaven依存関係でもあります。 それでは、それをpom.xmlに追加しましょう。


    com.fasterxml.jackson.core
    jackson-databind
    2.9.7

ジャクソンの最新バージョンを使用する代わりに、spring-kafkaのpom.xmlに追加されたバージョンを使用することをお勧めします。

最後に、Greetingメッセージを消費するリスナーを作成する必要があります。

@KafkaListener(
  topics = "topicName",
  containerFactory = "greetingKafkaListenerContainerFactory")
public void greetingListener(Greeting greeting) {
    // process greeting message
}

7. 結論

この記事では、Apache KafkaのSpringサポートの基本について説明しました。 メッセージの送受信に使用されるクラスについて簡単に説明しました。

この記事の完全なソースコードはover on GitHubにあります。 コードを実行する前に、Kafkaサーバーが実行中であり、トピックが手動で作成されていることを確認してください。