本文介紹spring中使用Kafka的三種方式,其中container方式最靈活,但是開發相對較複雜,stream方式使用最簡便,listener方式由於提供的最早,使用的較普遍。 具體的代碼參照 示例項目 https://github.com/qihaiyan/springcamp/tree/master/spring-kafka
一、概述
在實際項目中,用到kafka的場景非常普遍,特別是事件驅動的編程模式,kafka基本是標配。
二、KafkaListener
KafkaListener應該是目前使用比較多的一種方式,開發簡單,易於理解。但是從易用性的角度,應該會逐步被spring-cloud-stream所替代。 KafkaListener是一個註解,在對應的方法上加上這個註解,方法就可以處理接收到的kakfa消息,註解中通過topics參數指定需要消費的kakfa的topic,topics參數支持SPEL表達式,可以同時消費多個kafka topic:
@KafkaListener(topics = "test-topic")
public void receive(ConsumerRecord<String, String> consumerRecord) {
this.payload = consumerRecord.value();
log.info("received payload='{}'", payload);
}
帶註解的方法的入參是一個ConsumerRecord變量,存放了kafka中接收到的消息。
同時需要對kafka進行配置,可以指定kakfa服務器的地址,以及序列化方式:
spring.kafka:
bootstrap-servers: 192.168.1.1:2181
consumer:
group-id: utgroup
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
三、ConcurrentMessageListenerContainer
通過ConcurrentMessageListenerContainer可以已可編程的方式來處理kafka消息,這種方式的好處是topic是在程序中指定的,這樣可以將topic的配置存貯在任何地方,比如數據庫中,也可以按照不同的條件分支指定不同的topic,非常靈活。這是其它兩種方式做不到的。
ConcurrentMessageListenerContainer的配置:
@Component
public class MessageListenerContainerConsumer {
public static final String LISTENER_CONTAINER_TOPIC = "container-topic";
public Set<String> consumedMessages = new HashSet<>();
@PostConstruct
void start() {
MessageListener<String, String> messageListener = record -> {
System.out.println("MessageListenerContainerConsumer received message: " + record.value());
consumedMessages.add(record.value());
};
ConcurrentMessageListenerContainer<String, String> container =
new ConcurrentMessageListenerContainer<>(
consumerFactory(),
containerProperties(LISTENER_CONTAINER_TOPIC, messageListener));
container.start();
}
private DefaultKafkaConsumerFactory<String, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(
new HashMap<String, Object>() {
{
put(BOOTSTRAP_SERVERS_CONFIG, System.getProperty("spring.kafka.bootstrap-servers"));
put(GROUP_ID_CONFIG, "groupId");
put(AUTO_OFFSET_RESET_CONFIG, "earliest");
}
},
new StringDeserializer(),
new StringDeserializer());
}
private ContainerProperties containerProperties(String topic, MessageListener<String, String> messageListener) {
ContainerProperties containerProperties = new ContainerProperties(topic);
containerProperties.setMessageListener(messageListener);
return containerProperties;
}
}
以上代碼定義了MessageListenerContainerConsumer這個類,是一個spring的bean,在PostConstruct這個bean的初始化代碼中,我們使用了ConcurrentMessageListenerContainer,並指定了topic public static final String LISTENER_CONTAINER_TOPIC = "container-topic"
,在這個爲了便於演示我們使用了一個常量,實際上這個topic的值可以是任意變量,可以從數據庫中讀取,也可以通過實際的場景動態計算,這樣就做到了topic的靈活配置。
四、spring-cloud-stream
spring-cloud-stream是springcloud的一個子項目,這個項目的目標是一個事件驅動(Event-Driven)的編程框架。spring-cloud-stream對kafka進行了非常好的抽象,除了kakfa,還支持RabbitMQ,程序中除了配置文件外,完全看不到kafka的痕跡,意味着我們在開發的時候不需要關心底層的kakfa的細節,如果像從kafka切換到RabbitMQ,只需要修改一下引入的jar包和配置文件。
詳細介紹見spring的官方文檔: https://spring.io/projects/spring-cloud-stream
spring-cloud-stream是用了spring-cloud-function,我們只需要在程序中實現一個function接口,就可以處理kafka消息,編寫非常簡單。
i@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public Function<String, Object> handle() {
return String::toUpperCase;
}
}
除了配置文件外,代碼中只需要一行代碼public Function<String, Object> handle()
就實現了kafka消息的處理,這行代碼完全看不出跟kakfa有什麼關係,就是一個普通的方法,把抽象做到了極致。 需要注意方法的名字於配置文件中的名字要匹配。
配置:
spring:
cloud.stream:
bindings:
handle-in-0:
destination: testEmbeddedIn
content-type: text/plain
group: utgroup
handle-out-0:
destination: testEmbeddedOut
kafka:
binder:
brokers: 192.168.1.1:2181
configuration:
key.serializer: org.apache.kafka.common.serialization.ByteArraySerializer
value.serializer: org.apache.kafka.common.serialization.ByteArraySerializer
注意配置文件中 handle-in-0
和 handle-out-0
這2行配置,handle指的就是前面代碼中的public Function<String, Object> handle()
handle這個方法。spring-cloud-stream就是通過方法名和配置文件中配置項的名字,來確立代碼和配置的匹配關係,這也是約定優於配置的編程思想的體現。