Spring ApplicationEvent 是怎么体现观察者模式的?
简化版
Spring ApplicationEvent 体现了观察者模式:事件发布器负责发布事件,监听器负责订阅和处理事件,Spring 容器负责维护发布者与监听器之间的关系。业务代码只发布事件,不需要直接调用所有后续处理逻辑。
详细版
Spring 中常见事件机制包括:
- 事件对象:继承
ApplicationEvent或直接使用普通对象作为事件。 - 发布器:
ApplicationEventPublisher,负责发布事件。 - 监听器:
ApplicationListener或@EventListener,负责处理事件。 - 容器:负责发现监听器并分发事件。
例如订单支付成功后:
publisher.publishEvent(new OrderPaidEvent(orderId));
然后多个监听器可以分别处理短信、积分、日志等逻辑。
默认情况下,Spring 事件通常是同步执行的;如果需要异步,可以配合 @Async 和线程池。使用时要注意事务时机、异常传播、监听器耗时和幂等问题。
完整版教学
一、Spring 事件为什么适合做应用内解耦
在业务系统中,经常有“主流程完成后触发多个附加动作”的需求。
比如订单支付成功:
- 通知用户;
- 发放积分;
- 记录运营事件;
- 更新用户等级;
- 触发数据同步。
如果支付服务直接依赖所有下游服务,支付模块会越来越重。Spring 事件允许支付服务只发布一个 OrderPaidEvent,具体监听器各自处理。
这就是观察者模式在 Spring 应用内的典型落地。
二、事件发布器对应 Subject 的通知能力
在 Spring 里,发布事件通常依赖:
private final ApplicationEventPublisher publisher;
发布事件:
publisher.publishEvent(new OrderPaidEvent(orderId, userId, amount));
发布器不关心有哪些监听器,也不关心监听器做什么。它只负责把事件交给 Spring 事件机制分发。
三、监听器对应 Observer
监听器可以用 @EventListener:
@Component
public class SmsListener {
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
// 发送支付成功短信
}
}
也可以实现 ApplicationListener 接口:
@Component
public class PointListener implements ApplicationListener<OrderPaidEvent> {
public void onApplicationEvent(OrderPaidEvent event) {
// 增加积分
}
}
这两种方式本质都是注册观察者。
四、默认同步执行要特别注意
很多人以为“发布事件就是异步”,这在 Spring 里不一定成立。
默认情况下,Spring ApplicationEvent 通常会在发布线程内同步调用监听器。也就是说,如果某个监听器很慢,主流程也会被拖慢;如果监听器抛出运行时异常,还可能影响发布方。
如果需要异步处理,可以配置异步执行器并使用 @Async:
@Async
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
// 异步处理
}
异步后要考虑线程池容量、异常日志、上下文传递和幂等。
五、事务时机是高频追问
如果在事务方法里发布事件,要注意事件监听器执行时事务是否已经提交。
例如下单方法还没提交事务,监听器就去查订单,可能查不到或查到旧数据。
Spring 提供了 @TransactionalEventListener,可以指定事件在事务阶段触发,例如事务提交后:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPaid(OrderPaidEvent event) {
// 事务提交后执行
}
这在订单、支付、库存这类强事务场景中非常重要。
六、Spring 事件的同步与事务时机
事务内发布事件时,普通监听器可能在提交前执行;AFTER_COMMIT 监听器则等提交成功后触发,回滚时执行次数为 0。
transaction -> publish -> [sync listener now] -> commit -> [AFTER_COMMIT listener]
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 事务监听解决触发时机,不自动解决跨进程可靠投递,可靠场景应考虑 Outbox 等方案。 |
| 适用边界 | AFTER_COMMIT 适合依赖已提交数据的副作用,但它不是可靠消息;进程在提交后处理前崩溃仍可能丢失事件。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:事务监听解决触发时机,不自动解决跨进程可靠投递,可靠场景应考虑 Outbox 等方案。
八、常见误区与追问
- 误区:@TransactionalEventListener 默认一定异步。 它控制事务阶段,执行线程仍取决于事件广播器和是否配置异步。
- 误区:发布事件天然等于异步和最终一致性。 观察者只定义一对多通知关系;同步还是异步、是否跨进程以及一致性语义都由具体实现决定。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:事务提交后监听器失败会回滚原事务吗? 原事务已提交,通常不能再回滚;应独立记录失败并设计重试或补偿。
- 追问:观察者失败后由谁重试? 模式本身没有答案;同步场景要定义异常传播,异步场景要由执行器、事件总线或消息系统提供重试与死信策略。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
Spring ApplicationEvent 可以记成“容器帮你管理观察者模式”。发布器发事件,监听器处理事件,容器负责分发;默认通常同步,事务场景要关注 @TransactionalEventListener,耗时任务再考虑异步和幂等。