用
@TransactionalEventListener优雅地解耦"下单成功发短信"这种场景的人,不一定真搞懂了 Spring 事件机制的底层传播逻辑:事件是怎么从publishEvent传到@EventListener方法的?子容器的监听器为什么也能收到父容器的事件?事务还没提交,事件就触发了怎么办?今天从事件发布到监听器调用的完整链路,一次拆透。
一、为什么需要事件机制
先看一个没有事件的代码:
java
@Service
public class OrderService {
@Autowired
private SmsService smsService;
@Autowired
private PointService pointService;
@Autowired
private LogService logService;
@Autowired
private InventoryService inventoryService;
@Transactional
public void createOrder(OrderDTO dto) {
// 核心:创建订单
saveOrder(dto);
// 副作用:发短信、加积分、记日志、扣库存
smsService.sendOrderSuccessSms(dto.getPhone());
pointService.addPoints(dto.getUserId(), dto.getAmount());
logService.recordOrderLog(dto);
inventoryService.deduct(dto.getProductId(), dto.getQty());
}
}
问题一目了然:
耦合问题
├── OrderService 直接依赖 4 个副作用服务
├── 新增一个副作用(比如发优惠券)要改 OrderService
├── 某个副作用失败可能影响主流程
├── 无法控制副作用执行顺序
└── 单元测试需要 mock 所有依赖
用事件机制重构:
java
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher publisher;
@Transactional
public void createOrder(OrderDTO dto) {
// 核心:创建订单
saveOrder(dto);
// 发布事件,不管副作用
publisher.publishEvent(new OrderCreatedEvent(dto));
}
}
// 各副作用独立监听,互不干扰
@Component
public class SmsListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 发短信
}
}
@Component
public class PointListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 加积分
}
}
核心价值:发布者和监听者解耦,通过事件作为通信媒介。
二、Spring 事件的核心三件套
css
Spring 事件机制核心组件
│
├── 1. ApplicationEvent(事件)
│ ├── 事件载体,携带数据
│ ├── 早期必须继承 ApplicationEvent
│ └── Spring 4.2+ 可以用任意对象作为事件
│
├── 2. ApplicationEventPublisher(发布者)
│ ├── 发布事件的入口
│ ├── ApplicationContext 本身就是 Publisher
│ └── 注入即可使用
│
└── 3. ApplicationListener / @EventListener(监听者)
├── 接收并处理事件
├── 实现接口 或 标注注解
└── 可以有多个监听者监听同一事件
2.1 定义事件
java
// Spring 4.2+ 任意对象都能当事件,不需要继承 ApplicationEvent
public record OrderCreatedEvent(
Long orderId,
Long userId,
String phone,
BigDecimal amount
) {}
2.2 发布事件
java
@Service
public class OrderService {
// 方式一:直接注入 Publisher
@Autowired
private ApplicationEventPublisher publisher;
// 方式二:ApplicationContext 本身就是 Publisher
// @Autowired
// private ApplicationContext applicationContext;
public void createOrder(OrderDTO dto) {
saveOrder(dto);
// 发布事件
publisher.publishEvent(new OrderCreatedEvent(
dto.getOrderId(), dto.getUserId(),
dto.getPhone(), dto.getAmount()
));
}
}
2.3 监听事件
java
// 方式一:@EventListener 注解(推荐)
@Component
public class OrderEventListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
log.info("订单创建: {}", event.orderId());
// 处理逻辑
}
}
// 方式二:实现 ApplicationListener 接口(传统方式)
@Component
public class OrderCreatedListener
implements ApplicationListener<OrderCreatedEvent> {
@Override
public void onApplicationEvent(OrderCreatedEvent event) {
// 处理逻辑
}
}
| 对比项 | @EventListener | ApplicationListener 接口 |
|---|---|---|
| 灵活度 | 高(一个类监听多个事件) | 低(一个类只能监听一种事件) |
| SpEL 条件 | 支持 condition |
不支持 |
| 异步 | 配合 @Async 即可 | 需手动实现 |
| 代码侵入 | 低 | 需实现接口 |
| 推荐度 | ★★★★★ | ★★★ |
三、源码拆解:publishEvent 到底干了什么
这是本文的核心。很多人用了多年 publishEvent,但不知道调用后发生了什么。
3.1 整体链路
scss
publisher.publishEvent(event)
│
▼
AbstractApplicationContext.publishEvent()
│
├── 1. 获取事件类型
│
├── 2. 调用 ApplicationEventMulticaster
│ └── SimpleApplicationEventMulticaster.multicastEvent()
│ │
│ ├── 3. 获取所有匹配的 ApplicationListener
│ │ └── AbstractApplicationEventMulticaster.getApplicationListeners()
│ │ └── 通过 TypeDescriptor 做类型匹配
│ │
│ └── 4. 遍历 Listener 逐个调用
│ └── invokeListener(listener, event)
│ └── listener.onApplicationEvent(event) 或
│ MethodApplicationListenerAdapter.invoke()
│
└── 5. 父容器事件传播(如果有父容器)
3.2 publishEvent 源码
java
// AbstractApplicationContext.java
@Override
public void publishEvent(Object event) {
publishEvent(event, null);
}
protected void publishEvent(Object event, ResolvableType eventType) {
// 1. 包装事件:如果 event 不是 ApplicationEvent,用 PayloadApplicationEvent 包装
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent ae) {
applicationEvent = ae;
} else {
// ← 这就是为什么任意对象都能当事件的原因
applicationEvent = new PayloadApplicationEvent<>(this, event);
}
// 2. 通过事件广播器派发事件
if (this.earlyApplicationEvents != null) {
// 容器还没刷新完,先缓存
this.earlyApplicationEvents.add(applicationEvent);
} else {
// ← 核心调用
getApplicationEventMulticaster().multicastEvent(applicationEvent);
}
// 3. 父容器也要收到事件(事件传播)
if (this.parent != null) {
this.parent.publishEvent(event);
}
}
关键发现:
csharp
publishEvent 做了三件事
│
├── 1. 事件包装
│ └── 非 ApplicationEvent 对象 → PayloadApplicationEvent 包装
│ → 这就是为什么 record/POJO 也能当事件
│
├── 2. 事件派发
│ └── 交给 ApplicationEventMulticaster(默认 SimpleApplicationEventMulticaster)
│
└── 3. 父容器传播
└── 如果有父容器,父容器也 publishEvent
└── ← 这就是子容器发事件,父容器监听器也能收到的原因
3.3 multicastEvent 源码
java
// SimpleApplicationEventMulticaster.java
@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));
}
@Override
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
// 1. 获取所有匹配的 Listener
for (final ApplicationListener<?> listener : getApplicationListeners(event, eventType)) {
// 2. 逐个调用
Executor executor = getTaskExecutor();
if (executor != null) {
// ← 有异步线程池则异步执行
executor.execute(() -> invokeListener(listener, event));
} else {
// ← 默认同步执行
invokeListener(listener, event);
}
}
}
关键发现:
markdown
multicastEvent 的两个核心行为
│
├── 默认同步执行
│ ├── 没有配置 TaskExecutor 时,直接同步调用
│ ├── 监听器抛异常 → 传播给发布者
│ └── 监听器执行完,publishEvent 才返回
│
└── 可配置异步执行
├── 给 Multicaster 配了 TaskExecutor → 异步执行
├── 监听器抛异常不影响发布者
└── 但执行顺序不可控
3.4 监听器匹配逻辑
java
// AbstractApplicationEventMulticaster.java
protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType) {
// 缓存 key = (事件来源, 事件类型)
Object cacheKey = new CacheKey(event.getSource(), eventType);
// 先查缓存
ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
return retriever.getApplicationListeners();
}
// 没缓存则遍历所有已注册 Listener,做类型匹配
List<ApplicationListener<?>> allListeners = new ArrayList<>();
for (String beanName : this.listenerBeanNames) {
ApplicationListener<?> listener = getBean(beanName);
if (supportsEvent(listener, eventType)) {
allListeners.add(listener); // ← 类型匹配成功的才加入
}
}
// 缓存结果
// ...
return allListeners;
}
匹配的核心是 supportsEvent:
java
// 检查监听器声明的事件类型 是否和 当前发布的事件类型匹配
// 用的是泛型类型检查:如果 listener 声明的是 OrderCreatedEvent
// 只有 OrderCreatedEvent 及其子类事件才会匹配
这就是为什么不同类型的事件会精确投递到对应监听器的底层原因。
3.5 @EventListener 的解析过程
@EventListener 标注在普通方法上,Spring 怎么知道它是个监听器?
less
@EventListener 解析链路
│
├── 1. 容器启动时,EventListenerMethodProcessor 扫描所有 Bean
│
├── 2. 找到标注了 @EventListener 的方法
│
├── 3. 为每个方法创建 ApplicationListenerAdapter
│ ├── adapter 是个 ApplicationListener
│ └── onApplicationEvent() 内部反射调用目标方法
│
└── 4. 把 adapter 注册到 ApplicationEventMulticaster
└── multicastEvent 遍历时就能找到它
java
// EventListenerMethodProcessor 核心逻辑(简化)
public void afterSingletonsInstantiated() {
for (String beanName : applicationBeanNames) {
Object bean = getBean(beanName);
for (Method method : bean.getClass().getMethods()) {
EventListener annotation = method.getAnnotation(EventListener.class);
if (annotation != null) {
// 创建适配器
ApplicationListener<?> listener =
new ApplicationListenerMethodAdapter(beanName, bean, method);
// 注册到 Multicaster
context.addApplicationListener(listener);
}
}
}
}
四、同步 vs 异步:事件执行模式
4.1 默认同步
java
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 默认同步:发布事件后立即执行,执行完才返回
smsService.send(event.phone());
}
scss
同步执行时间线
├── publishEvent(event) ──→ 监听器1执行 ──→ 监听器2执行 ──→ 返回
│ ↑ 同步等待 ↑ 同步等待
└── 总耗时 = 监听器1耗时 + 监听器2耗时
同步的问题: 某个监听器慢,整个发布者都被拖慢。某个监听器抛异常,整个发布者也抛异常。
4.2 异步事件:@Async
java
@Async // ← 加这个注解
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 异步执行,不阻塞发布者
smsService.send(event.phone());
}
需要配置异步支持:
java
@Configuration
@EnableAsync // ← 开启异步
public class AsyncConfig {
@Bean("eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
4.3 三种执行模式对比
| 模式 | 配置方式 | 异常影响 | 执行顺序 | 适用场景 |
|---|---|---|---|---|
| 同步 | 默认 | 影响发布者 | 按注册顺序 | 强一致场景 |
| @Async 异步 | @EventListener + @Async | 不影响发布者 | 不可控 | 通知类副作用 |
| 全局异步 | 给 Multicaster 配 Executor | 不影响发布者 | 不可控 | 全部异步化 |
全局异步配置(不推荐,影响面太大)
│
├── 给 SimpleApplicationEventMulticaster 设置 TaskExecutor
├── 所有事件都异步执行
├── Spring 内部事件(如 ContextRefreshedEvent)也会异步
└── 可能导致初始化时序问题
推荐:保持全局同步,个别监听器用 @Async 异步。
五、@EventListener 的高级用法
5.1 SpEL 条件过滤
java
@EventListener(condition = "#event.amount > 1000")
public void onLargeOrder(OrderCreatedEvent event) {
// 只有金额 > 1000 的订单才会触发
log.info("大额订单: {}", event.orderId());
}
java
@EventListener(condition = "#event.userId != null")
public void onOrderWithUser(OrderCreatedEvent event) {
// 只有 userId 不为空才触发
}
SpEL 条件的底层: multicaster 会把匹配到的监听器中带 condition 的再过滤一遍,condition 为 true 才真正调用。
5.2 监听器返回值:变成新事件
java
@EventListener
public AuditLogEvent onOrderCreated(OrderCreatedEvent event) {
// 返回值会被当作新事件发布!
return new AuditLogEvent("ORDER_CREATED", event.orderId());
}
@EventListener
public void onAuditLog(AuditLogEvent event) {
// 会收到上面返回的 AuditLogEvent
log.info("审计日志: {}", event);
}
注意: 返回值会触发新事件,可能导致循环事件(A 返回 B,B 返回 A),要小心设计。
5.3 一个方法监听多个事件
java
@EventListener({OrderCreatedEvent.class, OrderUpdatedEvent.class})
public void onOrderChange(Object event) {
// 参数类型不能是具体事件,只能用共同父类或 Object
if (event instanceof OrderCreatedEvent e) {
// 处理创建
} else if (event instanceof OrderUpdatedEvent e) {
// 处理更新
}
}
六、事务事件:@TransactionalEventListener
这是 Spring 事件最实用的特性之一,解决了一个经典问题:
6.1 问题:事务还没提交,事件就发了
java
@Transactional
public void createOrder(OrderDTO dto) {
saveOrder(dto);
publisher.publishEvent(new OrderCreatedEvent(dto));
// ↑ 事件在同事务内同步触发!
// 此时事务还没 commit!
}
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 如果这里查数据库,查不到订单(事务没提交)
orderMapper.selectById(event.orderId()); // ← null!
}
css
普通 @EventListener 的问题
│
├── 事务内 publishEvent → 监听器同步执行
│ ├── 此时事务尚未提交
│ ├── 监听器查数据库 → 查不到
│ └── 如果监听器也操作同一数据库 → 同一事务
│
└── 事务回滚 → 监听器已经执行了
├── 发了短信 → 订单没下成功
├── 加了积分 → 订单没下成功
└── 数据不一致
6.2 解决:@TransactionalEventListener
java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// ← 事务提交后才执行!
// 此时查数据库能查到订单
orderMapper.selectById(event.orderId()); // ← 有数据
smsService.send(event.phone());
}
6.3 四种事务阶段
markdown
事务事件阶段
│
├── BEFORE_COMMIT
│ ├── 事务提交前执行
│ └── 典型场景:提交前校验
│
├── AFTER_COMMIT(最常用)
│ ├── 事务成功提交后执行
│ ├── 此时数据库已可见
│ └── 典型场景:发送通知、写日志、触发下游
│
├── AFTER_ROLLBACK
│ ├── 事务回滚后执行
│ └── 典型场景:补偿操作、告警
│
└── AFTER_COMPLETION
├── 事务完成(提交或回滚)后执行
└── 典型场景:清理资源
| 对比项 | @EventListener | @TransactionalEventListener |
|---|---|---|
| 触发时机 | publishEvent 立即 | 取决于事务阶段 |
| 数据可见性 | 事务未提交,不可见 | AFTER_COMMIT 时可见 |
| 无事务时 | 正常触发 | 默认不触发(可配置) |
| 事务回滚时 | 已触发 | AFTER_ROLLBACK 触发 |
6.4 无事务时的行为
java
// 如果 publishEvent 时没有事务上下文
// @TransactionalEventListener 默认不执行!
publisher.publishEvent(new OrderCreatedEvent(dto));
// ← 没有事务,监听器被跳过
// 配置 fallbackExecution = true 可以在没有事务时也执行
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = true // ← 没事务也执行
)
public void onOrderCreated(OrderCreatedEvent event) {
// 有事务 → 提交后执行
// 无事务 → 直接触发
}
6.5 底层原理
ini
@TransactionalEventListener 底层原理
│
├── 1. 注册时不直接注册为 ApplicationListener
│
├── 2. 而是注册为 TransactionSynchronization 的回调
│
├── 3. publishEvent 时,事件被 TransactionApplicationListener 接收
│ └── 检查是否有事务
│ ├── 有事务 → 注册 TransactionSynchronization
│ │ └── 在事务的 afterCommit/afterCompletion 回调中执行
│ └── 无事务 → fallbackExecution=true 则直接执行,否则跳过
│
└── 4. 关键依赖:TransactionSynchronizationManager
└── 这是 Spring 事务管理的核心工具类
七、事件传播:父子容器
Spring MVC 经典的父子容器结构:ContextLoaderListener 创建父容器(Service/DAO),DispatcherServlet 创建子容器(Controller)。
markdown
父子容器事件传播
│
├── 父容器 publishEvent
│ └── 只通知父容器的监听器(不向下传播到子容器)
│
└── 子容器 publishEvent
├── 通知子容器的监听器
└── 向上传播到父容器 → 父容器的监听器也收到
java
// 父容器的监听器
@Component
public class ParentListener {
@EventListener
public void handle(OrderCreatedEvent event) {
// 子容器发的事件也能收到
log.info("父容器收到: {}", event);
}
}
// 子容器的监听器
@Component
public class ChildListener {
@EventListener
public void handle(OrderCreatedEvent event) {
// 只能收到子容器发的,父容器发的也能收到(父publishEvent不向上)
// 等等,实际上:子容器发的事件会向上传播
log.info("子容器收到: {}", event);
}
}
这个设计的原因: 子容器能看到父容器的 Bean,但父容器看不到子容器的 Bean。事件传播方向和 Bean 可见性一致。
kotlin
事件传播方向 vs Bean 可见性
│
├── Bean 可见性:子能看父的 Bean,父看不到子的 Bean
│
├── 事件传播:
│ ├── 子→父:✅ 传播(子发的事件,父能收到)
│ ├── 父→子:❌ 不传播(父发的事件,子收不到)
│ └── 和 Bean 可见性方向一致:信息从大范围流向小范围
│
└── 源码位置:AbstractApplicationContext.publishEvent 中
└── if (this.parent != null) { this.parent.publishEvent(event); }
八、常见坑和最佳实践
坑 1:同步事件阻塞主流程
java
// ❌ 慢操作放在同步监听器里
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// HTTP 调用,3秒
thirdPartyApi.notify(event.orderId());
// 发布者同步等待 3 秒
}
// ✅ 用 @Async 异步或用 @TransactionalEventListener + @Async
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
thirdPartyApi.notify(event.orderId());
}
坑 2:@TransactionalEventListener 不触发
ini
排查清单
├── 检查是否有事务上下文(@Transactional 或事务管理器配置)
├── 检查是否配置了 fallbackExecution = true(无事务时)
├── 检查事务是否真的提交了(而不是回滚了)
├── 检查事件类型和方法参数是否匹配
└── 检查是否有异常被吞了(事务回滚但没报错信息)
坑 3:事件循环
java
// ❌ A 返回 B,B 返回 A → 死循环
@EventListener
public B onA(A event) {
return new B();
}
@EventListener
public A onB(B event) {
return new A();
}
// → StackOverflowError
坑 4:监听器异常影响主流程
java
// 同步模式下,监听器抛异常 → 发布者也抛异常
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
throw new RuntimeException("短信服务挂了");
// ← 发布者也会抛异常,事务可能回滚!
}
最佳实践总结
| # | 实践 | 说明 |
|---|---|---|
| 1 | 副作用用 @TransactionalEventListener(AFTER_COMMIT) | 确保主事务提交后才执行副作用 |
| 2 | 耗时操作加 @Async | 不阻塞主流程 |
| 3 | 事件对象用 record/不可变对象 | 避免监听器修改事件数据 |
| 4 | 监听器内加 try-catch | 避免个别监听器影响其他监听器 |
| 5 | 不要在监听器里操作同库同事务 | 副作用操作应独立事务 |
| 6 | 条件监听用 condition 属性 | 精确控制触发条件 |
| 7 | 无事务场景配 fallbackExecution | 避免"有事务才触发"的坑 |
| 8 | 不要用事件做流程控制 | 事件是"通知",不是"调用" |
九、事件机制 vs 消息队列
很多人会纠结:什么时候用 Spring Event,什么时候上 MQ?
vbnet
Spring Event vs MQ
│
├── Spring Event
│ ├── 同 JVM 内通信
│ ├── 没有持久化(重启丢失)
│ ├── 无重试机制(需自己实现)
│ ├── 性能高(直接方法调用)
│ └── 适合:模块解耦、架构内通知
│
└── MQ(Kafka / RabbitMQ / RocketMQ)
├── 跨进程/跨服务通信
├── 消息持久化
├── 自动重试和死信队列
├── 需要额外基础设施
└── 适合:微服务间通信、可靠性要求高
| 对比项 | Spring Event | 消息队列 |
|---|---|---|
| 通信范围 | 单 JVM | 跨进程/跨服务 |
| 持久化 | 无 | 有 |
| 可靠性 | 低(重启丢失) | 高 |
| 性能 | 高 | 受网络影响 |
| 复杂度 | 低 | 中~高 |
| 重试 | 需自己实现 | 内置 |
| 适用场景 | 模块解耦、通知 | 分布式通信 |
实践建议:单体应用用 Spring Event 解耦模块,微服务用 MQ 通信,两者不矛盾。
十、总结
java
Spring 事件机制知识图谱
│
├── 核心组件
│ ├── ApplicationEvent / 任意 POJO → 事件载体
│ ├── ApplicationEventPublisher → 发布入口
│ └── @EventListener / ApplicationListener → 监听器
│
├── 源码链路
│ ├── publishEvent → multicastEvent → invokeListener
│ ├── 任意对象 → PayloadApplicationEvent 包装
│ └── 父容器事件向上传播
│
├── 执行模式
│ ├── 默认同步(监听器阻塞发布者)
│ ├── @Async 异步(不阻塞但顺序不可控)
│ └── 全局异步(不推荐)
│
├── 事务事件
│ ├── @TransactionalEventListener
│ ├── AFTER_COMMIT 最为常用
│ ├── fallbackExecution 处理无事务场景
│ └── 底层依赖 TransactionSynchronization
│
└── 最佳实践
├── 副作用用 AFTER_COMMIT + @Async
├── 监听器加 try-catch 防止级联失败
├── 事件对象用 record 保证不可变
└── 不要用事件做流程控制
本文是 Java 技术系列第 7 篇,系列目录:
- Spring 循环依赖到底怎么解的?三级缓存源码拆解
- Spring 事务为什么老失效?7 种场景一次讲清
- Spring Boot 自动配置到底怎么生效的?源码级拆解
- Spring AOP 到底用 JDK 还是 CGLIB?代理选择机制源码拆解
- 线上 Full GC 频发怎么救?JVM 内存模型与 GC 调优一次讲透
- 分页插件到底怎么拦截 SQL?MyBatis-Plus 插件机制原理一次讲透
- 本文:Spring 事件机制到底怎么传播的?ApplicationEvent 源码拆解