news 2026/7/13 11:41:54

Day21-Spring事件机制:观察者模式在企业级解耦中的应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Day21-Spring事件机制:观察者模式在企业级解耦中的应用

专栏:《Java后端工程师进阶之路》从CRUD到AI工程师的完整跃迁路径(Day 21/ 90):好的架构不是把代码写得复杂,而是让复杂的需求改动起来简单。

一、一个真实的故事

一个电商项目,用户下单后要做三件事:发短信、扣积分、记日志。原来的代码是这样的——OrderService里直接注入了SmsServicePointServiceLogService,三个方法串行调用,一个下单接口拉到 3 秒。更要命的是,后来产品说下单后还要同步到 BI 系统、触发风控检查……每次加需求都得改OrderService

"为什么不考虑用事件机制解耦?"

很多写了三五年 Spring Boot 的兄弟,天天用@Autowired@Transactional@Async,却从来没碰过 Spring 自带的事件机制。这个从 Spring 1.0(2004年)就存在的功能,堪称观察者模式的教科书级实现。


二、Spring事件机制的核心角色(30秒速通)

Spring 事件机制一共就三个角色,理解起来不超过 30 秒:

[事件发布者] --发布--> [ApplicationEvent] --被--> [事件监听器] 接收
角色Spring 中的体现职责
事件(Event)继承ApplicationEvent携带事件数据,是"消息体"
发布者(Publisher)ApplicationEventPublisher发布事件,谁都可以是发布者
监听器(Listener)@EventListener注解接收并处理事件

核心流程一句话:发布者把事件扔给 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.9+

3.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 监听器加 @Async

@Slf4j @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 引入的杀手级特性。普通@EventListenerpublishEvent()调用时立即执行,而@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,这种名字三个月后你自己也看不懂。用业务语言命名:OrderPaidEventUserRegisteredEventGoodsShippedEvent。事件是业务动作的投影,不是技术动作的描述。

建议二:给事件加上 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、逻辑删除、多租户插件,以及那些你在官方文档里看不到的生产坑。点关注不迷路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 11:38:30

ROS C++消息生成:从.msg定义到可发布订阅的完整流程

1. 项目概述&#xff1a;为什么“消息生成”是ROS开发绕不开的第一道门槛刚接触ROS的人&#xff0c;常以为装完系统、跑通小乌龟就等于入门了。我带过十几期线下ROS实训班&#xff0c;几乎每届都有学员卡在“写完代码编译报错&#xff0c;提示找不到头文件”&#xff0c;或者“…

作者头像 李华
网站建设 2026/7/13 11:37:34

模板驱动型文档自动化:从Word手工排版到工程化流水线

1. 这不是“点几下就出PDF”的玩具&#xff0c;而是把文档生产从手工作坊升级成流水线的底层逻辑你有没有过这种经历&#xff1a;客户要一份产品说明书&#xff0c;你翻出去年的模板&#xff0c;改标题、换参数、删掉过时的截图&#xff0c;再手动调整页眉页脚——一上午过去了…

作者头像 李华
网站建设 2026/7/13 11:37:14

3分钟解决GitHub下载慢:Fast-GitHub终极加速指南

3分钟解决GitHub下载慢&#xff1a;Fast-GitHub终极加速指南 【免费下载链接】Fast-GitHub 国内Github下载很慢&#xff0c;用上了这个插件后&#xff0c;下载速度嗖嗖嗖的~&#xff01; 项目地址: https://gitcode.com/gh_mirrors/fa/Fast-GitHub 还在为GitHub的龟速下…

作者头像 李华
网站建设 2026/7/13 11:36:54

CISP-PTE 靶场环境复现:基于Docker快速搭建5类漏洞实战平台

CISP-PTE 靶场环境复现&#xff1a;基于Docker快速搭建5类漏洞实战平台 在网络安全领域&#xff0c;渗透测试能力的提升离不开高质量的实战环境。传统搭建漏洞靶场往往面临环境配置复杂、资源占用高、难以复现等问题。本文将介绍如何利用Docker技术&#xff0c;快速构建一个包含…

作者头像 李华
网站建设 2026/7/13 11:35:41

从数据切分到精排优化:构建高精度RAG智能导购系统的全链路实践

1. 为什么香水电商需要高精度RAG导购系统 想象一下这样的场景&#xff1a;一位顾客在香水电商平台输入"适合夏天的清新玫瑰香"。传统关键词搜索可能返回所有含"玫瑰"关键词的商品&#xff0c;而普通推荐系统则依赖历史购买数据——但这都无法解决核心痛点&…

作者头像 李华