Spring Boot事件监听,这东西到底解决啥问题?
一、先聊个场景,你就明白这玩意儿干啥的了
点过外卖吧?那咱们就用这个场景来说事。
你掏出手机下了单,付了钱。接下来会发生啥?
- 厨房那头开始备菜炒菜(这件事耽误不得,客户饿着呢)
- 手机收到一条短信:"您的订单已收到,预计30分钟送达"
- 你的会员账户里多了一堆积分
- 店长那个收银小喇叭"叮咚"响了一声
现在你设想一下,如果这家店的流程必须先发完短信 → 再加完积分 → 再响铃 → 最后厨房才能开始炒菜,你会不会觉得这店脑子有问题?
万一短信网关卡了三秒,后厨那师傅就拿着锅铲干等着。你作为客户,等餐时间硬生生多了好几秒。
稍微正常点的店,肯定是后厨该干啥干啥,短信、积分、响铃这几件事各自并行处理,谁也别耽误谁。
Spring Boot的事件监听机制,解决的就是这类问题——核心业务只管宣布"出事了",至于谁要响应、怎么响应,核心业务一概不关心。 大家各干各的,谁也不等谁。
二、就三个角色,没你想的那么复杂
这套东西其实就三个参与者:
- 事件:就是一个装数据的包裹,告诉你"刚才发生了一件事,这是相关数据"
- 发布者:就是"喊一嗓子"的那个人,通常是你核心业务代码里的某个Service
- 监听器:就是"听到动静就干活"的那个人,可能有多个,各干各的活
三、别废话了,直接上代码
老规矩,用一个真实的订单创建场景,从最基本的写法开始,一步一步往里加东西。
3.1 先加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
就这俩,别的不用。
3.2 第一步:把事件定义出来(就是那个"包裹")
订单创建之后,我们需要把订单号、用户ID、金额这几个关键数据打包传出去,让监听器们拿着用。
package com.example.demo.event;
import org.springframework.context.ApplicationEvent;
/**
* 订单创建事件
* 说白了就是一个装数据的筐,谁监听这个事件,谁就能从这个筐里拿数据
*/
public class OrderCreatedEvent extends ApplicationEvent {
/**
* 订单号
*/
private final String orderNo;
/**
* 用户ID
*/
private final Long userId;
/**
* 订单金额(单位:分,避免浮点数精度问题)
*/
private final Long amount;
/**
* 构造方法
* @param source 谁发的这个事件,通常传this就行
*/
public OrderCreatedEvent(Object source, String orderNo, Long userId, Long amount) {
super(source); // 父类要求,传进去就完了
this.orderNo = orderNo;
this.userId = userId;
this.amount = amount;
}
// -------- Getter 让监听器能拿到数据 --------
public String getOrderNo() {
return orderNo;
}
public Long getUserId() {
return userId;
}
public Long getAmount() {
return amount;
}
@Override
public String toString() {
return "OrderCreatedEvent{" +
"orderNo='" + orderNo + '\'' +
", userId=" + userId +
", amount=" + amount +
'}';
}
}
说一嘴:那个
source是父类ApplicationEvent的构造参数,大多数情况下你传this就行了。真正干活用到的是我们自己加的那三个字段。
3.3 第二步:写监听器——"收到包裹后我要干啥"
监听器的工作就是"当某某事件发生了,我该做什么"。
package com.example.demo.listener;
import com.example.demo.event.OrderCreatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 订单事件监听器
*/
@Component
public class OrderEventListener {
private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
/**
* 发短信
* Spring看到@EventListener注解的方法,并且参数是OrderCreatedEvent,
* 就会在事件发布时自动调用这个方法
*/
@EventListener
public void handleSendSms(OrderCreatedEvent event) {
log.info("【短信】给用户 {} 发短信,订单号:{}", event.getUserId(), event.getOrderNo());
// 假装调了一下短信接口,sleep模拟网络IO
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("【短信】发送成功");
}
/**
* 加积分
*/
@EventListener
public void handleAddPoints(OrderCreatedEvent event) {
log.info("【积分】用户 {} 获得 {} 积分", event.getUserId(), event.getAmount() / 100);
// 实际项目里这里会调积分服务的接口
}
/**
* 写日志
*/
@EventListener
public void handleWriteLog(OrderCreatedEvent event) {
log.info("【日志】记录订单创建:{}", event.getOrderNo());
}
}
这里有个细节:一个类里可以写多个
@EventListener方法,每个方法监听同一个事件,各干各的事,互不影响。你也可以把它们拆到不同的类里,看你的组织习惯。
3.4 第三步:发布事件——"喊一嗓子"
在订单Service里,把ApplicationEventPublisher注入进来,然后在合适的位置把事件发出去。
package com.example.demo.service;
import com.example.demo.event.OrderCreatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
@Autowired
private ApplicationEventPublisher eventPublisher; // 就靠这个发布事件
@Transactional
public void createOrder(Long userId, Long amount) {
// ---------- 核心:保存订单 ----------
String orderNo = "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6);
log.info("【核心】订单创建:{},用户:{},金额:{}分", orderNo, userId, amount);
// 实际项目里这里会调用 orderMapper.insert(order)
// 如果插入失败,事务回滚,后续代码不会执行
// ---------- 发布事件:通知别人"订单好了" ----------
// 注意:此时事务还没提交,监听器们正在同步执行
log.info("【核心】发布事件...");
OrderCreatedEvent event = new OrderCreatedEvent(this, orderNo, userId, amount);
eventPublisher.publishEvent(event);
log.info("【核心】事件发布完毕");
// 方法结束,事务提交
}
}
3.5 第四步:搞个Controller触发一下
package com.example.demo.controller;
import com.example.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/order/create")
public Map<String, Object> createOrder(
@RequestParam(defaultValue = "1001") Long userId,
@RequestParam(defaultValue = "10000") Long amount) {
long start = System.currentTimeMillis();
orderService.createOrder(userId, amount);
long cost = System.currentTimeMillis() - start;
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "订单创建成功");
result.put("costTime", cost + "ms");
return result;
}
}
3.6 跑一下看看
启动项目,POST请求一下 /order/create,控制台大概输出这样:
【核心】订单创建:ORD1693567890123abcde,用户:1001,金额:10000分
【核心】发布事件...
【短信】给用户 1001 发短信,订单号:ORD1693567890123abcde
【短信】发送成功
【积分】用户 1001 获得 100 积分
【日志】记录订单创建:ORD1693567890123abcde
【核心】事件发布完毕
注意看顺序:所有监听器执行完了,才打印"事件发布完毕"。说明默认是同步的,一个一个排队执行。
还有一个坑:要是任何一个监听器报错了,整个事务会回滚,订单创建失败。这点后面会说怎么处理。
四、你肯定会问:跟直接调@Async有啥区别?
这问题问到点子上了。很多人第一反应就是"我用@Async注解也能异步执行啊,搞这么复杂干嘛?"
4.1 先看直接用@Async的方式
@Service
public class OrderServiceV1 {
@Autowired
private SmsService smsService;
@Autowired
private PointsService pointsService;
@Transactional
public void createOrder(Long userId, Long amount) {
String orderNo = saveOrder(userId, amount);
// 显式调用,明确告诉程序要做什么
smsService.sendSmsAsync(orderNo, userId);
pointsService.addPointsAsync(userId, amount);
}
}
这么写有啥毛病?最大的毛病就是——如果产品经理说"再加个App推送",你得改OrderServiceV1这个类的代码。改一次两次还好,改个十次八次这个类就变成个上千行的怪物了。
4.2 再看事件的方式
@Service
public class OrderServiceV2 {
@Autowired
private ApplicationEventPublisher publisher;
@Transactional
public void createOrder(Long userId, Long amount) {
String orderNo = saveOrder(userId, amount);
// 只发事件,谁处理、怎么处理,一概不管
publisher.publishEvent(new OrderCreatedEvent(this, orderNo, userId, amount));
}
}
区别在哪? 主方法里只有一个publishEvent,新增功能完全不用动这个类,新建一个监听器就完事了。
| @Async直接调 | 事件监听 | |
|---|---|---|
| 主方法知道谁在干活吗? | 知道,挨个调了一遍 | 不知道,只管发事件 |
| 新增一个后续动作 | 改主方法代码 | 新建监听器就行 |
| 代码耦合度 | 越来越高 | 主类始终保持干净 |
说白了就是:你点了一个外卖,你是直接打电话给厨师、骑手、店长一个一个通知,还是在外卖App上点一下"下单"按钮完事?事件监听就是后者。
五、进阶:事务提交后再执行,这个很实用
上面那个版本有个隐藏问题,你可能已经注意到了:
@Transactional
public void createOrder(Long userId, Long amount) {
orderMapper.insert(order); // ① 插入数据,但事务还没提交
publisher.publishEvent(event); // ② 发布事件,监听器开始干活
// ③ 方法结束,事务提交
}
监听器在步骤②执行的时候,数据库事务还没提交。如果监听器里需要查询刚插入的订单数据,查不到!因为别的线程根本看不到未提交的数据。
这个在真实项目里经常遇到,解决方案就是Spring提供的@TransactionalEventListener。
5.1 四个事务阶段
Spring提供了四个"钩子",让你精确控制监听器在事务的哪个阶段执行:
| 阶段 | 执行时机 | 啥时候用? |
|---|---|---|
BEFORE_COMMIT |
事务提交前 | 需要和主业务在同一事务里,失败了要一起回滚 |
AFTER_COMMIT |
事务提交后 | 最常用,确保数据落盘了再做别的 |
AFTER_ROLLBACK |
事务回滚后 | 记录失败原因、做补偿 |
AFTER_COMPLETION |
事务完成后(不管成功还是失败) | 清理临时资源 |
5.2 用代码说话
package com.example.demo.listener;
import com.example.demo.event.OrderCreatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class OrderTransactionListener {
private static final Logger log = LoggerFactory.getLogger(OrderTransactionListener.class);
/**
* 事务提交后再执行——这个最常用
* 只有主方法的事务成功提交了,这个方法才会被触发
* 如果主方法回滚了,这个方法不会执行
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(OrderCreatedEvent event) {
log.info("【事务提交后】数据已经落盘了,订单号:{},放心查吧", event.getOrderNo());
// 这里适合干这些事:发MQ消息、调第三方接口、同步ES……
}
/**
* 事务回滚后执行
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(OrderCreatedEvent event) {
log.info("【事务回滚后】订单创建失败了,订单号:{}", event.getOrderNo());
// 记录失败日志、触发补偿流程
}
}
跑一下,看输出顺序:
【核心】订单创建:ORD1693567890123
【核心】发布事件...
【核心】事件发布完毕
(事务提交中...)
【事务提交后】数据已经落盘了,订单号:ORD1693567890123,放心查吧
注意:AFTER_COMMIT的监听器是在方法结束、事务提交之后才执行的。这时候数据已经妥妥地写在数据库里了,监听器里怎么查都安全。
5.3 生产级写法长这样
真实项目里通常是"事务提交后 + 异步执行"的组合:
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleSendSms(OrderCreatedEvent event) {
try {
// 调用短信网关
smsClient.send(event.getUserId(), "订单" + event.getOrderNo() + "已创建");
} catch (Exception e) {
// 注意:千万不能把异常抛出去,否则会影响其他监听器
// 记录日志 + 报警,让人工介入
log.error("短信发送失败,订单号:{}", event.getOrderNo(), e);
// 可以存到失败表,定时任务重试
}
}
注意别忘了在启动类上加@EnableAsync,不然@Async不生效。
六、几个容易踩的坑
坑1:监听器抛异常,主业务回滚
默认情况下,监听器和主业务在同一个事务里。监听器报错,主业务也跟着回滚。
// ❌ 这么写有风险
@EventListener
public void handle(OrderCreatedEvent event) {
smsService.send(event.getUserId()); // 万一发短信报错,整个订单都没了
}
// ✅ 稳妥的做法
@EventListener
public void handle(OrderCreatedEvent event) {
try {
smsService.send(event.getUserId());
} catch (Exception e) {
log.error("短信发送失败,不影响主流程", e);
// 记下来,后面人工或定时任务补偿
}
}
坑2:监听器太慢,拖垮了整个接口响应时间
默认同步执行,如果监听器里有个耗时操作,接口响应时间就会被拖长。
// ❌ 同步执行,接口被卡住
@EventListener
public void handle(OrderCreatedEvent event) {
Thread.sleep(5000); // 用户要等5秒才能看到"下单成功"
}
// ✅ 加个@Async就解决了
@Async
@EventListener
public void handle(OrderCreatedEvent event) {
Thread.sleep(5000); // 接口瞬间返回,监听器在后台慢慢跑
}
坑3:多个监听器的执行顺序不确定
如果你依赖某个监听器先执行,另一个后执行,需要加@Order明确顺序:
@Component
@Order(1)
public class FirstListener {
@EventListener
public void handle(OrderCreatedEvent event) {
// 先执行
}
}
@Component
@Order(2)
public class SecondListener {
@EventListener
public void handle(OrderCreatedEvent event) {
// 后执行
}
}
七、项目结构长这样
src/main/java/com/example/demo/
├── DemoApplication.java # 启动类,记得加@EnableAsync
├── controller/
│ └── OrderController.java # 入口
├── service/
│ └── OrderService.java # 核心业务,发布事件
├── event/
│ └── OrderCreatedEvent.java # 事件定义
└── listener/
├── OrderEventListener.java # 普通监听器
└── OrderTransactionListener.java # 事务监听器
八、总结一下
回到最开头的那个外卖场景。用事件监听的方式去组织代码,核心业务只管"下单"这一件事,短信、积分、日志都是旁边各自独立的监听器,谁出问题也不影响核心流程。新增一个"给店长发通知"的功能,也只需要加一个新的监听器,不用动核心代码。
记住两句话就够了:
- 核心业务只管喊一嗓子,谁响应、怎么响应,跟它没关系。
- 跟数据一致性相关的监听器,用
@TransactionalEventListener(phase = AFTER_COMMIT)确保事务提交后再执行,同时用@Async异步处理,别让旁路逻辑拖慢主流程。
代码这东西,你自己敲一遍比看十遍都管用。建议把上面的代码复制到你自己的项目里跑一下,改改参数,看看日志输出顺序,感受一下事件机制的运作方式。遇到问题欢迎留言交流。
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/22794911

浙公网安备 33010602011771号