资讯动态

Spring Event实战:从业务解耦到异步监听的最佳实践

发布时间:2026/9/7 6:36:54 来源:尧图企业网站定制
简介面向Spring开发者的Spring Event事件机制示例工程适合希望降低业务模块耦合、掌握事件发布与监听流程的初中级程序员。压缩包共75个文件以Java源码为主包含事件类、发布者、监听器完整代码同时提供class编译文件、XML配置、properties及Eclipse工程文件.project、.classpath、.checkstyle等便于直接导入IDE运行调试。包体仅44KB轻量精简目录结构清晰。已有675人学习下载。示例覆盖ApplicationEvent自定义事件、EventListener注解与ApplicationListener接口两种监听方式、事件处理顺序控制、同步异步线程模型等关键知识点并附带测试场景模拟可帮助读者快速理解Spring Event在真实项目中的用法将解耦开发思路应用到自己的模块设计中也适合作为团队内部技术分享的参考工程。 最近重构订单中台我在很多模块里引入了Spring Event做解耦效果非常明显。以前用户下单成功后要发短信、发站内信、加积分、更新统计、推送报表这些逻辑全塞在createOrder方法里一个方法六七十行每加一个需求就要动老代码测试也越来越难写。改成事件机制后主流程只保留落库和发布事件其它动作全部交给监听器代码清爽扩展也容易。这篇文章不是抄官方文档而是把我实际项目里的Spring Event示例、进阶玩法和踩过的坑整理一遍适合正在做业务解耦、或者刚接触Spring事件机制的同学看完可以直接在自己项目里跑通。1. 为什么我建议你用Spring Event从一段恶魔代码说起1.1 一个典型的业务耦合问题我先拿实际项目里最常见的代码来说。很多订单服务的创建逻辑长这样public void createOrder(OrderCreateRequest request) { Order order orderDao.insert(request); // 1. 订单落库 smsService.send(request.getMobile(), 订单创建成功); // 2. 发短信 messageService.push(request.getUserId(), 订单创建成功); // 3. 发站内信 integralService.add(request.getUserId(), 100); // 4. 加积分 reportService.refreshCreateOrder(); // 5. 更新统计 }这段代码最大的问题不是长而是把订单创建和创建后的附加动作绑死在一个事务里。短信服务超时订单创建就跟着失败第4步积分加失败了整个接口报错下次要加一个送优惠券的功能还得回过来改这个方法。业务在变这个方法会一直膨胀最后变成谁都不敢动的老代码。实际上发短信、加积分、更新报表这些动作对订单创建成功这事儿来说都是旁观者。订单服务只需要把订单创建好了这个消息广播出去谁关心、谁处理是别人的事。这正是事件机制要解决的思路。1.2 观察者模式和Spring Event的对应关系Spring Event就是观察者模式在Spring框架里的落地实现。理解这套机制只需要记住三个角色事件对象描述发生了什么Spring 4.2之前必须继承ApplicationEvent现在任意POJO都能当事件省事很多。发布器ApplicationEventPublisher负责广播事件它不关心谁在监听。监听器用EventListener标注的方法或者实现ApplicationListener接口的类收到事件后执行自己的逻辑。我用点外卖来类比你下单是发布事件商家接单、骑手取餐、平台推送通知就是不同的监听器。商家不需要知道骑手存在骑手也不用关心商家怎么做饭大家各干各的互不阻塞。Spring Event的价值就是让业务模块之间从直接调用变成发布-订阅耦合自然降下来。2. 从零开始一个最简单的Spring Event示例2.1 定义事件对象在Spring 4.2之后写一个事件类就是写普通POJO不需要继承任何父类public class OrderCreatedEvent { private final Long orderId; private final String userMobile; public OrderCreatedEvent(Long orderId, String userMobile) { this.orderId orderId; this.userMobile userMobile; } public Long getOrderId() { return orderId; } public String getUserMobile() { return userMobile; } }我个人强烈建议事件字段用final修饰事件发布后不可变。原因很简单事件对象会在多个监听器之间传递如果一个监听器改了字段后面监听器读到的是被改过的值排查起来非常痛苦。命名上就按业务动作Event来比如OrderCreatedEvent、OrderPaidEvent、StockChangedEvent让人一眼就能看出发生了什么。2.2 写一个监听器监听器就是一个普通Spring Bean的方法加上EventListener注解Component public class OrderNotifyListener { EventListener public void onOrderCreated(OrderCreatedEvent event) { smsService.send(event.getUserMobile(), 您的订单已创建编号 event.getOrderId()); } }注意监听方法的方法名可以随便起关键是参数类型。Spring会在发布事件时根据事件对象运行时类型找到所有参数类型匹配的监听方法。你也可以用实现ApplicationListener接口的方式写监听器但注解方式更灵活一个类里可以同时监听多个事件还能玩条件表达式后面会讲。2.3 发布事件在业务代码里注入ApplicationEventPublisher需要发布时调用publishEventService public class OrderService { private final ApplicationEventPublisher publisher; public OrderService(ApplicationEventPublisher publisher) { this.publisher publisher; } Transactional public void createOrder(OrderCreateRequest request) { Order order orderDao.insert(request); publisher.publishEvent(new OrderCreatedEvent(order.getId(), request.getMobile())); } }默认情况下publishEvent是同步的。也就是说发布事件后Spring会找到所有匹配的监听器并逐个执行完publishEvent才会返回。如果监听器里有耗时的短信推送、第三方调用下单接口的RT会被明显拉长。这里先记住这个结论后面讲异步再解决。整个流程走一遍就是请求进入createOrder- 订单落库 - 发布事件 - Spring根据事件类型找到监听器 - 执行onOrderCreated- 返回。逻辑清晰扩展时也不需要动主流程。3. 让事件更好用异步、事务、条件控制3.1 异步监听Async 自定义线程池发短信、发站内信这类操作完全没必要阻塞主流程给监听器加上Async就能异步执行Component EnableAsync public class OrderNotifyListener { Async(notifyExecutor) EventListener public void onOrderCreated(OrderCreatedEvent event) { // 异步执行不会阻塞发布线程 smsService.send(event.getUserMobile(), 您的订单已创建编号 event.getOrderId()); } }这里有三个容易踩的坑。第一EnableAsync要记得加否则Async不生效。第二最好自定义线程池并指定名称别用默认的。Spring Boot默认会提供一个applicationTaskExecutor但它的线程数、队列长度不一定适配你的业务高并发下容易成为瓶颈。我常用的是这样的配置Bean(notifyExecutor) public Executor notifyExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(200); executor.setThreadNamePrefix(notify-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }CallerRunsPolicy的意思是线程池满了就退回到调用方线程继续执行而不是直接把任务丢掉。对通知场景来说慢一点可以接受但消息丢失不能接受。第三Async和EventListener都是靠代理实现的如果监听器在同一个类内部用this调用自己代理不生效异步就会失效这一点在排查问题时特别值得留意。3.2 事务提交后再处理TransactionalEventListener如果事件是在事务方法里发布的而监听器里要做一些对数据一致性敏感的操作比如发送MQ或者处理账务直接监听会有两个问题事务还没提交监听器查不到最新数据监听器里的数据库操作会和主事务抢连接。TransactionalEventListener就是为了解决这个时机问题TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onOrderPaid(OrderPaidEvent event) { // 订单事务提交成功后再发短信/MQ }默认的phase是AFTER_COMMIT也就是事务成功提交后才触发。除此之外还有BEFORE_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION按需选择。有个特别容易忽略的细节如果发布事件的方法不在事务里这个监听器默认是不执行的。想让它没事务时也执行得加上fallbackExecution trueTransactionalEventListener(phase TransactionPhase.AFTER_COMMIT, fallbackExecution true) public void onOrderPaid(OrderPaidEvent event) { // ... }我遇到过不止一次某个监听器不触发排查半天发现是外面包了一层事务但那个事务被切面吞掉了Transactional根本没生效。所以我把这条规则记在心里事务事件监听器没事务就静默不执行有这个特性在就一定要想清楚是否加fallbackExecution。3.3 条件监听一个监听器按条件分派EventListener注解里可以写SpringEL表达式满足条件才触发。最常见的两个场景一是按事件的某个字段过滤EventListener(condition #event.success) public void onTradeResult(TradeResultEvent event) { // 只有successtrue才会进来 }二是按业务来源分流EventListener(condition #event.source mobile) public void onChannelNotify(OrderCreatedEvent event) { // 只有来源是mobile才处理 }条件表达式看着灵活但别写太复杂。一旦SpEL表达式写错方法直接不执行而且不会报错排查成本很高。我的原则是简单条件用condition复杂逻辑就拆成多个监听器方法或者在方法体里用if判断保证可读性和可排查性。另外Spring还支持监听泛型事件。比如定义DataChangeEventT监听方法里写DataChangeEventUserSpring会按方法签名里的泛型参数匹配只收到对应泛型类型的事件。这套机制适合做通用数据变更通知但刚上手时不建议一上来就搞泛型先把基础事件跑通再逐步增加复杂度。4. 实战案例订单支付成功后的通知与积分4.1 业务拆解我们模拟一个支付回调场景逻辑包括更新订单状态为已支付、发短信通知用户、给用户加积分、更新用户等级、触发大数据报表统计。在设计上我把它们拆成几个独立环节互不干扰OrderPaidEvent携带订单ID、用户ID、支付金额、支付时间。短信监听器负责发短信。积分监听器负责加积分。报表监听器负责同步统计数据。支付回调方法只做两件事修改订单状态、发布事件。其它动作全部后移任何一个环节失败都不影响订单支付状态。4.2 核心代码定义事件public class OrderPaidEvent { private final Long orderId; private final Long userId; private final BigDecimal amount; public OrderPaidEvent(Long orderId, Long userId, BigDecimal amount) { this.orderId orderId; this.userId userId; this.amount amount; } // getter 省略 }发布事件Transactional public void handlePaid(PaidCallbackRequest request) { orderService.markPaid(request.getOrderId()); publisher.publishEvent(new OrderPaidEvent(orderId, userId, amount)); }监听器TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT, fallbackExecution true) public void onOrderPaid(OrderPaidEvent event) { smsService.sendTemplate(event.getUserId(), paySuccess, event.getAmount()); } Async(notifyExecutor) TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT, fallbackExecution true) public void onOrderPaidForIntegral(OrderPaidEvent event) { integralService.add(event.getUserId(), event.getAmount()); }这里我把短信监听做成同步事务事件保证事务提交后短信一定发出去积分监听则放到异步线程里因为积分服务偶尔抖动不想拖慢回调接口。4.3 为什么这么做这样设计最大的收益是支付回调主流程稳定了新增一个支付成功送优惠券的功能时只需要再加一个监听器完全不碰原方法。监听器之间彼此独立可以用不同的线程池、不同的失败策略扩展性很强。代价也需要说清楚事件发布和监听器执行如果不在一个线程里事务上下文、请求上下文、traceId这些都没有了监听器里拿不到RequestContextHolder这类线程绑定的数据。所以我设计事件对象时会把userId、orderId、amount这些关键业务字段全部塞进去监听器只依赖事件携带的数据不依赖线程上下文这样最稳妥。5. 常见问题与排查技巧速查5.1 监听器不触发的几个原因我把平时排查事件不执行的思路整理成一张速查表现象可能原因解决方案监听器完全没执行监听器类没被Spring扫描到检查Component、包扫描路径监听器没执行发布的事件类型和监听参数类型不匹配确认发布对象运行时类型与监听方法参数一致监听器没执行用TransactionalEventListener但发布方无事务加fallbackExecution true监听器没执行监听方法里写了SpEL condition但不满足简化条件或改在方法体内判断监听器没执行异步事件线程池异常没有日志检查异步线程池的异常处理加监控Spring匹配监听器时有一个特性要注意它是按事件对象的运行时类型去匹配的。如果发布的是子类事件监听父类的监听器也能收到。反过来如果发布的是接口类型监听具体实现类的监听器就收不到。这个特性有时挺好用比如定义一个BaseNotifyEvent所有通知类监听器都能统一处理但在泛型事件里也容易埋雷泛型参数不匹配时监听器会静默不执行排查很费劲。5.2 Async失效怎么排查Async失效是个高频问题90%的原因出在代理上。常见情况有同一个类内部this调用绕过代理EnableAsync没加监听器类没注册成Bean方法或类是final的CGLIB没法代理异步逻辑直接失效。排查技巧很直白在异步方法里打印一下当前线程名。如果和发布事件的线程一样说明代理没生效如果线程名变成了notify-之类的自定义前缀说明异步链路走通了。另外一旦用了自定义线程池可以在监控里加一个线程池状态指标比如队列积压数、活跃线程数防止线程池被打满后悄悄丢了任务。5.3 异常和执行顺序处理同步监听器里抛异常异常会一路抛到publishEvent调用处后面的监听器不会继续执行。这是个容易出大事的细节如果第一个监听器挂了后面所有监听器全部被跳过。我的处理习惯是重要的监听器内部自己捕获异常并记录日志不往外抛不重要的通知类监听器直接try-catch多个监听器都需要执行的时候优先用异步隔离线程。监听器执行顺序可以用Order控制数值越小越先执行。比如先扣减库存的监听器设Order(1)发通知的监听器设Order(100)能让关键依赖先执行完。还有一个事务上的坑在AFTER_COMMIT事务事件监听器里做远程调用要小心。事务虽然提交了但事务同步管理里可能还持有数据库连接如果远程调用很慢连接一直不释放数据库连接池会被拖垮。我一般只在这个阶段发MQ或者写本地表真正的远程RPC交给MQ消费端去做。5.4 给事件机制加一层可观测性项目里我给事件发布和监听过程加了简单的指标监控发布事件时记录事件类型、耗时和调用次数监听器执行前后也埋点。Spring Boot Actuator加Micrometer的配置很成熟通过/actuator/metrics能直接看到事件处理的情况排查问题时会省很多力气。这里也提醒一句生产环境要把监控端点保护好配上鉴权别让敏感信息裸奔。6. 项目落地后我沉淀的几点体会6.1 我在团队里定的几条规矩用Spring Event三四年了最大的感受是解耦不是银弹。事件机制让主流程变短了但也让调用链变得隐晦一个请求的完整处理路径不再是线性可读的。所以在团队里我定了几条规矩事件类统一放在event包里命名必须体现业务动作不允许出现Event1这种无意义的名字。事件对象只放必要业务字段别把Entity、HttpSession、请求对象塞进去。发布事件的时机要明确最好在事务方法内配合TransactionalEventListener控制触发时机。监听器里必须打印入参日志没有traceId就手动塞一个业务ID方便排查链路。这些规矩治不了什么大毛病但能避免项目后期事件满天飞、看代码像看迷宫的情况。6.2 除了业务解耦事件还能这样用最后分享一个我经常用的小技巧Spring Boot的ApplicationReadyEvent特别适合做应用启动后的初始化。比如缓存预热、定时任务注册、字典数据加载都可以监听ApplicationReadyEvent来做而不是在PostConstruct里做。原因是PostConstruct执行时很多依赖Bean可能还没完全准备好监听应用启动完成事件则更安全。事件机制还能用来做模块间通信。我在一个项目里把用户变更、商品变更都定义成事件缓存模块、搜索模块、消息模块各自监听彻底避免模块间互相直接调用Service。这套思路和DDD里的领域事件有点像简单场景下用Spring Event足够不需要一上来就引入消息中间件。关键是先把同步事件跑通理解发布订阅的本质再逐步上异步、上事务事件踩过的坑自己记下来后面就顺了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价