1. 引言
Apache Kafka 是一個分散式的、容錯的流處理系統。在本文中,我們將介紹Spring對Apache Kafka的支援,以及原生Kafka Java客戶端Api 所提供的抽象級別。
Spring Kafka 通過 @KafkaListener 註解,帶來了一個簡單而典型的 Spring 模板程式設計模型,它還帶有一個 KafkaTemplate 和訊息驅動的 POJO 。
2. 安裝和設定
要下載和安裝Kafka,請參考官方指南。然後還需要在 pom.xml
檔案中新增 spring-kafka
:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
新建一個 Spring Boot 示例應用程式,以預設配置啟動。
3. 配置 Topics
以前我們使用命令列工具在 Kafka
中建立 topic
,例如:
$ bin/kafka-topics.sh --create \
--zookeeper localhost:2181 \
--replication-factor 1 --partitions 1 \
--topic mytopic
但是隨著 AdminClient 在Kafka中的引入,我們現在可以通過程式設計來建立 Topic
。
如下程式碼,新增 KafkAdmin
bean 到 Spring中,它將自動為 NewTopic
類的所有 bean
新增 topic
:
@Configuration
public class KafkaTopicConfig {
@Value(value = "${kafka.bootstrapAddress}")
private String bootstrapAddress;
@Bean
public KafkaAdmin kafkaAdmin() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
return new KafkaAdmin(configs);
}
@Bean
public NewTopic topic1() {
return new NewTopic("developlee", 1, (short) 1);
}
}
4. 訊息生成
要建立訊息,首先需要配置 ProducerFactory ,並設定建立 Kafka Producer 例項的策略,然後使用 KafkaTemplate
。 KafkaTemplate
包裝了 Producer
例項,並提供向 Kafka Topic
傳送訊息的簡便方法。
在整個應用程式上下文中使用單個例項將提供更高的效能。因此推薦使用一個 Producer
例項。該例項是執行緒安全的,所以 KakfaTemplate
例項也是執行緒安全的,
4.1. Producer 配置
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> 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<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
4.2. 訊息釋出
我們使用 KafkaTemplate
來發布訊息:
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void sendMessage(String msg) {
kafkaTemplate.send(topicName, msg);
}
send
API 返回 ListenableFuture
物件。如果我們想阻塞傳送執行緒並獲得關於傳送訊息的結果,我們可以呼叫ListenableFuture
物件的 get
API。執行緒將會等待結果,但它會降低生產者的速度。
Kafka是一個快速流處理平臺。因此,最好非同步處理結果,這樣後續訊息就無需等待前一條訊息的結果。我們可以通過回撥來實現:
public void sendMessage(String message) {
ListenableFuture<SendResult<String, String>> future =
kafkaTemplate.send(topicName, message);
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onSuccess(SendResult<String, String> 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在Spring Bean工廠中可用,就可以使用 @KafkaListener
註解配置基於POJO的消費者。
配置類上需要新增 @EnableKafka
註解,以便能夠檢測Spring
管理的bean上的 @KafkaListener
註解:
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> 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<String, String>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
5.2. 訊息消費
@KafkaListener(topics = "topicName", groupId = "foo")
public void listenGroupFoo(String message) {
System.out.println("Received Message in group foo: " + message);
}
可以為一個 topic 實現多個 listener,每個topic 都有不同的組Id。此外,一個消費者可以監聽來自不同 topic 的訊息:
@KafkaListener(topics = "topic1, topic2", groupId = "foo")
Spring 還支援使用 listener 中的 @Header 註解檢索一個或多個訊息標題:
@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. 消費來自特定分割槽的訊息
注意到,我們只使用一個分割槽建立了 topic “developlee”。但是,對於具有多個分割槽的主題,@KafkaListener 可以顯式訂閱具有初始偏移量 topic 的特定分割槽:
@KafkaListener(
topicPartitions = @TopicPartition(topic = "topicName",
partitionOffsets = {
@PartitionOffset(partition = "0", initialOffset = "0"),
@PartitionOffset(partition = "3", initialOffset = "0")}),
containerFactory = "partitionsKafkaListenerContainerFactory")
public void listenToPartition(
@Payload String message,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
System.out.println(
"Received Message: " + message"
+ "from partition: " + partition);
}
由於 initialOffset 已被髮送到該 listener 中的分割槽0,因此每次初始化該 listener
時,將重新使用以前從分割槽0和分割槽3消耗的所有訊息。如果不需要設定偏移量,我們可以使用 @TopicPartition 註解的 partitions 屬性只設定沒有偏移量的分割槽:
@KafkaListener(topicPartitions
= @TopicPartition(topic = "topicName", partitions = { "0", "1" }))
5.4. 為Listener新增訊息過濾器
通過新增自定義過濾器,可以將 listener
配置為使用特定型別的訊息。這可以通過將 RecordFilterStrategy
設定為 KafkaListenerContainerFactory
來完成:
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String>
filterKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setRecordFilterStrategy(
record -> record.value().contains("World"));
return factory;
}
然後可以將 listener
配置為使用此容器工廠:
@KafkaListener(
topics = "topicName",
containerFactory = "filterKafkaListenerContainerFactory")
public void listenWithFilter(String message) {
System.out.println("Received Message in filtered listener: " + message);
}
在這個 listener
中,所有與過濾器匹配的訊息都將被丟棄。
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<String, Greeting> greetingProducerFactory() {
// ...
configProps.put(
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, Greeting> greetingKafkaTemplate() {
return new KafkaTemplate<>(greetingProducerFactory());
}
新的 KafkaTemplate
可用於傳送 Greeting 訊息:
kafkaTemplate.send(topicName, new Greeting("Hello", "World"));
6.2. 消費自定義訊息
同樣,我們修改 ConsumerFactory
和 KafkaListenerContainerFactory
來正確反序列化 Greeting 訊息:
@Bean
public ConsumerFactory<String, Greeting> greetingConsumerFactory() {
// ...
return new DefaultKafkaConsumerFactory<>(
props,
new StringDeserializer(),
new JsonDeserializer<>(Greeting.class));
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Greeting>
greetingKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Greeting> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(greetingConsumerFactory());
return factory;
}
spring-kafka
JSON序列化器和反序列化器使用 Jackson 庫,該庫是 spring-kafka
專案的可選maven依賴項。我們也把它加到 pom.xml 檔案:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
建議不要使用 Jackson 的最新版本,而是使用 pom.xml 檔案 中 spring-kafka
的版本。
最後,我們需要編寫一個 listener 來 消費 Greeting 訊息:
@KafkaListener(
topics = "topicName",
containerFactory = "greetingKafkaListenerContainerFactory")
public void greetingListener(Greeting greeting) {
// process greeting message
}
7. 結語
在本文中,我們介紹了Apache Kafka 和 Spring 整合的基礎知識,且簡要介紹了用於傳送和接收訊息的類。
本文的完整原始碼可以在GitHub上找到. 在執行程式碼之前,請確保伺服器正在執行 Kafka。
如果你覺得文章還不錯,記得關注公眾號: 鍋外的大佬
劉一手的部落格