比 MQ 更轻的异步方案:Spring 内置的这个隐藏功能,很多人还不知道
2026/4/21 11:12:13 网站建设 项目流程

👉这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:

  • 《项目实战(视频)》:从书中学,往事上“练”

  • 《互联网高频面试题》:面朝简历学习,春暖花开

  • 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题

  • 《精进 Java 学习指南》:系统学习,互联网主流技术栈

  • 《必读 Java 源码专栏》:知其然,知其所以然

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、IoT物联网等功能:

  • 多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • 微服务:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本

  • 问题:越写越胖的 Service 方法

  • 四个核心角色

  • 两种使用方式:接口实现 vs 注解

  • Spring 内置事件

  • 同步还是异步?

  • @TransactionalEventListener:事务提交后再执行

  • 源码解析:事件是怎么走到监听器的

  • 踩坑实录

  • Spring Event vs MQ:怎么选?

  • 生产级最佳实践

  • 回到开头那个问题


问题:越写越胖的 Service 方法

工作年头长了,见过太多这样的 Service 方法:

public void createOrder(Order order) { // 1. 保存订单 orderDao.save(order); // 2. 发送短信通知 smsService.sendOrderConfirm(order.getUserPhone(), order.getId()); // 3. 更新用户积分 pointService.addPoints(order.getUserId(), order.getTotalAmount()); // 4. 记录操作日志 auditService.log("CREATE_ORDER", order.getId()); // 5. 推送站内消息 notifyService.push(order.getUserId(), "您的订单已提交"); // 6. 触发风控检查 riskService.check(order); }

技术上没 Bug,跑得也好好的。但有一天产品说"下单后还要同步购物车",你得改;运营说"VIP 下单要额外赠券",你还得改。核心下单逻辑就这样被各种非核心需求越填越厚,所有依赖硬编码在一起。哪天riskService挂了,整个下单都跟着挂——这明显不合理。

很多人的第一反应是上 MQ。但如果是单体应用,或者只是同 JVM 内的逻辑解耦,部署一套 MQ 中间件有点杀鸡用牛刀。

Spring 早就提供了一套轻量级事件驱动机制——ApplicationEvent+@EventListener,本质是发布-订阅模式,零额外依赖,Spring Boot 开箱即用。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

四个核心角色

Spring事件机制核心组件全景:事件、发布者、广播器、监听者四大角色的关系图

1. ApplicationEvent(事件)

数据载体。继承自 JDKEventObject,包含sourcetimestamp。Spring 4.2 后支持发布任意对象(不继承也行),内部自动封装为PayloadApplicationEvent

2. ApplicationEventPublisher(发布者)

只有一个方法publishEvent()ApplicationContext实现了这个接口,注入ApplicationEventPublisher更优雅——依赖接口而非实现

3. ApplicationEventMulticaster(广播器)

整个机制的核心调度引擎。维护所有已注册的监听器列表,负责将事件分发给匹配的监听者。默认实现SimpleApplicationEventMulticaster

4. ApplicationListener(监听者)

接收并处理事件,通过实现接口或注解定义。

角色

对应组件

责任

事件

ApplicationEvent

信息载体

发布者

ApplicationEventPublisher

触发事件

广播器

ApplicationEventMulticaster

调度分发

监听者

ApplicationListener

/@EventListener

处理事件

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

两种使用方式:接口实现 vs 注解

方式一:实现 ApplicationListener 接口(传统方式)

Step 1:定义事件对象

public class OrderCreatedEvent extends ApplicationEvent { privatefinal String orderId; privatefinal Long userId; privatefinal BigDecimal totalAmount; public OrderCreatedEvent(Object source, String orderId, Long userId, BigDecimal totalAmount) { super(source); this.orderId = orderId; this.userId = userId; this.totalAmount = totalAmount; } public String getOrderId() { return orderId; } public Long getUserId() { return userId; } public BigDecimal getTotalAmount() { return totalAmount; } }

Step 2:发布事件

@Service publicclass OrderService { privatefinal ApplicationEventPublisher eventPublisher; public OrderService(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } @Transactional public Order createOrder(CreateOrderRequest req) { Order order = buildOrder(req); orderDao.save(order); eventPublisher.publishEvent( new OrderCreatedEvent(this, order.getId(), order.getUserId(), order.getTotalAmount()) ); return order; } }

Step 3:实现监听器

@Component publicclass SmsNotifyListener implements ApplicationListener<OrderCreatedEvent> { @Override public void onApplicationEvent(OrderCreatedEvent event) { smsService.send(event.getUserId(), "订单 " + event.getOrderId() + " 已提交"); } } @Component publicclass PointsListener implements ApplicationListener<OrderCreatedEvent> { @Override public void onApplicationEvent(OrderCreatedEvent event) { pointService.addPoints(event.getUserId(), event.getTotalAmount()); } }

缺点:一个监听器只能监听一种事件类型,类里只能有一个处理方法。

方式二:@EventListener 注解(推荐)

Spring 4.2 引入,灵活度大幅提升,主流写法:

@Component publicclass OrderEventHandler { @EventListener public void handleSms(OrderCreatedEvent event) { smsService.send(event.getUserId(), "订单已提交"); } // 同时监听多种事件 @EventListener(value = {OrderCreatedEvent.class, OrderPaidEvent.class}) public void handleOrderStateChange(ApplicationEvent event) { auditService.log(event); } // 条件监听:只处理金额 > 1000 元的 @EventListener(condition = "#event.totalAmount > 1000") public void handleHighValueOrder(OrderCreatedEvent event) { vipService.notifyHighValue(event.getOrderId()); } // 返回新事件:自动广播下一个 @EventListener public InventoryUpdateEvent handleOrder(OrderCreatedEvent event) { returnnew InventoryUpdateEvent(this, event.getOrderId()); } }

几个少被提及的功能:**condition** 支持 SpEL 过滤;返回值如果是事件对象会自动发布,适合事件链;@Order控制执行顺序。

@EventListener @Order(1) // 先执行 @Async public void riskCheck(OrderCreatedEvent event) { ... } @EventListener @Order(2) // 后执行 @Async public void sendSms(OrderCreatedEvent event) { ... }

Spring 内置事件

Spring 在容器生命周期关键节点也会发布事件:

Spring内置事件类型列表

事件

触发时机

常用场景

ContextRefreshedEvent

容器初始化或刷新完成

预热缓存、加载配置

ContextStartedEvent

调用context.start()

启动定时任务

ContextStoppedEvent

调用context.stop()

清理资源

ContextClosedEvent

容器关闭

优雅关机

RequestHandledEvent

HTTP 请求处理完毕

请求监控

最常用的是ContextRefreshedEvent预热缓存的经典用法:

@Component public class CachePreloadListener { @EventListener(ContextRefreshedEvent.class) public void preloadCache(ContextRefreshedEvent event) { // 父子容器场景会触发两次,需要防守 if (event.getApplicationContext().getParent() != null) { return; } CompletableFuture.runAsync(() -> { provinceService.loadAll(); hotProductService.preload(); log.info("缓存预热完成"); }); } }

Spring Boot 里有更好用的ApplicationReadyEvent,只触发一次:

@EventListener(ApplicationReadyEvent.class) public void onReady() { cacheService.initAll(); }

同步还是异步?

Spring 事件默认同步——publishEvent()会阻塞,等所有监听器执行完才继续。好处是简单、对事务友好;坏处是监听器卡顿会拖慢发布者

开启异步

三步搞定:

第一步:开启异步支持

@SpringBootApplication @EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

第二步:配置专用线程池(强烈建议,否则会用ForkJoinPool.commonPool

@Configuration public class AsyncConfig { @Bean("eventExecutor") public Executor eventTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(16); executor.setQueueCapacity(500); executor.setThreadNamePrefix("event-async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }

第三步:给监听器加 @Async

@Async("eventExecutor") @EventListener public void handleSms(OrderCreatedEvent event) { smsService.send(event.getUserId(), "订单已提交"); }
两个必知的异步局限

1. 异常不传播:异步监听器的异常在线程池线程中自行消化,发布者完全感知不到。必须在监听器内 try-catch。

2. 返回值发布失效:加了@Async后,返回事件对象不会自动发布,需手动publishEvent

@Async("eventExecutor") @EventListener public void handleSms(OrderCreatedEvent event) { try { smsService.send(event.getUserId(), "订单已提交"); } catch (Exception e) { log.error("短信发送失败,订单 ID: {}", event.getOrderId(), e); alarmService.notify("SMS_FAILED", e.getMessage()); } }

@TransactionalEventListener:事务提交后再执行

场景:用户修改订单后删缓存。如果用普通@EventListener,缓存删了但事务可能回滚——缓存击穿

@TransactionalEventListener解决这个问题:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void cleanCache(OrderUpdatedEvent event) { redisTemplate.delete("order:" + event.getOrderId()); }

phase

触发时机

AFTER_COMMIT

(默认)

事务成功提交后

AFTER_ROLLBACK

事务回滚后

AFTER_COMPLETION

事务完成后(无论成败)

BEFORE_COMMIT

事务提交前

两个隐形陷阱

1. 没有事务时事件会被丢弃

// 没有 @Transactional 的方法发布事件 public void someMethod() { eventPublisher.publishEvent(new OrderUpdatedEvent(...)); // @TransactionalEventListener 会直接丢弃! }

fallbackExecution = true解决:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void cleanCache(OrderUpdatedEvent event) { redisTemplate.delete("order:" + event.getOrderId()); }

2. AFTER_COMMIT 阶段里起不了新事务

事务已提交,加了@Transactional默认也不会新开事务,必须指定Propagation.REQUIRES_NEW

源码解析:事件是怎么走到监听器的

注册阶段

容器refresh()过程中完成两件事:

初始化广播器

protected void initApplicationEventMulticaster() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { this.applicationEventMulticaster = beanFactory.getBean(...); } else { this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); } }

注册监听器:先注册编程式添加的监听器,再从 BeanFactory 找所有ApplicationListener接口的 Bean。@EventListener注解由EventListenerMethodProcessor处理——在所有单例 Bean 初始化完成后扫描带注解的方法,封装为ApplicationListenerMethodAdapter注册到广播器。

分发阶段
// SimpleApplicationEventMulticaster#multicastEvent() public void multicastEvent(ApplicationEvent event, ResolvableType eventType) { ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); Executor executor = getTaskExecutor(); for (ApplicationListener<?> listener : getApplicationListeners(event, type)) { if (executor != null) { executor.execute(() -> invokeListener(listener, event)); } else { invokeListener(listener, event); } } }

getApplicationListeners()内部有缓存,首次调用遍历所有监听器做类型匹配,之后走缓存。

完整调用链:

publishEvent() └─ multicastEvent() └─ getApplicationListeners()(带缓存的匹配) └─ invokeListener() └─ doInvokeListener() └─ listener.onApplicationEvent() ← 你的业务代码

踩坑实录

坑 1:事件对象设计成可变的
@EventListener public void handleA(OrderCreatedEvent event) { event.setStatus("PROCESSING"); // 修改了事件对象 } @EventListener public void handleB(OrderCreatedEvent event) { // handleA 已经改了 status,这里看到脏数据 doSomething(event.getStatus()); }

多个监听器共享同一个事件对象。事件对象必须不可变:属性加final,不提供 setter。

坑 2:开了 @Async 却没开 @EnableAsync
@EventListener @Async public void asyncHandle(OrderCreatedEvent event) { // 看起来异步,实际同步执行,而且无任何报错 }

@Async不生效的表现是性能没提升却找不到原因。**@EnableAsync必须加在主类或配置类上。**

坑 3:事件循环调用
@EventListener public void handleA(EventA event) { eventPublisher.publishEvent(new EventB()); } @EventListener public void handleB(EventB event) { eventPublisher.publishEvent(new EventA()); // 死循环!StackOverflowError }

设计事件时要明确依赖关系,避免形成环。

坑 4:@TransactionalEventListener 无事务时默默丢事件

上面已经提到。没事务 + 没设fallbackExecution = true= 事件被默默丢弃,调试时极难发现。

Spring Event vs MQ:怎么选?

Spring Event 与 MQ 对比选型决策树

对比维度

Spring Event

MQ(Kafka/RocketMQ)

适用场景

同 JVM 内模块解耦

跨服务通信

可靠性

进程崩溃就丢事件

持久化 + 重试

延迟

内存级,≈ 0

受网络影响

开发成本

零依赖,注解即用

需部署中间件

一致性

本地事务保障

需分布式事务

吞吐量

异步+批量 ~9w/s

受网络并发限制

简单说:单体应用 / 同 JVM 逻辑解耦 / 不要求消息持久化 → Spring Event微服务跨服务通信 / 高可靠消息传输 → MQ

生产级最佳实践

1. 事件对象保持轻量:只传 ID,不传大对象。

public class OrderCreatedEvent extends ApplicationEvent { private final String orderId; private final String version = "1.0"; }

2. 一个监听器只做一件事PaymentListener只处理支付,CouponListener只发券。

3. 异步监听器必须自己捕异常

4. 加监控:关键链路的监听器加耗时统计和告警。

5. 事件命名要语义化:用过去式动词——OrderCreatedEventPaymentSuccessEvent。不要用DoSomethingEvent

回到开头那个问题

那个越写越胖的createOrder,用事件机制改造:

@Transactional public Order createOrder(CreateOrderRequest req) { Order order = buildOrder(req); orderDao.save(order); eventPublisher.publishEvent(new OrderCreatedEvent(this, order)); return order; } // 新增功能?加个监听器就好,不动核心代码 @Component publicclass NewFeatureListener { @EventListener public void handleNewFeature(OrderCreatedEvent event) { // 新功能在这里 } }

一个经得起考验的系统,不是靠把所有逻辑塞进一个方法里,而是让不同模块各司其职。Spring 事件机制,正是实现这种协作的工具。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。 谢谢支持哟 (*^__^*)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询