Day21-Spring事件机制:观察者模式在企业级解耦中的应用
专栏《Java后端工程师进阶之路》从CRUD到AI工程师的完整跃迁路径Day 21/ 90好的架构不是把代码写得复杂而是让复杂的需求改动起来简单。一、一个真实的故事一个电商项目用户下单后要做三件事发短信、扣积分、记日志。原来的代码是这样的——OrderService里直接注入了SmsService、PointService、LogService三个方法串行调用一个下单接口拉到 3 秒。更要命的是后来产品说下单后还要同步到 BI 系统、触发风控检查……每次加需求都得改OrderService。为什么不考虑用事件机制解耦很多写了三五年 Spring Boot 的兄弟天天用Autowired、Transactional、Async却从来没碰过 Spring 自带的事件机制。这个从 Spring 1.02004年就存在的功能堪称观察者模式的教科书级实现。二、Spring事件机制的核心角色30秒速通Spring 事件机制一共就三个角色理解起来不超过 30 秒[事件发布者] --发布-- [ApplicationEvent] --被-- [事件监听器] 接收角色Spring 中的体现职责事件Event继承ApplicationEvent携带事件数据是消息体发布者PublisherApplicationEventPublisher发布事件谁都可以是发布者监听器ListenerEventListener注解接收并处理事件核心流程一句话发布者把事件扔给 Spring 容器Spring 找到所有对这个事件感兴趣的监听器逐个调用。发布者不需要知道谁是监听者监听者也不需要知道谁发的。这就是解耦的本质。// 极简版演示 Component public class OrderService { Autowired private ApplicationEventPublisher publisher; Transactional public void placeOrder(Order order) { // 核心业务保存订单 orderRepository.save(order); // 发布事件谁关心谁自己来拿 publisher.publishEvent(new OrderPlacedEvent(order)); // OrderService 不需要知道短信怎么发、积分怎么扣 } }三、从零搭建 Spring 事件体系完整代码环境Spring Boot 3.2 | JDK 17 | Maven 3.93.1 自定义事件类Spring 4.2 之后你不需要继承ApplicationEvent随便一个 POJO 就能当事件用。但我建议仍然继承——它能强迫你显式定义事件源source让事件有意义。import lombok.Getter; import org.springframework.context.ApplicationEvent; /** * 订单已支付事件 * 携带事件源订单实体和事件时间戳 */ Getter public class OrderPaidEvent extends ApplicationEvent { private final Long orderId; // 订单ID private final Long userId; // 用户ID private final BigDecimal amount; // 支付金额 private final LocalDateTime paidAt; // 支付时间 public OrderPaidEvent(Object source, Long orderId, Long userId, BigDecimal amount) { super(source); this.orderId orderId; this.userId userId; this.amount amount; this.paidAt LocalDateTime.now(); } }要点事件类应该是不可变的——所有字段用final通过构造器注入。事件一旦发布就不该被修改这是事件溯源Event Sourcing的基本纪律。3.2 编写监听器import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; Slf4j Component public class OrderEventListener { /** * 发送支付成功短信 * 默认是同步执行的——会阻塞发布者线程 */ EventListener public void handleSendSms(OrderPaidEvent event) { log.info(发送支付成功短信用户[{}] 订单[{}] 金额[{}], event.getUserId(), event.getOrderId(), event.getAmount()); // 实际调用短信服务 smsService.sendPaymentSuccess(event.getUserId(), event.getOrderId()); } /** * 扣减积分 */ EventListener public void handleDeductPoints(OrderPaidEvent event) { log.info(扣减积分用户[{}] 消费[{}]积分{}, event.getUserId(), event.getAmount(), event.getAmount().multiply(new BigDecimal(0.1))); pointService.addPoints(event.getUserId(), event.getAmount()); } /** * 记录操作日志条件监听只处理金额大于100的大额订单 */ EventListener(condition #event.amount 100) public void handleLogForLargeOrder(OrderPaidEvent event) { log.warn(大额订单记录订单[{}] 用户[{}] 金额[{}], event.getOrderId(), event.getUserId(), event.getAmount()); auditLogService.record(event); } }这里有个细节值得说一下EventListener(condition #event.amount 100)这是 Spring 通过SpEL 表达式实现的条件过滤。事件会被发布给所有监听器但只有满足条件的方法才会真正执行。它比在方法体内写if更干净——因为职责分离了条件是事件的属性不是监听器的逻辑。3.3 发布事件Service public class PaymentService { Autowired private ApplicationEventPublisher publisher; // 如果不想依赖 Spring 接口实现 ApplicationEventPublisherAware 也行 // 但 ApplicationEventPublisherAware 已经过时了直接用注入就好 Transactional public void pay(Long orderId) { Order order orderRepository.findById(orderId) .orElseThrow(() - new OrderNotFoundException(orderId)); order.setStatus(OrderStatus.PAID); orderRepository.save(order); // 构造事件并发布 OrderPaidEvent event new OrderPaidEvent( this, orderId, order.getUserId(), order.getAmount() ); publisher.publishEvent(event); log.info(订单[{}]支付完成事件已发布, orderId); } }执行流程如下PaymentService.pay(orderId) ↓ orderRepository.save(order) ← 落库 ↓ publisher.publishEvent(event) ← 发布事件 ↓ Spring 遍历所有 EventListener ↓ handleSendSms(event) ← 同步执行 handleDeductPoints(event) ← 同步执行 handleLogForLargeOrder(event) ← 条件满足才执行 ↓ pay() 方法返回默认情况下事件处理是同步的。这意味着上面三个监听器会顺序执行pay()方法要等所有监听器执行完才返回。如果发短信花了 2 秒扣积分花了 1 秒整个支付接口就要 3 秒。这就是为什么要引入异步事件。四、异步事件让下单回到毫秒级4.1 开启异步支持Configuration EnableAsync // ← 这一步很多人忘 public class AsyncConfig { /** * 自定义事件线程池 * 不配置的话 Spring 会用 SimpleAsyncTaskExecutor * 那玩意每来一个任务就 new 一个线程——在生产环境会炸 */ Bean(name eventTaskExecutor) public Executor eventTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); // 核心线程数 executor.setMaxPoolSize(10); // 最大线程数 executor.setQueueCapacity(100); // 队列容量 executor.setThreadNamePrefix(event-); // 线程名前缀排查问题时救命 executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy() ); // 拒绝策略交给调用线程执行不会丢任务 executor.initialize(); return executor; } }4.2 监听器加 AsyncSlf4j Component public class OrderEventListener { Async(eventTaskExecutor) // 指定线程池 EventListener public void handleSendSms(OrderPaidEvent event) { // 现在这个方法会在独立线程中执行 // pay() 方法不用等它完成就能返回 smsService.sendPaymentSuccess(event.getUserId(), event.getOrderId()); } Async(eventTaskExecutor) EventListener Order(2) // ← 控制执行顺序数字越小越先执行 public void handleDeductPoints(OrderPaidEvent event) { pointService.addPoints(event.getUserId(), event.getAmount()); } Async(eventTaskExecutor) EventListener Order(1) public void handleLog(OrderPaidEvent event) { auditLogService.record(event); // 日志先记积分别丢 } }加上Async后完整的执行时序变成了这样pay() event-1 线程 event-2 线程 │ ├─ save(order) ├─ publishEvent(event) ──┬──────────────────────┬──────────────── │ │ handleLog() │ handleSms() │ │ handlePoints() │ ├─ return 支付成功 │ │ │ ↓ (异步继续) ↓ (异步继续) ▼ HTTP 响应 100ms 记录日志... 发送短信...pay()方法把事件扔出去就返回了具体的事情由线程池慢慢做。支付接口的响应时间从「下单短信积分日志」降到了「下单发布事件」用户体验立竿见影。但是异步有一个坑——如果支付事务回滚了怎么办五、事务绑定事件TransactionalEventListener 的正确姿势这是 Spring 4.2 引入的杀手级特性。普通EventListener在publishEvent()调用时立即执行而TransactionalEventListener可以指定在事务的哪个阶段执行5.1 四种事务阶段Slf4j Component public class TransactionalOrderListener { /** * AFTER_COMMIT默认事务提交后执行 * 适用场景事务性操作完成后的通知 * 如果事务回滚这个监听器根本不会触发 */ TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void afterCommit(OrderPaidEvent event) { log.info(事务已提交开始发送短信通知); smsService.sendPaymentSuccess(event.getUserId(), event.getOrderId()); // 此时能保证数据库里已经有这条订单了 } /** * AFTER_ROLLBACK事务回滚后执行 * 适用场景失败补偿、告警 */ TransactionalEventListener(phase TransactionPhase.AFTER_ROLLBACK) public void afterRollback(OrderPaidEvent event) { log.error(订单[{}]支付回滚触发告警, event.getOrderId()); alertService.sendAlert(订单支付异常, event.getOrderId()); } /** * AFTER_COMPLETION不管成功还是失败都执行 * 适用场景清理资源、取消锁定 */ TransactionalEventListener(phase TransactionPhase.AFTER_COMPLETION) public void afterCompletion(OrderPaidEvent event) { log.info(支付事务完成成功或失败释放库存锁定); inventoryService.unlock(event.getOrderId()); } /** * BEFORE_COMMIT事务提交前执行 * 适用场景最后的校验 * 注意这里抛异常会导致事务回滚 */ TransactionalEventListener(phase TransactionPhase.BEFORE_COMMIT) public void beforeCommit(OrderPaidEvent event) { // 提交前再次校验库存 if (!inventoryService.check(event.getOrderId())) { throw new InsufficientInventoryException(库存不足); } } }5.2 完整示例支付业务全流程结合事务绑定事件 异步支付业务的完整流程应该是这样的Service public class PaymentService { Autowired private ApplicationEventPublisher publisher; /** * 支付方法 * 核心思路事务内只管「数据一致性」事务外的事交给事件 */ Transactional(rollbackFor Exception.class) public PaymentResult pay(Long orderId) { // 1. 事务内确保数据一致性 Order order orderRepository.findByIdForUpdate(orderId); // SELECT FOR UPDATE if (order.getStatus() ! OrderStatus.PENDING) { throw new OrderStatusException(订单状态异常); } order.setStatus(OrderStatus.PAID); order.setPaidAt(LocalDateTime.now()); orderRepository.save(order); // 2. 发布事件注意TransactionalEventListener 的事件 // 只有事务提交后才会被消费这里只是登记 publisher.publishEvent(new OrderPaidEvent( this, orderId, order.getUserId(), order.getAmount() )); // 3. 快速返回 return PaymentResult.success(orderId); } // 事务在此提交 // ↓ // TransactionalEventListener(AFTER_COMMIT) 开始执行 // → 异步线程池发送短信、扣积分、记日志、同步BI... }对比改造前后的区别维度改造前紧耦合改造后事件驱动支付接口耗时~3000ms~100ms新增下游逻辑修改 PaymentService新增 Listener不改现有代码事务一致性短信失败但支付成功事务提交后才发通知异常隔离短信挂了支付也挂短信挂了不影响支付可测试性需要 mock 所有依赖只需要验证事件是否发布六、三个实战建议建议一事件命名体现业务语义不要叫DataChangedEvent或者UpdateEvent这种名字三个月后你自己也看不懂。用业务语言命名OrderPaidEvent、UserRegisteredEvent、GoodsShippedEvent。事件是业务动作的投影不是技术动作的描述。建议二给事件加上 traceId在分布式系统中一个请求的链路可能跨越多个服务。事件机制让这个链路更复杂了。在构造事件时把 MDC 中的traceId塞进去public OrderPaidEvent(Object source, Long orderId) { super(source); this.orderId orderId; this.traceId MDC.get(traceId); // 关键传递追踪ID }在监听器里把 traceId 放回 MDC这样日志就能串联起来了。建议三事件数量不是越多越好我见过有项目里一个下单操作发布了 23 个事件——每个事件对应一个微小的监听逻辑。这不叫解耦这叫隐藏的意大利面条。一个好的判断标准是如果没有这个事件下游能不能正常工作如果答案是「能」那这个事件就是多余的。事件应该承载必要的业务副作用而不是替代方法调用。Spring 事件机制不是什么新概念——2004 年就有了。但用好它能让你的代码在业务复杂度增长时保持足够的内聚性。好的架构不是把代码写得复杂而是让复杂的需求改动起来简单。下篇预告Day 22我们将进入持久层实战聊聊MyBatis-Plus 的深度实践——动态 SQL、逻辑删除、多租户插件以及那些你在官方文档里看不到的生产坑。点关注不迷路。