Spring Kafka示例 & 监听超时问题

58 阅读3分钟

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,添加KafkaAdminSpring Bean,它将自动为所有类型为NewTopicBean添加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);
    }
}

配置生产者

创建ProducerFactoryBean,它将设定创建 Kafka Producer实例的策略。

接着创建KafkaTemplateBean,它封装了一个 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宿主机启动的。所以我配置了listeners0.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项目启动后能够持续挂起并监听,在Applicationmain方法最后添加一段休眠的代码:

while (true) {
    Thread.sleep(Long.MAX_VALUE);
}

结果如图所示,显示消费者接收到了消息。

参考

在 Spring 应用中整合 Apache Kafka 以生产、消费消息 - spring 中文网