你以为 Spring 事件只是“发个通知”?——别急,事务一致性、异步坑、泛型匹配规则一个都跑不了!

举报
bug菌 发表于 2026/01/13 11:46:05 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学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 事件驱动模型讲透:

  1. ApplicationEvent / ApplicationListener 基础
  2. @EventListener 同步与 @Async 异步
  3. @TransactionalEventListener:事务提交后再触发,解决一致性
  4. 泛型事件(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 进一步提供基于 ResolvableTypesupportsEventType(用于泛型类型匹配),它在 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 事件当成系统的“解耦利器”,我建议你至少把下面几条当作底线(都是血泪换来的经验):

  1. 默认同步:别误会它是异步队列。同步监听器要“快”,否则拖慢主流程。
  2. 异步就要可观测:日志、指标、告警要跟上。因为异常不会冒泡回去。
  3. 事务一致性优先:涉及“读刚写的数据/发外部消息”的,优先考虑 @TransactionalEventListener AFTER_COMMIT。
  4. 事件 payload 轻量化:少塞实体,多塞 id/快照,避免懒加载与状态不确定。
  5. 泛型事件要稳:关键链路别玩得太“花”,必要时用具名事件类固定类型信息,避免匹配规则边缘行为。

结尾:你确定你要“事件驱动”,还是只是想“甩锅式解耦”?🤔

事件驱动在 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 -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。