Spring Boot 整合 Kafka:生产环境标准配置与最佳实践

一、环境准备

组件 版本 说明
Java 17+ Spring Boot 3.x 需 Java 17+
Apache Kafka 3.6.x 下载地址:Kafka 官网
Spring Boot 3.1.0+ 通过 Spring Initializr 生成项目
Maven/Gradle 最新 项目构建工具

启动 Kafka 服务(本地开发示例):

复制代码
# 启动 Zookeeper(Kafka 3.0+ 已内置)
bin/zookeeper-server-start.sh config/zookeeper.properties

# 启动 Kafka 服务
bin/kafka-server-start.sh config/server.properties

二、项目搭建(Maven 依赖)

1. 添加 Spring Kafka 依赖

复制代码
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>3.1.0</version>
</dependency>

💡 关键点 :Spring Boot 3.x 已移除 spring-boot-starter-kafka,直接使用 spring-kafka


三、YAML 配置(application.yml)

重点:YAML 配置替代 properties,支持对象序列化器配置

java 复制代码
spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: com.example.config.AttendanceSerializer # 自定义序列化器
      batch-size: 16384 # 批量发送优化
      linger-ms: 100 # 批量发送延迟
      retries: 3
      acks: all
    consumer:
      group-id: attendance-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: com.example.config.AttendanceDeserializer # 自定义反序列化器
      auto-offset-reset: earliest
      enable-auto-commit: true
      max-poll-records: 500
      isolation.level: read_committed
      # 事务配置(可选)
      transaction-id-prefix: attendance-trans

🌟 YAML 配置优势

  • 层级清晰,避免 . 重复
  • 支持嵌套配置(如 producer.batch-size
  • 与 Spring Boot 3.x 完美兼容

四、自定义序列化器(核心代码)

1. 定义消息实体(AttendanceStatisticsSingleDto)

java 复制代码
package jnpf.model.attendance.event;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AttendanceStatisticsSingleDto implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @NotBlank(message = "租户Id不能为空")
    private String tenantId;
    
    @NotBlank(message = "考勤组Id不能为空")
    private String groupId;
    
    @NotBlank(message = "用户Id不能为空")
    private String userId;
    
    @NotNull(message = "日期不能为空")
    private Date day;
}

2. 创建序列化器(AttendanceSerializer.java)

java 复制代码
package com.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.model.attendance.event.AttendanceStatisticsSingleDto;
import org.apache.kafka.common.serialization.Serializer;

import java.util.Map;

public class AttendanceSerializer implements Serializer<AttendanceStatisticsSingleDto> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        // 配置方法,无需额外操作
    }

    @Override
    public byte[] serialize(String topic, AttendanceStatisticsSingleDto data) {
        try {
            return objectMapper.writeValueAsBytes(data);
        } catch (Exception e) {
            throw new RuntimeException("序列化失败: " + data, e);
        }
    }

    @Override
    public void close() {
        // 无需关闭资源
    }
}

3. 创建反序列化器(AttendanceDeserializer.java)

java 复制代码
package com.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.model.attendance.event.AttendanceStatisticsSingleDto;
import org.apache.kafka.common.serialization.Deserializer;

import java.util.Map;

public class AttendanceDeserializer implements Deserializer<AttendanceStatisticsSingleDto> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        // 配置方法,无需额外操作
    }

    @Override
    public AttendanceStatisticsSingleDto deserialize(String topic, byte[] data) {
        try {
            return objectMapper.readValue(data, AttendanceStatisticsSingleDto.class);
        } catch (Exception e) {
            throw new RuntimeException("反序列化失败: " + topic, e);
        }
    }

    @Override
    public void close() {
        // 无需关闭资源
    }
}

💡 为什么需要自定义序列化器?

Spring Kafka 默认只支持 String/byte[],复杂对象必须通过序列化器转换为字节数组。


五、完整集成示例

1. 生产者服务(发送 AttendanceStatisticsSingleDto)

java 复制代码
package com.example.service;

import com.example.config.AttendanceSerializer;
import jnpf.model.attendance.event.AttendanceStatisticsSingleDto;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class AttendanceProducer {

    private final KafkaTemplate<String, AttendanceStatisticsSingleDto> kafkaTemplate;

    public AttendanceProducer(KafkaTemplate<String, AttendanceStatisticsSingleDto> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void sendAttendanceData(AttendanceStatisticsSingleDto attendance) {
        // 发送消息到主题 attendance-topic
        kafkaTemplate.send("attendance-topic", attendance.getUserId(), attendance);
        
        System.out.println("✅ 消息发送成功: " + attendance.getUserId() + " | " + attendance.getDay());
    }
}

2. 消费者服务(接收 AttendanceStatisticsSingleDto)

java 复制代码
package com.example.consumer;

import com.example.config.AttendanceDeserializer;
import jnpf.model.attendance.event.AttendanceStatisticsSingleDto;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;

@Service
public class AttendanceConsumer {

    @KafkaListener(
        topics = "attendance-topic",
        groupId = "attendance-group",
        containerFactory = "kafkaListenerContainerFactory"
    )
    public void listen(AttendanceStatisticsSingleDto attendance, 
                      Acknowledgment ack,
                      Headers headers) {
        
        // 获取 Kafka 元数据
        int partition = Integer.parseInt(headers.lastHeader("partition").value().toString());
        long offset = headers.lastHeader("offset").value().toString().getBytes()[0];
        
        System.out.println("✅ 消息消费成功 | 分区: " + partition + 
                          " | 偏移量: " + offset + 
                          " | 用户: " + attendance.getUserId());
        
        // 手动提交偏移量(可选)
        ack.acknowledge();
    }
}

3. 配置 Kafka 监听器容器工厂(关键!)

java 复制代码
package com.example.config;

import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.ContainerCustomizer;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.DeserializationException;

@Configuration
public class KafkaConfig {

    @Bean
    public ConsumerFactory<String, AttendanceStatisticsSingleDto> consumerFactory() {
        // 配置消费者工厂
        return new DefaultKafkaConsumerFactory<>(
            Map.of(
                "bootstrap.servers", "localhost:9092",
                "group.id", "attendance-group",
                "key.deserializer", StringDeserializer.class.getName(),
                "value.deserializer", AttendanceDeserializer.class.getName()
            )
        );
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, AttendanceStatisticsSingleDto> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, AttendanceStatisticsSingleDto> factory = 
            new ConcurrentKafkaListenerContainerFactory<>();
        
        factory.setConsumerFactory(consumerFactory());
        factory.setCommonErrorHandler(new DefaultErrorHandler(
            new FixedBackOff(1000L, 3), // 重试3次,间隔1秒
            new TopicPartitionOffset("attendance-topic.DLQ", 0) // 死信队列
        ));
        
        // 启用自动提交
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
        
        return factory;
    }
}

🌟 关键配置说明

  • setCommonErrorHandler:实现自动重试 + 死信队列
  • setAckMode(ContainerProperties.AckMode.MANUAL):手动提交偏移量(推荐)
  • AttendanceDeserializer:与生产者序列化器严格匹配

六、测试用例(JUnit 5)

java 复制代码
package com.example;

import com.example.service.AttendanceProducer;
import jnpf.model.attendance.event.AttendanceStatisticsSingleDto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;

@SpringBootTest
public class KafkaIntegrationTest {

    @Autowired
    private AttendanceProducer producer;

    @Test
    public void testSendAttendanceData() {
        AttendanceStatisticsSingleDto attendance = AttendanceStatisticsSingleDto.builder()
            .tenantId("tenant_001")
            .groupId("group_001")
            .userId("user_001")
            .day(new Date())
            .build();
        
        producer.sendAttendanceData(attendance);
        
        // 实际测试需等待消费者处理(此处简化)
        System.out.println("✅ 测试消息已发送");
    }
}

测试执行流程

  1. 启动 Kafka 服务
  2. 运行 Spring Boot 应用
  3. 执行测试用例
  4. 消费者控制台输出日志

七、最佳实践与配置优化

场景 配置 说明
批量发送优化 spring.kafka.producer.batch-size=16384 spring.kafka.producer.linger-ms=100 提升吞吐量 30%+
消费者线程控制 spring.kafka.consumer.max-poll-records=500 防止单次拉取过多消息
事务保障 spring.kafka.consumer.transaction-id-prefix=attendance-trans 确保消息与数据库操作原子性
死信队列 error-handler.dlq-topic=attendance-topic.DLQ 消费失败消息自动移至 DLQ
监控指标 micrometer.kafka.enabled=true 集成 Prometheus 监控

八、常见问题排查(YAML 配置特有)

问题 现象 解决方案
序列化器未生效 ClassCastException: String cannot be cast to AttendanceStatisticsSingleDto 检查 value-serializer 配置路径是否正确
YAML 缩进错误 Invalid configuration 严格使用 2 空格缩进(避免 Tab)
死信队列未创建 消息未进入 DLQ 手动创建主题:kafka-topics.sh --create --topic attendance-topic.DLQ --partitions 1 --replication-factor 1
日期格式异常 Invalid format for Date AttendanceStatisticsSingleDto 中添加 @JsonFormat 注解
消费者无法启动 No available broker 检查 bootstrap-servers 是否可访问

九、总结:Spring Boot + Kafka 核心流程

关键结论

  1. YAML 配置更清晰 :避免 properties 中的 spring.kafka.consumer.group-id 拼写错误

  2. 自定义序列化器是核心:必须与消息实体严格匹配

  3. 死信队列是生产必备:任何消费者都应配置 DLQ

  4. 手动提交偏移量 :避免消息丢失(AckMode.MANUAL
    💡 立即实践

  5. 创建 AttendanceStatisticsSingleDto 实体

  6. 实现 AttendanceSerializer/AttendanceDeserializer

  7. 配置 application.yml 中的序列化器路径

  8. 启动 Kafka + Spring Boot 服务

  9. 发送测试消息 → 查看消费者日志

相关推荐
宁酱醇1 小时前
ORACLE 练习1
java·开发语言
2501_941982051 小时前
Python开发:外部群消息自动回复
java·前端·数据库
qinaoaini1 小时前
Spring中Aware的用法以及实现
java·数据库·spring
康小庄2 小时前
Java自旋锁与读写锁
java·开发语言·spring boot·python·spring·intellij-idea
沙河板混2 小时前
@Mapper注解和@MapperScan注解
java·spring boot·spring
知识即是力量ol2 小时前
口语八股:MySQL 核心原理系列(一):索引篇
java·数据库·mysql·八股·索引·面试技巧
xifangge20252 小时前
[报错] SpringBoot 启动报错:Port 8080 was already in use 完美解决(Windows/Mac/Linux)
java·windows·spring boot·macos·错误解决
没有bug.的程序员2 小时前
容器网络深度探究:从 CNI 插件选型内核到 K8s 网络策略安全防护实战指南
java·网络·安全·kubernetes·k8s·cni·容器网络
野犬寒鸦2 小时前
缓存与数据库一致性的解决方案:实际项目开发可用
java·服务器·数据库·后端·缓存