你以为 Spring 事件只是“发个通知”?——别急,事务一致性、异步坑、泛型匹配规则一个都跑不了!
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
前言:事件驱动真香,但“香”得有点烫嘴🔥
老实讲,我第一次在 Spring 里用事件(Event-Driven)的时候,心态特别朴素:
“我把订单创建完,丢个事件出去,让别的模块自己处理——解耦,优雅,完美!”😎
然后现实啪啪打脸:
- 事件监听器里查库,事务还没提交,结果查不到数据(我当场愣住);
- 我一兴奋加了
@Async,结果异常也不往外抛,日志还没配好,问题像蒸发了一样; - 泛型事件写得很开心,结果监听器死活不触发——我盯着代码半小时,怀疑人生😵💫
所以这篇文章不走“概念念经”路线,我们按你的大纲,一步一步把 Spring 事件驱动模型讲透:
ApplicationEvent/ApplicationListener基础@EventListener同步与@Async异步@TransactionalEventListener:事务提交后再触发,解决一致性- 泛型事件(Generic Events)的匹配规则(到底怎么判“匹配”的)
所有关键点我都会尽量以 官方文档/源码接口语义为准(少听江湖传言😄)。
1. 基础:ApplicationEvent 与 ApplicationListener(事件系统的“老祖宗”)
1.1 ApplicationEvent:事件对象的传统形态
Spring 很早就有事件模型。传统做法是:
- 定义一个事件类(通常继承
ApplicationEvent) - 发布事件(
ApplicationEventPublisher#publishEvent) - 写监听器(实现
ApplicationListener<E>)
ApplicationEvent 本质上是一个带 source(事件源对象)和时间戳的事件基类。
示例:定义事件(传统写法)
public class OrderCreatedEvent extends org.springframework.context.ApplicationEvent {
private final Long orderId;
public OrderCreatedEvent(Object source, Long orderId) {
super(source);
this.orderId = orderId;
}
public Long getOrderId() {
return orderId;
}
}
吐槽一句:继承 ApplicationEvent 不是必须的(后面你会看到 @EventListener 甚至可以直接监听任意对象事件),但它依然有存在价值:语义清晰、工具链支持好、团队一看就懂。
1.2 ApplicationListener:监听器接口(支持泛型过滤)
ApplicationListener<E extends ApplicationEvent> 是典型的观察者模式监听接口,而且它支持通过泛型声明自己关心的事件类型,Spring 会据此做事件过滤。
示例:实现监听器(传统写法)
@Component
public class OrderCreatedListener implements ApplicationListener<OrderCreatedEvent> {
@Override
public void onApplicationEvent(OrderCreatedEvent event) {
System.out.println("收到订单事件,orderId=" + event.getOrderId());
}
}
优点:明确、直接、老少皆宜。
缺点:一个监听器类通常就干一件事,项目大了类会变多;另外方法签名固定,不够灵活。
2. @EventListener:从“实现接口”进化到“方法级监听”😄
从 Spring 4.2 开始,推荐使用 @EventListener 在方法上声明监听逻辑,更灵活。官方注解语义也明确:它标记一个方法作为应用事件监听器,方法参数通常就是要监听的事件类型。
2.1 同步监听:默认就是同步(在发布事件的线程里执行)
@Component
public class OrderEventHandlers {
@EventListener
public void handle(OrderCreatedEvent event) {
// 默认同步:publishEvent 的线程会执行到这里
System.out.println("同步处理订单事件: " + event.getOrderId());
}
}
这里有个“心理陷阱”:
你以为事件是“异步通知”,但 Spring 默认是“同步回调”。
也就是说:监听器慢=发布事件的方法也慢。
所以“事件驱动”并不自动等于“异步”,这点一定要拎清😅。
2.2 异步监听:@Async + @EventListener(但别把坑也异步了🤣)
官方明确提到:想异步处理事件监听器,可以使用 Spring 的 @Async 支持,但要注意限制:
- 异步监听器抛出的异常不会传播回发布者线程
- 异步监听器方法不能通过“返回值”再发布后续事件(有相关限制)
2.2.1 开启异步能力(必要)
@Configuration
@EnableAsync
public class AsyncConfig {
}
2.2.2 异步监听器示例
@Component
public class OrderAsyncHandlers {
@Async
@EventListener
public void handleAsync(OrderCreatedEvent event) {
System.out.println("异步处理开始: " + event.getOrderId());
// 模拟耗时
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
// 故意抛个异常
if (event.getOrderId() % 2 == 0) {
throw new RuntimeException("异步监听器炸了,orderId=" + event.getOrderId());
}
System.out.println("异步处理完成: " + event.getOrderId());
}
}
关键提醒(非常现实):
- 发布事件的地方不会感知你的异常(官方已说明异常不传播)。
- 所以你必须在异步监听器里做足够的日志/监控,或者配置
AsyncUncaughtExceptionHandler(否则“悄无声息的失败”最可怕😇)。
2.2.3 线程池别用默认的(默认的容易把你坑到怀疑人生)
@Async 允许你指定 executor(通过 value/qualifier),官方注解也说明了 value 用于匹配具体 Executor/TaskExecutor Bean。
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("event-");
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.initialize();
return executor;
}
}
@Component
public class OrderAsyncHandlers {
@Async("eventExecutor")
@EventListener
public void handleAsync(OrderCreatedEvent event) {
// ...
}
}
不然你线上一波事件高峰,默认策略把线程挤爆/排队拖死,那就不是“解耦”,是“解体”了🥲。
3. 事务一致性:@TransactionalEventListener(真·救命稻草🧯)
3.1 经典翻车现场:监听器里查不到刚写的数据
你在一个事务里创建订单:
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
private final OrderRepository repo;
public OrderService(ApplicationEventPublisher publisher, OrderRepository repo) {
this.publisher = publisher;
this.repo = repo;
}
@Transactional
public Long createOrder() {
Order order = repo.save(new Order());
publisher.publishEvent(new OrderCreatedEvent(this, order.getId()));
return order.getId();
}
}
如果监听器里立刻去查:
@Component
public class OrderQueryListener {
private final OrderRepository repo;
public OrderQueryListener(OrderRepository repo) {
this.repo = repo;
}
@EventListener
public void on(OrderCreatedEvent e) {
// 可能查不到:事务未提交,其他事务隔离级别下不可见
System.out.println("listener find: " + repo.findById(e.getOrderId()));
}
}
这就是“事件驱动 + 事务”的天然矛盾:
事件发布发生在事务内,但你希望监听发生在“提交之后”。
要不然:查不到数据、发消息提前、外部系统收到“幽灵订单”……后果很精彩(精彩到想请假😵💫)。
3.2 Transaction-bound Events:事务绑定事件的官方机制
Spring 官方参考文档专门有一节“Transaction-bound Events”,明确指出:
- 普通
@EventListener是常规事件监听 - 若需要绑定事务,请用
@TransactionalEventListener - 默认绑定在提交阶段(commit phase)
3.2.1 AFTER_COMMIT:事务提交后才执行(默认就是它)
@Component
public class OrderAfterCommitListener {
@TransactionalEventListener
public void onAfterCommit(OrderCreatedEvent e) {
// 默认 TransactionPhase.AFTER_COMMIT
System.out.println("事务提交后再处理: " + e.getOrderId());
}
}
官方 Javadoc 明确:@TransactionalEventListener 默认 phase 是 AFTER_COMMIT;如果当前没有事务,默认不会处理,除非开启 fallbackExecution()。
3.2.2 其他阶段:BEFORE_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION
有些场景你会想更细:
BEFORE_COMMIT:提交前做校验/补充(但注意仍在事务里)AFTER_ROLLBACK:回滚后做补偿/告警AFTER_COMPLETION:不管提交/回滚都执行(但不能再依赖“会提交”的资源状态)
这些枚举语义在 TransactionPhase 里有说明,尤其 AFTER_COMPLETION 还强调:与底层事务资源交互不会再被提交。
@Component
public class OrderTxPhasesListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void onRollback(OrderCreatedEvent e) {
System.out.println("回滚后处理: " + e.getOrderId());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void onCompletion(OrderCreatedEvent e) {
System.out.println("事务完成后处理(不区分提交/回滚): " + e.getOrderId());
}
}
3.3 fallbackExecution:没事务时要不要执行?别“默认忽略”把自己坑了
@TransactionalEventListener 默认:没有事务就不处理。
这其实很合理:你都说“事务提交后触发”,那没事务自然没“提交后”。
但现实是:有些方法既可能在事务里被调用,也可能在非事务里被调用(比如某个批处理脚本)。这时候你可以:
@TransactionalEventListener(fallbackExecution = true)
public void onTxOrNonTx(OrderCreatedEvent e) {
System.out.println("有事务=提交后触发;没事务=立刻触发");
}
我个人经验是:谨慎开。
因为一旦开了,你就要确保监听器逻辑在“事务语境”和“非事务语境”下都安全。否则你会得到一个非常魔幻的行为:
同样的事件,在不同调用路径下触发时机不一致。
这种 bug 有时候不是难,是“阴间”😇。
3.4 事务一致性的正确姿势:事件里放“最小必要信息”
一个很实用的原则:
事务事件别塞整个实体对象(尤其是 JPA Entity)。
塞orderId、塞业务关键字段即可。
原因很朴素:
- 实体可能是懒加载代理,出了事务就炸
- 事件对象跨线程(异步)/跨阶段(after commit)时,实体状态未必可靠
- 你真正需要的是“事实”:某个 orderId 已经创建并提交
4. 泛型事件(Generic Events):匹配规则到底怎么判?我真的被它坑过😅
先说结论:Spring 的事件匹配不仅看 Class,也可能看泛型参数。
ApplicationListener 本身就支持“通过泛型声明感兴趣的事件类型”,并由容器做过滤。
而 Spring 为了支持更丰富的事件元信息和泛型处理,还提供了 GenericApplicationListener(更强的元数据能力,处理泛型事件类型等)。
4.1 为什么会有“泛型事件”这回事?
你可能想表达:
- “这是一个数据变更事件,但 payload 类型不同”
- “这是一个领域事件,但不同聚合类型不同”
于是你写:
public class DomainEvent<T> {
private final T payload;
public DomainEvent(T payload) {
this.payload = payload;
}
public T payload() { return payload; }
}
然后你想监听某一种 payload:
@Component
public class UserEventListener {
@EventListener
public void on(DomainEvent<UserCreated> event) {
System.out.println("只监听 UserCreated: " + event.payload());
}
}
问题来了:
- 这个监听器会不会收到
DomainEvent<OrderCreated>? - Spring 到底按什么规则匹配?只看原始类型
DomainEvent,还是会把<UserCreated>也算进去?
4.2 匹配规则的核心:Spring 会“尽可能”解析泛型类型
从官方接口语义你能看出来:
ApplicationListener支持通过泛型声明事件类型,并据此过滤。GenericApplicationListener进一步提供基于ResolvableType的supportsEventType(用于泛型类型匹配),它在 Spring 4.2 起就强调“full handling of generic event types”。GenericApplicationListenerAdapter还会通过“反射解析监听器声明的泛型事件类型”来确定支持的事件类型。
翻译成人话就是:
如果 Spring 能在运行时解析出监听器方法参数的泛型信息,它就会拿泛型一起做匹配;解析不出来,就可能退化为只按原始类型匹配。
4.3 你最容易踩的两个坑(我替你踩过了😭)
坑 1:事件发布时丢失泛型信息(类型擦除 + 你写法不当)
如果你发布事件像这样:
publisher.publishEvent(new DomainEvent<>(new UserCreated(...)));
在 Java 运行时,new DomainEvent<>(...) 的泛型参数是擦除的(只剩 DomainEvent)。
Spring 有时仍能通过 ResolvableType 推断 payload,但一旦推断链断了,匹配就会“变得随缘”。
更稳的写法:让事件类型在类层面就固定,比如:
public class UserCreatedEvent extends DomainEvent<UserCreated> {
public UserCreatedEvent(UserCreated payload) { super(payload); }
}
然后监听:
@EventListener
public void on(UserCreatedEvent e) { ... }
这下就很稳,因为 Class 级别的类型信息固定了。
坑 2:监听器参数写得“太抽象”,导致匹配范围过大
你写:
@EventListener
public void on(DomainEvent<?> e) { ... }
那它就可能吃到所有 DomainEvent,范围很大。
如果你同时存在多个监听器,优先级/顺序要考虑(比如 @Order),否则日志里会像放烟花一样乱😵💫(事务事件也支持 @Order 来排序,这是官方也提到的点)。
5. 把整套模型串起来:一个“订单创建 -> 提交后发消息 -> 异步通知”的完整示例
来,咱们把同步、事务后触发、异步组合一下,写一条“像样的生产链路”。
5.1 事件定义(只放必要信息)
public record OrderCreatedEvent(Long orderId) { }
是的,不继承 ApplicationEvent 也行。你用 @EventListener 监听任意对象就可以(Spring 的事件机制支持发布任意对象事件,传统 ApplicationEvent 只是其中一种形态)。这一点在 @EventListener 的设计目的里也能看出它更偏“方法级监听任意事件类型”。
5.2 发布事件(事务内发布)
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
private final OrderRepository repo;
public OrderService(ApplicationEventPublisher publisher, OrderRepository repo) {
this.publisher = publisher;
this.repo = repo;
}
@Transactional
public Long createOrder() {
Order order = repo.save(new Order());
publisher.publishEvent(new OrderCreatedEvent(order.getId()));
return order.getId();
}
}
5.3 提交后触发:确保数据已落库
@Component
public class OrderAfterCommitHandler {
private final OutboxService outboxService;
public OrderAfterCommitHandler(OutboxService outboxService) {
this.outboxService = outboxService;
}
@TransactionalEventListener
public void onAfterCommit(OrderCreatedEvent event) {
// AFTER_COMMIT:提交成功后才会执行(默认)
outboxService.saveOutboxMessage("ORDER_CREATED", event.orderId());
}
}
默认 AFTER_COMMIT、无事务不执行(除非 fallbackExecution=true)这些都是 @TransactionalEventListener 的官方语义。
5.4 异步通知:把慢活丢到线程池
@Component
public class OrderNotificationHandler {
private final NotificationClient client;
public OrderNotificationHandler(NotificationClient client) {
this.client = client;
}
@Async("eventExecutor")
@EventListener
public void notify(OrderCreatedEvent event) {
client.send("订单已创建: " + event.orderId());
}
}
异步监听异常不回传、限制条件这些别忘了(官方已经提醒)。
6. 一些“工程化建议”(中立但真诚🙂)
你如果打算把 Spring 事件当成系统的“解耦利器”,我建议你至少把下面几条当作底线(都是血泪换来的经验):
- 默认同步:别误会它是异步队列。同步监听器要“快”,否则拖慢主流程。
- 异步就要可观测:日志、指标、告警要跟上。因为异常不会冒泡回去。
- 事务一致性优先:涉及“读刚写的数据/发外部消息”的,优先考虑
@TransactionalEventListenerAFTER_COMMIT。 - 事件 payload 轻量化:少塞实体,多塞 id/快照,避免懒加载与状态不确定。
- 泛型事件要稳:关键链路别玩得太“花”,必要时用具名事件类固定类型信息,避免匹配规则边缘行为。
结尾:你确定你要“事件驱动”,还是只是想“甩锅式解耦”?🤔
事件驱动在 Spring 里确实好用——它让模块之间不再手牵手走路,而是“你发个信号我自己处理”。
但你要是没想清楚:同步/异步边界、事务提交时机、泛型匹配规则,它也能让你把 bug 藏得特别深,深到凌晨两点你看日志都像看天书🥲。
所以我最后送你一句很欠揍但很实用的反问:
**你发的到底是“事件”,还是“一个你不想负责的副作用”?**😄
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)