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);
}
结果如图所示,显示消费者接收到了消息。