加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc...
👉 Kafka Producer 端(生产者)配置
👉 Kafka Consumer 端(消费者)配置
区分了不同的消费模式 (批量消费、单条消费),并且生产者也是分别独立管理。
🛠️ 流程步骤概览
阶段 | 内容 |
---|---|
配置阶段 | 通过 @Configuration 注解注册生产者、消费者配置。 |
属性注入 | 通过 @Value 注解注入 application.yml 或 application.properties 中的 Kafka 配置。 |
Bean 定义 | 生产者定义 KafkaTemplate ,消费者定义 KafkaListenerContainerFactory (分批量和单条消费两种)。 |
使用阶段 | 在业务代码中注入对应的 KafkaTemplate 发送消息,注解 @KafkaListener 配合不同的 ContainerFactory 接收消息。 |
📈 数据结构变化过程
阶段 | 数据结构 | 变化说明 |
---|---|---|
生产者发送前 | Java对象/String | Java对象通常先转为 String(通过序列化器) |
传输到 Kafka | byte[] | Kafka 内部只认识字节数组 |
消费者接收 | byte[] → String | 消费者接收到字节流,反序列化回 String |
消费逻辑处理 | Java对象/String | 再根据业务场景反序列化成 Java 对象处理 |
📚 示例操作流程(完整链路)
1. 启动阶段
- Spring Boot 扫描到
@Configuration
,注册 KafkaTemplate 和 KafkaListenerContainerFactory。
2. 发送消息(生产者)
less
@Autowired
@Qualifier("kafkaTemplate")
private KafkaTemplate<String, String> kafkaTemplate;
public void sendMessage(String topic, String message) {
kafkaTemplate.send(topic, message);
}
3. 消费消息(消费者)
typescript
@KafkaListener(topics = "test-topic", containerFactory = "kafkaBRConsumerFactory")
public void listenBatch(List<ConsumerRecord<String, String>> records) {
for (ConsumerRecord<String, String> record : records) {
System.out.println("Received message: " + record.value());
}
}
// 或者单条消费
@KafkaListener(topics = "test-topic", containerFactory = "kafkaOBORConsumerFactory")
public void listenSingle(ConsumerRecord<String, String> record) {
System.out.println("Received single message: " + record.value());
}
- 生产者和消费者配置完全隔离,职责清晰。
- 支持批量消费 (性能高)和逐条消费(处理更精细)两种模式。
- KafkaTemplate 统一封装消息发送,内部使用的是 ProducerFactory。
- KafkaListenerContainerFactory 统一封装消息监听,内部使用的是 ConsumerFactory。
- 生产消费流程:Java对象 → Kafka字节流传输 → Java对象。
📄 1. application.yml
Kafka配置模板
yaml
kafka:
producer:
servers: 127.0.0.1:9092
retries: 3 # 发送失败重试次数
batch:
size: 16384 # 批量发送最大字节数
linger: 1 # 等待时间(ms)
buffer:
memory: 33554432 # 32MB缓冲区大小
consumer:
servers: 127.0.0.1:9092
enable:
auto:
commit: false # 是否自动提交offset
session:
timeout: 30000 # session超时时间
auto:
commit:
interval: 1000 # 自动提交offset时间间隔
group:
id: my-consumer-group
auto:
offset:
reset: latest # 最新位置消费(可选 earliest)
concurrency: 3 # 并发线程数
注意:
-
producer.servers
和consumer.servers
通常是一样的(Kafka 集群地址)。 -
auto.offset.reset
:latest
:只消费启动后新的消息earliest
:从最早可用的消息开始消费(适合首次启动的消费者)
🚀 2. 消费者端:加上消息重试机制
在你的消费者容器工厂上,加个 SeekToCurrentErrorHandler
,遇到异常能自动重试N次(否则Kafka默认抛出异常整个消费线程就挂掉了!)
dart
private KafkaListenerContainerFactory<?> getConsumerFactory(boolean batchListener) {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(concurrency);
factory.setBatchListener(batchListener);
// 消息处理异常时重试配置(重要)
factory.setErrorHandler(new SeekToCurrentErrorHandler(
new FixedBackOff(5000L, 3) // 5秒重试一次,总共重试3次
));
return factory;
}
细节说明:
SeekToCurrentErrorHandler
会把这条消息重新投到消费队列中,不会丢失。FixedBackOff(5000L, 3)
:表示每隔5秒重试一次 ,重试3次后如果还失败就记录error日志。
如果你想更极致一点,比如:重试 N 次还失败就发到 死信队列(DLQ) ,也可以配!
⚡ 3. 生产者端:Kafka发送消息异步带回调
别直接 kafkaTemplate.send()
完就不管了,加一个Callback回调,拿到真正的发送成功 or 失败,做链路监控。
补充一下发送方法:
less
@Autowired
@Qualifier("kafkaTemplate")
private KafkaTemplate<String, String> kafkaTemplate;
public void sendMessageWithCallback(String topic, String message) {
kafkaTemplate.send(topic, message).addCallback(
success -> {
if (success != null) {
System.out.println("发送成功!topic:" + success.getRecordMetadata().topic() +
" partition:" + success.getRecordMetadata().partition() +
" offset:" + success.getRecordMetadata().offset());
}
},
failure -> {
System.err.println("发送失败!原因:" + failure.getMessage());
}
);
}
说明:
success
可以拿到:topic、partition、offset,链路追踪好用!failure
可以拿到异常信息,比如超时、网络问题、队列满等。
✅ 总结(目前你的Kafka模块)
组件 | 细节 |
---|---|
Producer | 配置 ProducerFactory、KafkaTemplate,支持异步Callback |
Consumer | 配置 ConsumerFactory、ListenerContainerFactory,支持批量、逐条消费、异常重试 |
配置文件 | application.yml统一管理 |
错误处理 | 加了 SeekToCurrentErrorHandler 自动重试 |
发送保障 | 生产者发送加回调,失败报警 |
🚀 你可以直接拿来实战用,真正可以扛流量的 Kafka 消费生产链路!
🧩 1. 死信队列处理机制整体流程
✅ 正常消费 →
❌ 消费失败 + 重试多次 →
🔁 还是失败 →
➡️ 将原消息投递到【死信Topic】(Dead Letter Topic)
➡️ 后续人工修复 or 自动补偿
🛠️ 2. 代码实现分三步
(1)修改你的 getConsumerFactory
方法,加 DeadLetterPublishingRecoverer
我们要替换掉原来的 SeekToCurrentErrorHandler
,换成支持死信队列的异常处理器:
less
@Autowired
@Qualifier("kafkaTemplate") // 用来发到死信topic
private KafkaTemplate<String, String> kafkaTemplate;
private KafkaListenerContainerFactory<?> getConsumerFactory(boolean batchListener) {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(concurrency);
factory.setBatchListener(batchListener);
// 配置死信队列处理器
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate,
(r, e) -> new TopicPartition(r.topic() + ".DLT", r.partition())
// 失败后投递到原Topic后缀加.DLT(Dead Letter Topic)
);
// 失败后重试3次,每次间隔5秒,重试完仍失败则交给recoverer处理
factory.setErrorHandler(new SeekToCurrentErrorHandler(recoverer, new FixedBackOff(5000L, 3)));
return factory;
}
🔵 解释一下:
DeadLetterPublishingRecoverer
:失败后自动把消息重新生产到.DLT
结尾的新Topic,比如消费order-event
失败了,就投到order-event.DLT
FixedBackOff(5000, 3)
:5秒重试1次,总共重试3次- 如果3次都失败,就触发
recoverer
,把消息扔到死信队列里。
(2)application.yml 配置补充(DLQ Topic创建)
Kafka死信队列是一个普通的Topic,只不过你最好提前建好(也可以用自动创建)。
比如:
vbnet
kafka:
topics:
- name: order-event
- name: order-event.DLT # 死信队列
如果你用的 Kafka Server 开了自动建Topic(auto.create.topics.enable=true),也可以不手动建。
(3)写个死信队列监听器(DLT Consumer)
比如你可以新建一个 DLQ 处理器,专门监听死信消息:
typescript
@Component
public class DeadLetterConsumer {
@KafkaListener(topics = "order-event.DLT", groupId = "dead-letter-group")
public void consumeDeadLetter(String message) {
System.err.println("【死信消息处理】收到消息:" + message);
// TODO: 这里可以发告警、落库、人工介入、补偿逻辑等
}
}
注意:这里可以记录日志、发告警(钉钉/飞书/邮件),或者存到DB里,方便后续人工修复。
🎯 3. DLQ机制总结图
rust
正常消费成功 --> 直接ack
正常消费失败 --> 重试 --> 成功ack
重试N次失败 --> DeadLetterPublishingRecoverer --> 投递到 死信Topic
死信消费者监听 --> 告警 or 补偿处理
🔥 总结
步骤 | 内容 |
---|---|
1 | 给 KafkaListenerContainerFactory 加上 DeadLetterPublishingRecoverer |
2 | 配好死信Topic(一般就是正常Topic后加 .DLT ) |
3 | 写一个专门的死信消费者 |
4 | 死信消息可以告警、存库、人工补偿 |
这样配置下来,你的 Kafka 消费链路就非常稳定了:不会因为单条脏数据影响整体消费系统健康。
- 死信消息里最好加上:
- 原始消息体
- 消费失败原因(异常栈)
- 时间戳
- 死信消费可以打到链路追踪系统,比如接入 Skywalking/Zipkin
- 对重要消息类型可以设置死信消息补偿重试机制
🚀 每个Bean的作用详细解释
Bean | 作用 |
---|---|
ServerEndpointExporter |
核心 !Spring Boot 启动时会扫描 @ServerEndpoint 注解的类,自动注册成 WebSocket 端点。没有它的话,@ServerEndpoint 注解不会生效! |
BaseEndpointConfigure |
扩展配置,通常用来自定义 WebSocket 握手过程,比如:校验token、设置自定义属性、统一Session管理、拦截器(handshake拦截)等。 |
🔥 WebSocket启动内部执行流程
- Spring Boot 容器启动 →
- 扫描到
@Configuration
注解的WebSocketConfig
→ - 创建
ServerEndpointExporter
实例,注册到 Spring 容器中 → ServerEndpointExporter
开始扫描项目中所有@ServerEndpoint
注解的类 →- 找到后,自动将这些类注册成 WebSocket端点(endpoint) →
- 创建
BaseEndpointConfigure
Bean,供你后续扩展(比如你可以写自己的modifyHandshake
)
写一个 @ServerEndpoint 示例(最简版)
比如:
typescript
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.OnOpen;
import javax.websocket.OnClose;
@ServerEndpoint("/ws/echo")
public class EchoWebSocket {
@OnOpen
public void onOpen(Session session) {
System.out.println("WebSocket连接成功: " + session.getId());
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到消息: " + message);
try {
session.getBasicRemote().sendText("服务端返回: " + message);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
System.out.println("WebSocket连接关闭: " + session.getId());
}
}
解释一下:
@ServerEndpoint("/ws/echo")
:对外暴露的连接地址,例如:ws://localhost:8080/ws/echo
@OnOpen
:连接建立时触发@OnMessage
:收到消息时触发@OnClose
:连接关闭时触发
场景 | 优化建议 |
---|---|
Session管理 | 统一保存在ConcurrentHashMap<String, Session>,方便广播发消息 |
心跳检测 | 定时推送PING消息,避免连接断了还以为连着 |
安全认证 | 在 modifyHandshake 里校验token / header |
异常捕获 | 包一下 @OnError 防止异常断开 |
线程模型 | 注意,WebSocket回调是在Tomcat nio线程池内,不要阻塞!可以扔到自己的业务线程池处理 |
🔥 带Token鉴权的WebSocket全流程设计
✅ 客户端连接 ws://yourserver/ws/xxx?token=xxx
✅ 服务器在握手时解析token
✅ token合法:允许连接
❌ token非法:拒绝连接,关闭通道
🛠️ 正式上代码
(1)改造你的 BaseEndpointConfigure
typescript
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class BaseEndpointConfigure extends ServerEndpointConfig.Configurator {
/**
* 修改握手逻辑,增加鉴权处理
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// 从url参数中拿token
String token = getTokenFromRequest(request);
if (!validateToken(token)) {
throw new RuntimeException("无效的token,禁止连接WebSocket!");
}
// 可以把用户信息保存到属性中,供后续OnOpen等使用
sec.getUserProperties().put("token", token);
}
private String getTokenFromRequest(HandshakeRequest request) {
String query = request.getQueryString(); // ?token=xxx
if (query != null && query.startsWith("token=")) {
return query.substring(6);
}
return null;
}
private boolean validateToken(String token) {
// TODO: 这里可以接入真实的token校验,比如JWT解析、Redis验证之类的
return token != null && token.length() > 5;
}
}
🔵 解释一下:
modifyHandshake
:在WebSocket握手阶段触发,可以用来做参数检查、认证鉴权。getTokenFromRequest
:从WebSocket连接的url参数里提取token。validateToken
:校验token是否合法(这里简单演示了长度判断,你可以换成解析JWT、查Redis等更复杂的逻辑)。
(2)你的 @ServerEndpoint
类里怎么拿token?
比如你的 EchoWebSocket
:
typescript
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/ws/echo", configurator = BaseEndpointConfigure.class)
public class EchoWebSocket {
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
String token = (String) config.getUserProperties().get("token");
System.out.println("WebSocket连接建立,token=" + token);
}
@OnMessage
public void onMessage(String message, Session session) {
try {
session.getBasicRemote().sendText("服务端返回:" + message);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
System.out.println("WebSocket连接关闭");
}
@OnError
public void onError(Session session, Throwable error) {
System.err.println("WebSocket发生错误:" + error.getMessage());
}
}
注意!
这里 @ServerEndpoint
要加上:
ini
configurator = BaseEndpointConfigure.class
不然不会执行自定义握手逻辑!
(3)客户端连接示例
连接时 URL 应该像这样带上 token
:
bash
ws://localhost:8080/ws/echo?token=abcdefg12345
如果 token 不对(比如为空或者非法),服务器在握手阶段就直接拒绝连接了!
🧩 总流程复盘
- 客户端连接携带token
- SpringBoot 启动 WebSocketConfig,注册
ServerEndpoint
- 握手时触发
BaseEndpointConfigure.modifyHandshake
- 校验token是否合法
- 合法 -> 保存到UserProperties -> 正常连接
- 非法 -> 抛异常 -> 连接失败
✅ 总结一下
点 | 内容 |
---|---|
✅ | 支持WebSocket连接带token校验 |
✅ | 校验不通过,连接直接拒绝 |
✅ | 校验通过,token信息可在后续OnOpen里获取 |
✅ | 整个流程干净利落,兼容你现在的配置体系 |