Spring Kafka示例 & 监听超时问题
前言
在Spring
项目中使用Spring Kafka
工具连接Kafka Broker
,在启动时遇到监听超时问题,特此记录一下以免再次入坑。
创建Spring项目
浏览器访问 https://start.spring.io/
初始化一个 Spring
项目,引入Spring Kafka
依赖。
将下载后的项目压缩包解压,在idea
中打开该项目,此时项目中除了Pom.xml
配置文件、application.properties
配置文件以及Application
启动类,项目还是空空如也。
Pom.xml
文件引入了Spring Kafka
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Kafka配置
配置Kafka地址
application.properties
配置文件添加
#kafka配置advertised.listeners的ip和端口
spring.kafka.bootstrap-servers=localhost:9092
配置 Topic
使用编程方式创建Kafka Topic
,添加KafkaAdmin
到Spring Bean
,它将自动为所有类型为NewTopic
的Bean
添加Topic
。
如果不需要创建Topic
,此类可以省略。
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaAdmin;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaTopicConfig {
@Value(value = "${spring.kafka.bootstrap-servers}")
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("test-topic", 1, (short) 1);
}
}
配置生产者
创建ProducerFactory
的Bean
,它将设定创建 Kafka Producer
实例的策略。
接着创建KafkaTemplate
的Bean
,它封装了一个 Producer
实例,并提供向 Kafka topic
发送消息的便捷方法。
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaProducerConfig {
@Value(value = "${spring.kafka.bootstrap-servers}")
private String bootstrapAddress;
@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());
}
}
发送消息服务类
创建一个KafkaService
工具类,kafkaTemplate.send
方法会返回一个 ListenableFuture
,为了不阻塞发送线程,可以使用whenComplete
这种异步方式处理结果。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFuture;
import java.util.concurrent.CompletableFuture;
@Service
public class KafkaService {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void sendMessage(String topicName, String msg) {
kafkaTemplate.send(topicName, msg);
}
public void sendMessageWhenComplete(String topicName, String message) {
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topicName, message);
CompletableFuture<SendResult<String, String>> completableFuture = future.completable();
completableFuture.whenComplete((result, ex) -> {
if (ex == null) {
System.out.println("Sent message=[" + message +
"] with offset=[" + result.getRecordMetadata().offset() + "]");
} else {
System.out.println("Unable to send message=[" +
message + "] due to : " + ex.getMessage());
}
});
}
}
配置消费者
application.properties
配置文件添加
spring.kafka.consumer.group-id=my-group
编程方式添加消费者配置,我们需要配置一个 ConsumerFactory
和一个 KafkaListenerContainerFactory
。一旦 Spring Bean Factory
中的这些 Bean 可用,就可以使用 @KafkaListener
注解配置基于 POJO 的消费者。
配置类上需要使用 @EnableKafka
注解,以便在 Spring
管理的 Bean
上检测 @KafkaListener
注解:
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import java.util.HashMap;
import java.util.Map;
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Value(value = "${spring.kafka.bootstrap-servers}")
private String bootstrapAddress;
@Value("${spring.kafka.consumer.group-id}")
private String groupId;
@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;
}
}
发送消息示例
在启动类中利用ConfigurableApplicationContext
获取KafkaService
对象,调用其sendMessage
方法。
import com.example.kafkademo.service.KafkaService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class KafkademoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(KafkademoApplication.class, args);
System.out.println("KafkaDemoApplication started");
System.out.println("Send Kafka Message started");
KafkaService kafkaService = context.getBean(KafkaService.class);
kafkaService.sendMessage("test-topic", "hello spring kafka");
System.out.println("Send Kafka Message end");
}
}
启动日志如图所示
提示Broker
可能未被创建,不可获得。
Connection to node -1 (localhost/127.0.0.1:9092) could not be established. Broker may not be available.
错误原因
Kafka
配置Listener
有误,我的server.properties
配置文件如下:
listeners=PLAINTEXT://0.0.0.0:9092
advertised.listeners=PLAINTEXT://localhost:9092
首先说下我的Kafka
是配置在我的windows
系统的子系统wsl
里,而spring kafka
项目是在windows
宿主机启动的。所以我配置了listeners
为0.0.0.0
。
listeners
表示Kafka
监听的网络,0.0.0.0
表示 Kafka
接受来自任何 IP
的连接。
advertised.listeners
配置的是Kafka
暴露出去的地址,如果listeners
没有配置0.0.0.0
,那么advertised.listeners
是不需要配置的;否则,advertised.listeners
需要配置实际的IP
,如果配置成localhost
,则会造成客户端访问的是自己的localhost
代表的IP
地址。
所以,我在Spring
项目中使用localhost:9092
访问Kafka
超时,是因为该localhost
代表的是我的windows
宿主机的地址,而不是Kafka
实际部署的wsl
系统的地址。
解决方案
advertised.listeners
改成实际地址。
advertised.listeners=PLAINTEXT://172.19.72.190:9092
Spring
项目配置改成advertised.listeners
配置的外部地址。
spring.kafka.bootstrap-servers=172.19.72.190:9092
成功运行
在wsl
系统里创建消息监听,此时因为Kafka
客户端和服务端在一个网络,所以可以使用localhost:9092
。
bin/kafka-console-consumer.sh --topic test-topic --from-beginning --bootstrap-server localhost:9092
启动Spring Kafka
项目,查看命令行,发现收到了消息。
消费消息示例
添加消费者监听
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class KafkaConsumer {
@KafkaListener(topics = "test-topic", groupId = "my-group")
public void listenGroup(String message) {
System.out.println("Received Message in group my-group: " + message);
}
}
为了让Spring Kafka
项目启动后能够持续挂起并监听,在Application
的main
方法最后添加一段休眠的代码:
while (true) {
Thread.sleep(Long.MAX_VALUE);
}
结果如图所示,显示消费者接收到了消息。