
做后端这几年我越来越觉得判断一个系统设计得好不好看得不是 CRUD 写得多溜而是看业务变更时能不能按兵不动。订单创建、工单流转、用户注册这些业务节点背后往往跟着一大串动作如果全都写在 Service 方法里今天加一个通知明天加一个日志核心代码会越来越臃肿。Spring Boot 的 ApplicationListener 事件监听机制就是我用来给业务松绑的一把利器。这篇文章我会从原理到实践完整拆一遍这套机制讲清楚事件是怎么发布、怎么派发、怎么被监听的也会把实际项目里踩过的坑一并聊了适合正在用 Spring Boot 写业务、想优化代码结构的朋友。1. 事件监听机制到底解决了什么问题1.1 从下单后发短信这个经典需求说起我见过最多的代码腐烂是从一个需求开始的用户下单成功后需要发短信、记日志、扣库存甚至还要清一次缓存。新手最顺手的写法是在订单创建方法里把动作全平铺开public void createOrder(Order order) { // 1. 保存订单 orderMapper.insert(order); // 2. 发短信 smsService.send(order.getPhone(), 您的订单已提交); // 3. 记录日志 logService.save(创建订单, order); // 4. 扣库存 stockService.reduce(order.getSkuId(), order.getCount()); // 5. 清缓存 cacheService.evict(order: order.getId()); }这么写最简单代价也最明显所有非核心逻辑全都和创建订单绑死了。今天加推送通知要改订单服务明天不想记日志了还得改订单服务。更麻烦的是任何一个附加动作抛异常主流程可能跟着挂。就算不追求高深架构光是核心类频繁被非核心需求改动这一点长期迭代下来就够让人头疼。事件监听机制就是专门收拾这种局面的。它的核心思路是把发生了什么订单已创建和需要做什么反应发短信、写日志、扣库存彻底拆开。订单服务只负责发布一个订单已创建的事件剩下的响应动作交给监听器去处理。这样新需求来了只需新增一个监听器不用动原来的业务代码。在 Spring Boot 里这套机制默认就是可用的核心入口是 ApplicationListener 接口不需要额外引入任何依赖。这套机制适合谁我总结了三类人一是刚接触 Spring想搞懂 ApplicationContext 背后运作方式的新人二是已经写业务但感觉 Service 层越来越臃肿的开发者三是在做工程重构、想把逻辑按事件驱动重新梳理的架构师。后面我会从原理一路讲到实战不需要太多前置知识只要写过 Spring Boot 的 CRUD 就能跟上。1.2 观察者模式在 Spring 里落地后的完整画面Spring 的事件机制本质是观察者模式的实现。观察者模式大家应该不陌生一个对象状态变化通知所有关注它的对象。生活里最好理解的例子是微信群——你在群里发一条消息群里所有人都能收到但你不需要一个一个私聊。映射到 Spring 里发布者ApplicationEventPublisher负责发消息。事件ApplicationEvent就是消息内容的载体。监听者ApplicationListener就是群里那些关注消息的人。这套机制在 Spring Framework 里很早就存在了Spring Boot 在此基础上做了更多事情它把启动过程拆成了多个阶段事件比如 ApplicationStartedEvent、ApplicationReadyEvent还让你在监听器里能拿到 Spring 容器相关的对象。这些内置事件在排查启动问题、做启动后初始化工作时非常有用后面我会单独展开。一个容易被忽略的细节是Spring 的事件广播默认是同步执行的。也就是说发布事件后所有监听器依次执行完发布方法才返回。这个特性决定了事务边界、异常处理都会跟着发布线程走很多人没意识到这点就埋了坑。这里先埋个伏笔后面排查问题环节会重点讲。2. 核心原理拆解一个事件从发布到监听经历了什么2.1 三个核心角色ApplicationEvent、ApplicationListener、EventPublisher先看事件类。传统写法里自定义事件要继承 ApplicationEventpublic class OrderCreatedEvent extends ApplicationEvent { private final Order order; public OrderCreatedEvent(Object source, Order order) { super(source); this.order order; } public Order getOrder() { return order; } }这里要解释一下 source 参数。它代表事件是谁产生的通常传 this也就是发布事件的当前对象。生产环境里有时候响应式框架或工具类不方便传 this直接传一个标识字符串也能凑合但语义上不推荐。source 的核心作用是在异常排查时帮你找到事件来源别小看这个值出了问题查日志时它能帮你省一个小时。从 Spring Framework 4.2 开始其实不再强制要求事件继承 ApplicationEvent 了。你可以直接定义一个普通 POJO 当作事件发布时 Spring 会把它包装成 PayloadApplicationEvent 再广播。这个设计解放了写法也让 EventListener 注解可以更自然地处理任意类型参数。接下来是监听接口Component public class OrderCreatedListener implements ApplicationListenerOrderCreatedEvent { Override public void onApplicationEvent(OrderCreatedEvent event) { Order order event.getOrder(); smsService.send(order.getPhone(), 您的订单已提交); } }通过泛型监听器明确声明自己关心 OrderCreatedEvent。Spring 会在事件广播时做类型匹配只有类型合适的监听器才会被调用。如果有多个监听器关心同一个事件它们都会收到通知。发布者是最简单的直接注入 ApplicationEventPublisherService public class OrderService { Autowired private ApplicationEventPublisher eventPublisher; public void createOrder(Order order) { orderMapper.insert(order); // 业务完成后发布事件 eventPublisher.publishEvent(new OrderCreatedEvent(this, order)); } }Spring 容器本身实现了 ApplicationEventPublisher所以哪怕你把 ApplicationContext 直接注入进来调用 publishEvent 也能工作。但更推荐注入 ApplicationEventPublisher 这个最小接口原因是它对测试友好也不暴露容器的其他能力遵循了接口隔离原则。2.2 多播器是怎么把事件精确送到对应监听器的很多人不理解发布和监听之间发生了什么。真正干活的中间人是 ApplicationEventMulticaster也就是事件多播器。publishEvent 方法内部会委托给多播器多播器拿到事件后遍历容器里所有 ApplicationListener挨个做类型判断匹配成功的就调用其 onApplicationEvent 方法。这段逻辑藏在 Spring 容器源码里核心流程可以简单归纳为四步publishEvent() - 找 ApplicationEventMulticaster - multicastEvent() - 遍历候选监听器做类型匹配 - 调用匹配到的 onApplicationEvent()Spring Boot 项目默认使用 SimpleApplicationEventMulticaster。它的同步广播逻辑很直接如果监听器抛异常异常会直接沿着调用链往上传发布事件的方法也会跟着失败。这个特性在业务上有利有弊好处是强一致性监听逻辑出错了主流程能感知到坏处是非核心逻辑出错会影响核心动作。类型匹配时Spring 会做泛型解析。比如你定义了一个 ApplicationListener 那么 OrderCreatedEvent 的任意子类事件它也能收到。同理如果你监听的是一个父类型事件子类型事件也能匹配。这个机制在做抽象事件的时候很好用比如定义一个 BasePayEvent微信支付、支付宝支付都继承它一个监听器就能处理所有支付事件。2.3 Spring Boot 启动内置事件从 Starting 到 ReadySpring Boot 在启动过程中会按顺序发布一系列事件序列大致是ApplicationStartingEvent - ApplicationEnvironmentPreparedEvent - ApplicationContextInitializedEvent - ApplicationPreparedEvent - ApplicationStartedEvent - ApplicationReadyEvent。启动失败时会发布 ApplicationFailedEvent。这里我顺手整理了一张对照表每个事件发生时容器处于什么状态事件触发时机能拿到什么ApplicationStartingEvent启动刚运行容器未创建SpringApplication 本身ApplicationEnvironmentPreparedEventEnvironment 已准备容器未创建EnvironmentApplicationContextInitializedEvent容器已创建尚未加载 Bean 定义ApplicationContextApplicationPreparedEventBean 定义已加载容器还没刷新ApplicationContextApplicationStartedEvent容器已刷新Runner 执行前ApplicationContextApplicationReadyEventRunner 都执行完了应用可对外服务ApplicationContextApplicationFailedEvent启动过程抛异常ApplicationContext可能不完整我实际用最多的场景有两个。一是用 ApplicationReadyEvent 做应用启动完成后的初始化比如预热本地缓存、预加载字典数据。二是用 ApplicationFailedEvent 做启动失败时的告警通知比如把失败原因推到钉钉或者日志中心。有一点要特别提醒像 ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent 这类事件发生时 ApplicationContext 还没创建出来监听器里是拿不到容器 Bean 的。如果这时注入 ApplicationContext 或者某个 Service会报bean 未就绪类错误。所以首发阶段的监听器要非常克制只做轻量逻辑比如设置系统参数、注册配置源。3. 动手实践三种监听器写法与关键配置3.1 老派写法实现 ApplicationListener 接口第一种写法就是前面示例里的实现 ApplicationListener 接口并标成 Component。这种方式的好处是类型明确IDE 能给你很好的补全和跳转缺点是每个事件都要建一个类事件多了类文件会爆炸。有些场景里监听器不是由 Spring 管理的而是你希望手动挂载。可以在构造 SpringApplication 时调用 addListenersSpringApplication app new SpringApplication(MyApplication.class); app.addListeners(new OrderCreatedListener()); app.run(args);这样监听器不需要 Component 注解也能生效。注意它在容器外监听器里那些 Bean 依赖是拿不到的除非你自己传入。所以我建议能用 Component 就尽量用。还有一种地方能看到 ApplicationListener 的身影就是 SpringApplicationRunListener。它和普通事件监听器不是一回事是 SpringApplication 的运行时监听器用来感知启动过程的更底层事件比如 started、environmentPrepared。自定义 SpringApplicationRunListener 需要写在 META-INF/spring.factories 里而且 Spring Boot 3 之后它的构造方法签名有了变化必须接收 SpringApplication 和 String[] args 两个参数。这属于进阶玩法生产环境里偶尔用来做启动埋点很合适但一般业务项目用不到。3.2 注解写法EventListener 让监听器更清爽从 Spring 4.2 开始官方推荐用 EventListener 注解不用再实现接口了Component public class OrderListener { EventListener public void onOrderCreated(OrderCreatedEvent event) { Order order event.getOrder(); smsService.send(order.getPhone(), 您的订单已提交); } EventListener public void onOrderPaid(OrderPaidEvent event) { // 处理支付成功逻辑 } }监听方法的参数就是要处理的事件类型一个类里可以写多个监听方法按业务聚合代码可读性一下就上来了。这个注解还支持 SpEL 条件过滤。比如我只想监听金额超过 100 的订单EventListener(condition #event.order.amount 100) public void onOrderCreated(OrderCreatedEvent event) { // 大额订单特别处理 }condition 表达式里 #event 就是方法参数名。这种条件过滤很适合做分级处理比如大额订单走人工审核、普通订单自动通过。注解写法背后的实现原理值得说一句Spring 会在 Bean 初始化阶段后扫描带 EventListener 的方法把它们包装成 ApplicationListenerMethodAdapter再注册到事件广播系统里。所以你写注解监听和实现 ApplicationListener 接口最终底层拿到的是同样的东西。从 Spring Boot 2.3 开始这套注解机制在社区里已经成为主流写法绝大多数开源项目都在用注解监听。我个人的建议也是新项目一律用 EventListener只有在极少数需要稳定类型契约、或者做框架组件时才考虑接口实现。3.3 事务边界监听TransactionalEventListener 处理提交后通知业务开发里有个高频坑监听器在主事务还没提交时就去查数据库结果查不到刚写的数据。比如用户注册后发欢迎邮件邮件服务在事务提交前去查用户表数据还没落库查询返回空。Spring 专门提供了一个解决方案TransactionalEventListener。它可以指定监听的时机默认是 AFTER_COMMIT也就是事务提交成功后才触发TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onUserRegistered(UserRegisteredEvent event) { // 此时事务已提交能查到时数据 emailService.sendWelcome(event.getUserId()); }可选的 phase 有 BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK 和 AFTER_COMPLETION。BEFORE_COMMIT 适合在提交前做一些强校验AFTER_ROLLBACK 适合做失败补偿AFTER_COMPLETION 不管成功失败都会触发。它的底层实现是事务同步机制监听器会被注册到当前事务的同步列表里当事务走到对应阶段时才回调。这里有两个需要留意的点如果发布事件时当前没有事务在跑监听器默认不会执行。要让它在这种情况下也执行可以设置 fallbackExecution true。事件必须在新事务里发布。如果发布事件是在一个已经关闭的事务里同步注册可能已经晚了监听器还是不触发。我用这个注解处理过订单超时关单的补偿逻辑也处理过积分发放的幂等逻辑。凡是业务写完但必须等数据落库才能执行的后续动作它就是最合适的选择。3.4 异步监听与自定义线程池配置默认情况下监听器和发布方法跑在同一个线程。如果监听器里是一个耗时的网络请求比如调用外部短信网关发布方法的响应时间会被拖长。这种非核心动作建议异步化最简单的方式是给监听方法加上 AsyncEventListener Async(smsExecutor) public void onOrderCreated(OrderCreatedEvent event) { smsService.send(event.getOrder().getPhone(), 您的订单已提交); }注意 Async 要生效需要满足几个条件。第一启动类或配置类要有 EnableAsync。第二必须是代理对象调用也就是说监听器方法不能是自己类内部互相调用。第三Spring Boot 项目里如果存在一个 Executor BeanSpring 会优先使用它作为默认异步执行器如果没配置则使用 SimpleAsyncTaskExecutor它是每来一个任务就 new 一个线程的实现释放后线程直接扔掉不建议在生产环境这么跑。我通常单独配置一个业务用的线程池把 corePoolSize 设置成 CPU 核数的两倍左右队列容量看业务峰值来定。异步监听不是无限开线程一定要控制资源上限。这里还有一个容易踩的坑异步监听器里如果用了 ThreadLocal 来传 TraceId、用户信息之类的上下文异步线程是拿不到这些值的。因为 ThreadLocal 是线程绑定的跨线程就丢了。要么改成传递参数要么用 MDC 配合自定义线程池的装饰器去拷贝上下文这在链路追踪场景里非常常见。4. 事件监听在真实业务场景里怎么打组合拳4.1 企业办公用品系统的库存预警与审计日志实战理论讲完了拿一个实际项目例子来串一遍。我之前做过一个企业办公用品管理系统里面有个核心场景员工提交办公用品领用申请。这个操作涉及库存扣减、库存低量提醒、审计日志记录和前端实时通知。如果用传统方式写申请服务会变得非常臃肿。我最后是这么设计的。定义领用申请事件public class ApplyCreatedEvent { private final Long applyId; private final String employeeNo; private final String itemCode; private final Integer quantity; public ApplyCreatedEvent(Long applyId, String employeeNo, String itemCode, Integer quantity) { this.applyId applyId; this.employeeNo employeeNo; this.itemCode itemCode; this.quantity quantity; } }申请服务只负责保存申请单和发布事件public void createApply(ApplyForm form) { Apply apply new Apply(); // ... 组装、校验、保存 applyMapper.insert(apply); eventPublisher.publishEvent(new ApplyCreatedEvent( apply.getId(), apply.getEmployeeNo(), apply.getItemCode(), apply.getQuantity())); }后面所有扩展都通过监听器接入。库存监听器负责扣库存并检查预警审计监听器负责写操作日志推送监听器负责把消息通过 WebSocket 发给前端管理员。后来需求变化要加邮件通知部门主管时我只新增了一个监听器类申请服务一行没动。这个例子里的事件没有继承 ApplicationEvent而是普通 POJO。发布时 Spring 会包装成 PayloadApplicationEvent。这样设计的好处是事件类不依赖 Spring API写单元测试时直接 new 一个事件对象就行。4.2 配合 WebSocket 推送和监控上报在办公用品系统里前端需要实时看到库存预警。传统的 HTTP 轮询性能差、实时性弱用 WebSocket 更合适。结合事件监听机制代码结构会非常清晰库存监听器在扣减库存后如果发现低于阈值就发布一个 StockLowEventWebSocket 推送监听器收到这个事件后从事件里取出数据推送到对应前端会话。WebSocket 的接入在 Spring Boot 里主要是实现 WebSocketHandler 或者用 STOMP 协议这里不展开讲。重点是事件机制和 WebSocket 之间天然是解耦的推送逻辑作为事件消费者存在库存服务完全不知道前端的存在。监控上报也同理。我之前把员工自助申领大件物品的业务事件上报给监控系统用于统计高频申请。做法是定义一个 MetricsEvent在核心监听器里把事件上报到 Prometheus 或者 Spring Boot Admin 可采集的端点。Spring Boot Admin 本身更多的是监控应用运行状态比如内存、线程、健康检查如果你想把业务指标也纳进去通过事件监听做数据聚合是个很轻量的方案不需要额外引入消息队列。4.3 监听器的执行顺序用 Order 稳住节奏多个监听器监听同一个事件时执行顺序默认是不保证的。如果业务上要求先扣库存再发通知就得显式控制顺序。Spring 提供了两种方式实现 Ordered 接口或者使用 Order 注解。我比较推荐注解方式EventListener Order(1) public void reduceStock(ApplyCreatedEvent event) { // 先扣库存 } EventListener Order(2) public void sendNotify(ApplyCreatedEvent event) { // 后发通知 }数字越小优先级越高先执行。顺序控制虽然简单但我要提醒一点不要依赖监听器顺序来处理强一致性的业务。比如扣库存失败就必须回滚申请单这种强关联逻辑放在同一个事务里更稳妥而不是靠两个监听器的先后顺序。事件监听的定位是解耦和响应跨监听器的强事务需求应该回到事件发布方法中处理或者用事务事件 补偿机制来保证最终一致。顺序控制在异步场景里还有个细节如果你把某个监听器做成 Async那么顺序只保证到进入线程池排队这一步异步执行本身不保证完成顺序。所以有严格先后要求的监听器最好不要混用同步和异步。5. 常见问题与排查技巧实录5.1 监听器一直没触发先从这两个地方查这是新人问得最多的问题我发布了事件监听器就是不执行怎么回事我一般先让同事查两处。第一处是事件类型是否匹配。监听器方法参数、监听器泛型是不是和发布的事件类型一致。比如发布的是 PayloadApplicationEvent监听器却定义了 EventListener(OrderCreatedEvent.class)类型对不上自然不会触发。用普通 POJO 作事件时特别容易忽略发布对象实际被包装成 PayloadApplicationEvent 这层关系。第二处是监听器是否真的被 Spring 管理。检查类上有没有 Component 或 Service 注解组件扫描的包路径能不能覆盖到它。Spring Boot 主类默认扫描的是主类所在包及子包监听器放在其他包下就扫不到。还有一类隐蔽情况事件发布得太早。比如在构造函数里、在 PostConstruct 里发布事件此时容器还在初始化阶段一些监听器 Bean 还没注册完成事件就丢了。现在我判断了也不会去改这段代码。碰到启动期间的事件需求正确的姿势是监听对应的 Spring Boot 启动事件比如 ApplicationReadyEvent等容器完全就绪再执行。5.2 Async 异步监听不生效的根因异步监听不生效九成是代理没生效。Spring 的 Async 是通过 AOP 动态代理实现的只有通过代理对象调用方法时切面才会介入。如果监听器类内部自己调用了带 Async 的方法走的不是代理异步就失效了。但监听器比较特殊它的 EventListener 是靠 Spring 在启动时注册的方法调用路径其实没问题所以更应该检查的是 EnableAsync 有没有加。肉眼可见的坑是很多项目已经加在启动类上了但如果配置类被排除扫描或者启动类不在扫描路径内EnableAsync 也不会生效。还有一个容易被忽视的原因异步执行器没配好。Spring Boot 项目里默认的 Async 执行器在不同版本上有差异。如果没显式提供 Executor Bean某些版本会 fallback 到 SimpleAsyncTaskExecutor这个执行器的特点是每次任务都新建线程不在池里复用。响应慢、线程暴涨通常是它引起的。我在生产环境里看到过一次一万个请求进来创建了一万个线程的情况就是因为没配线程池。排查时先看一眼线程池的配置再下结论。5.3 监听器抛异常引发的连锁反应同步事件广播下监听器抛异常会直接传到发布事件的方法。我遇到过一个案例订单创建成功后一个做统计的监听器连接外部报表服务超时导致订单创建接口直接返回 500用户以为下单失败实际上单子已经落库了。这种错误是很典型的非核心逻辑拖垮核心流程。应对方案分两层。第一层是监听器内部做防御用 try-catch 包裹和主业务无关的调用做好异常日志。第二层是把不是必须成功的外部调用改成异步监听并且给异步任务配置独立的异常处理。Spring 里 Async 的异常默认进 TaskExecutor 的 handler你可以实现 AsyncUncaughtExceptionHandler 来统一记录错误和告警。我个人的习惯是同步监听器只做必须有结果才能继续的事比如校验凡是可延迟、可失败后重试的动作全部异步化或者放到消息队列里。事件机制虽然灵活但也容易让人误用一旦把通知变成同步阻塞门槛解耦就变成了另一种耦合。5.4 从 Spring Boot 2.3 到 3.x 的版本演进注意点Spring Boot 2.3 以后事件机制本身变化不大但周边环境一直在变。我整理了几个版本升级时需要留意的点。Spring Boot 2.3 开始Spring Framework 4.2 的普通 POJO 事件已经成为主流固化成模板后基本不写继承 ApplicationEvent 的类。Spring Boot 2.6 默认禁止了 Bean 之间的循环依赖。事件监听器如果互相注入或者监听器依赖发布事件的 Service、而 Service 又依赖监听器启动时可能报循环引用错误。解决办法是打破依赖环比如通过 ObjectProvider 延迟获取而不是修改配置强行放开循环引用。Spring Boot 3 将基线提升到 Java 17同时从 javax 迁移到 jakarta 命名空间。用 IDE 自动导入时容易引到旧的 javax.annotation 包导致注解完全不生效。这个坑在 Spring Boot 3.x 项目里很常见。Spring Boot 3 里 SpringApplicationRunListener 的构造方法签名变了。如果你自定义过它升级后必须把构造方法改成接收 SpringApplication 和 String[] args。至于热词里提到的 IDEA 社区版其实用社区版开发 Spring Boot 完全可行。社区版缺的是 Spring Initializr 的图形化创建向导、以及部分 Spring 专属的配置文件提示你可以用 start.spring.io 页面生成项目模板再用 IDEA 打开平时写代码、跑测试、Git 操作都不受影响。配置提示弱一点多查文档就好。版本演进里最核心的一条经验是升级前先看看官方 release notes别只盯着功能新增破坏性变更往往藏在默认行为调整里。Spring Boot 2.6 的循环引用就是典型例子默认行为一变很多老项目启动直接失败但这类问题不是事件机制本身造成的而是整体容器行为调整引发的。我个人在实际项目里体会最深的是事件驱动思维不能用过头。它是解耦利器但本地事件是进程内的、没有持久化的应用重启会丢事件跨服务也拿不到。真要保证消息不丢、可重试、可追溯还是得靠 MQ 这类自带存储和重试能力的消息中间件。事件监听适合解决进程内、低延迟、可容忍丢失的联动场景。最后分享一个团队里现在还在用的小习惯每定义一个新事件都在注释里写清楚发布时机、消费者和事务边界时间长了这就是一份活的事件字典排查线上问题时会轻松很多。如果你刚开始重构一个臃肿的 Service 层试着从找出一个业务节点、把它拆成事件发布 监听器开始一旦体会到这种拆法的爽感你会忍不住回不去的。