SpringでのApache Kafkaの紹介
1. 概要
Apache Kafkaは、分散型でフォールトトレラントなストリーム処理システムです。
この記事では、KafkaのSpringサポートと、ネイティブのKafkaJavaクライアントAPIに対して提供される抽象化のレベルについて説明します。
Spring Kafkaは、KafkaTemplateおよび@KafkaListenerアノテーションを介したメッセージ駆動型POJOを備えたシンプルで典型的なSpringテンプレートプログラミングモデルをもたらします。
参考文献:
2. インストールとセットアップ
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. 消費者構成
メッセージを消費するには、ConsumerFactoryとKafkaListenerContainerFactoryを構成する必要があります。 これらの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. リスナー用のメッセージフィルターの追加
リスナーは、カスタムフィルターを追加することにより、特定の種類のメッセージを消費するように構成できます。 これは、RecordFilterStrategyをKafkaListenerContainerFactoryに設定することで実行できます。
@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を使用します。 ProducerFactoryとKafkaTemplateのコードを見てみましょう。
@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. カスタムメッセージの消費
同様に、ConsumerFactoryとKafkaListenerContainerFactoryを変更して、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サーバーが実行中であり、トピックが手動で作成されていることを確認してください。