Spring 事件发布-订阅机制
Spring 事件发布-订阅机制
一个业务动作(注册、下单、支付)往往要顺带触发一堆下游动作(发邮件、写日志、记统计、推消息)。把这些动作一个个
new出来并直接调用,主流程会被拖得又长又脏。Spring 的事件机制就是用来把这些"顺带的事"从主流程里摘出去的。
一、它解决什么问题
典型场景:用户注册成功。
- 写一条"用户注册"操作日志
- 发一封欢迎邮件
- 给统计系统上报新增用户数
- 通知风控系统做新用户画像
如果这些逻辑都写在 register() 方法里,注册这个动作就和"发邮件""记日志"强耦合——改一处,处处受影响;其中一个下游慢,注册接口就跟着慢;其中一个抛异常,注册直接失败。
事件机制把结构改成:
register() → 发布 UserRegisteredEvent → [监听器A] [监听器B] [监听器C]
主流程只管发布,下游各自订阅、互不知道对方存在
发布者不关心谁在听、听的人干了什么。这是发布-订阅(Pub/Sub)模式在 Spring 容器内的落地。
二、四个核心角色
| 角色 | 接口 / 类 | 职责 |
|---|---|---|
| 事件 | ApplicationEvent(或其子类,Spring 4.2+ 后也可以是任意 POJO) |
承载"发生了什么"的数据载体 |
| 发布者 | ApplicationEventPublisher |
调用 publishEvent(...) 把事件丢出去 |
| 广播器 | ApplicationEventMulticaster |
真正干活的中枢:找到所有匹配的监听器并逐个通知 |
| 监听器 | ApplicationListener 或 @EventListener 方法 |
收到事件后执行自己的逻辑 |
关键认知:你调用的是 publishEvent,但真正遍历监听器、决定同步还是异步、处理异常的,是容器里的 ApplicationEventMulticaster。默认实现是 SimpleApplicationEventMulticaster。
发布者与广播器的关系:
ApplicationEventPublisher.publishEvent(e)
│
▼
AbstractApplicationContext → 把活儿转交给
│
▼
ApplicationEventMulticaster.multicastEvent(e)
│ 找匹配监听器
▼
ApplicationListener.onApplicationEvent(e) / @EventListener 方法
三、三种监听写法(演进)
写法 1:实现 ApplicationListener 接口(早期写法)
@Component
public class WelcomeEmailListener implements ApplicationListener<UserRegisteredEvent> {
@Override
public void onApplicationEvent(UserRegisteredEvent event) {
System.out.println("发欢迎邮件给:" + event.getUsername());
}
}
早期(Spring 4.2 之前)的标准写法。一个类只处理一种事件,存在感强但略啰嗦。
写法 2:@EventListener 注解(推荐)
@Component
public class UserListeners {
@EventListener
public void recordLog(UserRegisteredEvent event) {
System.out.println("写审计日志:" + event.getUsername());
}
@EventListener
public void reportStats(UserRegisteredEvent event) {
System.out.println("上报统计:新增用户 " + event.getUsername());
}
}
一个类里写多个方法,每个方法处理一种事件,靠方法参数类型自动匹配事件,不用再写一堆接口实现类。这是现在的主流写法。
写法 3:异步监听 @Async
@Component
public class UserListeners {
@EventListener
@Async
public void sendEmail(UserRegisteredEvent event) {
// 发邮件可能很慢,丢到别的线程执行
System.out.println("异步发邮件给:" + event.getUsername());
}
}
@Async 需要类上或配置类上加 @EnableAsync。加了之后,这个监听器在独立线程跑,不阻塞注册主流程。(前提与坑见第五节)
四、最小可运行示例(Spring Boot)
1. 事件对象
public class UserRegisteredEvent extends ApplicationEvent {
private final String username;
// source 通常是发布事件的那个 Bean(约定而已,填 this 即可)
public UserRegisteredEvent(Object source, String username) {
super(source);
this.username = username;
}
public String getUsername() {
return username;
}
}
Spring 4.2+ 起,事件不必继承
ApplicationEvent。直接publishEvent(new UserRegisteredEvent(...))也行,Spring 会用PayloadApplicationEvent包一层再广播。继承ApplicationEvent的好处是能拿到getTimestamp()等基类信息,且语义更清晰。
2. 发布者
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher publisher;
public void register(String username) {
// 1. 主流程:落库等核心逻辑
System.out.println("注册用户:" + username);
// 2. 发布事件,剩下的交给监听器
publisher.publishEvent(new UserRegisteredEvent(this, username));
}
}
ApplicationEventPublisher 由容器注入;也可以直接注入 ApplicationContext(它继承了该接口)。
3. 监听器
@Component
public class UserListeners {
@EventListener
public void recordLog(UserRegisteredEvent event) {
System.out.println("[日志] " + event.getUsername());
}
@EventListener
public void sendWelcome(UserRegisteredEvent event) {
System.out.println("[邮件] " + event.getUsername());
}
}
调用 userService.register("alice"),控制台会按顺序打印:
注册用户:alice
[日志] alice
[邮件] alice
主流程完全没碰"写日志""发邮件"的代码——解耦达成。
五、同步还是异步(重点)
默认是同步的:监听器在 publishEvent 调用者所在的同一个线程里执行,一个接一个跑完,才返回 register()。
| 行为 | 同步(默认) | 异步(配 @Async 或自定义广播器) |
|---|---|---|
| 执行线程 | 与发布者同一线程 | 线程池里的其他线程 |
| 主流程是否被阻塞 | 是(监听器没跑完,publishEvent 不返回) |
否(监听器后台跑,主流程立即继续) |
| 监听器抛异常 | 会传播回 publishEvent,可能让发布者失败 |
只在该监听线程内抛,主流程无感(但需自己兜底异常) |
| 执行顺序 | 受 @Order 控制,可预期 |
不保证顺序 |
两个让事件异步化的途径:
途径 A:方法上加 @Async(粒度细,逐个监听器控制)
每个想异步的监听方法上加 @Async + @EnableAsync。缺点:监听器多了要一个个加。
途径 B:替换广播器(全局生效,整个应用的事件都异步)
@Configuration
public class AsyncEventConfig {
@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster multicaster =
new SimpleApplicationEventMulticaster();
multicaster.setTaskExecutor(
Executors.newThreadPerTaskExecutor(
Thread.ofVirtual().factory()));
return multicaster;
}
}
注意:
Thread.ofVirtual()是 Java 21+ 的 API,Java 8/11/17 会无法编译。低版本环境请用传统ThreadPoolTaskExecutor,兼容性更好(需要 importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor):
@Configuration
public class AsyncEventConfig {
@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster multicaster =
new SimpleApplicationEventMulticaster();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-");
executor.initialize();
multicaster.setTaskExecutor(executor);
return multicaster;
}
}
关键点:Bean 名字必须是
applicationEventMulticaster,容器启动时会用它替换掉默认的那个同步广播器。一旦替换,所有事件都走线程池,除非你显式想要同步,否则要谨慎。
异步的坑:监听器跑在别的线程,原事务、原 ThreadLocal(如用户登录态、TraceId)统统带不过去。需要这些上下文的下游逻辑,异步前要想清楚。
六、监听器执行顺序
多个监听器监听同一事件时,用 @Order 控制先后:
@EventListener
@Order(1)
public void recordLog(UserRegisteredEvent event) { ... }
@EventListener
@Order(2)
public void sendWelcome(UserRegisteredEvent event) { ... }
@Order 值越小越先执行。只对同步监听器有效;异步监听器由线程池调度,顺序不可预期。
七、事务绑定事件 @TransactionalEventListener
这是最容易被忽略、却最实用的特性。
问题:注册方法里先 INSERT 用户,再 publishEvent。如果监听器同步执行,此时事务可能还没提交——监听器去查刚插入的用户,会查不到(脏读/未提交读),或下游发消息时数据库里还没有这条记录。
@TransactionalEventListener 让监听器等到事务到达指定阶段才触发:
阶段 phase |
触发时机 |
|---|---|
BEFORE_COMMIT |
事务提交前 |
AFTER_COMMIT(默认) |
事务成功提交后 |
AFTER_ROLLBACK |
事务回滚后 |
AFTER_COMPLETION |
事务结束(提交或回滚都算) |
@Component
public class UserListeners {
// 等注册事务真正提交成功,才去发通知
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void notifyDownstream(UserRegisteredEvent event) {
System.out.println("事务已提交,安全通知下游:" + event.getUsername());
}
}
两个要点:
- 默认
AFTER_COMMIT:事务回滚了,监听器就不执行——避免"用户没注册成功却发了欢迎邮件"的尴尬。 fallbackExecution:默认false。若发布事件时根本不在事务里(没有@Transactional),监听器不会执行。想"没事务也照常跑",设fallbackExecution = true:
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = true)
public void notifyDownstream(UserRegisteredEvent event) { ... }
八、事件过滤 condition
@EventListener 支持 condition,用 SpEL 表达式按事件属性决定是否处理:
// 只有当事件对象的 amount > 100 时才执行
@EventListener(condition = "#event.amount > 100")
public void handleLargeOrder(OrderCreatedEvent event) { ... }
- 事件是个普通 POJO:
#event指代方法参数,#event.amount取其字段。 - 事件继承了
ApplicationEvent:同样用#event访问参数;想拿getSource()可用#root.source。
九、容器内置的生命周期事件
Spring 容器自身在启动/关闭时也会发事件,常用于"容器就绪后做初始化":
| 事件 | 触发时机 |
|---|---|
ContextRefreshedEvent |
ApplicationContext 初始化或刷新完成(所有 Bean 已加载) |
ContextStartedEvent |
调用 start() 后 |
ContextStoppedEvent |
调用 stop() 后 |
ContextClosedEvent |
调用 close() 后 |
ApplicationReadyEvent |
Spring Boot 应用完全启动就绪(常用来做启动后的预热/打点) |
ApplicationFailedEvent |
启动失败 |
常用写法:
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
System.out.println("应用已就绪,可以开始干活了");
}
注意
ContextRefreshedEvent在父子容器/多次刷新场景下可能触发多次;需要"只跑一次"的初始化,优先用ApplicationReadyEvent。
十、常见坑
- 同步异常的传染性:默认同步,某个监听器抛异常会一路传回
publishEvent的调用方。不想因为一个下游崩了影响主流程,要么给监听器自己try-catch,要么改成异步。 - 监听器里再发事件导致循环:监听器 A 发布事件 B,监听器 B 又发布事件 A——无限递归。发布下游事件时要想清楚因果链。
- 异步丢上下文:线程池里拿不到原线程的事务、
ThreadLocal、登录态。需要的话手动传递(如TaskDecorator透传 MDC)。 @TransactionalEventListener静默不执行:不在事务内发布(且没设fallbackExecution=true)时,监听器不会跑,排查半天以为没生效。- 事件对象若被包装:直接
publishEvent(普通POJO)会被PayloadApplicationEvent包一层;监听器参数写 POJO 类型即可正常匹配,但若监听器写的是PayloadApplicationEvent之外的基类,要注意类型。 @Order对异步无效:别指望异步监听器按顺序执行。
十一、什么时候该用,什么时候别用
适合用事件:
- 主流程完成后的"附赠动作"(通知、日志、统计、缓存刷新)
- 多个模块都要对同一个业务动作做出反应,但不想互相耦合
- 需要"事务提交后才做"的下游(用
@TransactionalEventListener)
不适合用事件:
- 强一致、必须成功的关键步骤(如"扣库存"是下单的核心,不该用事件"尽力而为")
- 需要返回值给调用方的结果计算(事件是单向通知,不回传结果)
- 调用链很短、只有一个下游,硬拆反而增加阅读成本
一句话:事件适合"发生了,顺便通知一下",不适合"必须做成"的核心链路。
十二、和 RabbitMQ 的对比:什么时候用哪个
不少读者会问:"既然要解耦,为什么不直接上 MQ?" 两者长得像,但解决的是不同层面的问题。
Spring 事件在进程内,RabbitMQ 在进程间。 这是最根本的区别:
| 维度 | Spring 事件 | RabbitMQ |
|---|---|---|
| 作用范围 | 同一个 JVM / 同一个应用内 | 跨进程、跨服务、甚至跨语言 |
| 通信方式 | 内存方法调用(multicastEvent 遍历监听器) |
网络消息投递,经过 broker 中转 |
| 发布者是否知道消费方 | 不知道,但也"逃不出"本应用 | 不知道,消费方可能根本不在同一台机器上 |
| 可靠性 | 默认不持久化,进程一挂事件就丢 | 消息可持久化 + ACK 机制,broker 正常就不丢 |
| 事务绑定 | @TransactionalEventListener 原生支持 |
需要自己实现事务消息 / 最终一致性(如本地消息表) |
| 投递保证 | 同步时有顺序(@Order),异步不保证 |
单队列内有序,可配置手动 ACK、重试、死信队列 |
| 削峰填谷 | 不行,监听器再慢也是应用内线程 | 可以,broker 先扛住流量,消费方按自身能力消费 |
| 运维成本 | 零,跟着应用走 | 需要部署、监控、运维 broker |
| 延迟 | 极低(内存调用) | 有网络往返,但通常毫秒级足够 |
典型场景对照:
- 用 Spring 事件:注册成功 → 记操作日志、刷新本地缓存、发站内信——全是同一个应用里的模块,"顺手通知一下"就够了,上 MQ 纯属杀鸡用牛刀。
- 用 RabbitMQ:订单服务 → 库存服务、支付服务、物流服务——参与方是独立部署的不同服务,需要消息在发布方宕机后也不丢、可以重试补偿。
- 混合使用:单体时先用 Spring 事件(
@TransactionalEventListener),拆成微服务后再把"跨服务的那条线"换成 MQ——事务监听器里改调rabbitTemplate.convertAndSend(...)即可,业务代码基本不动。
一句话决策:先问参与方在不在同一个进程里——同一个应用内用 Spring 事件;跨应用、跨服务,或需要持久化可靠投递,才上 RabbitMQ。
小结
- 四个角色:事件(
ApplicationEvent)→ 发布者(ApplicationEventPublisher)→ 广播器(ApplicationEventMulticaster)→ 监听器(@EventListener)。 - 默认同步、同线程;想异步用
@Async或替换名为applicationEventMulticaster的 Bean。 - 需要"事务提交后才执行"的下游,用
@TransactionalEventListener,默认AFTER_COMMIT,注意fallbackExecution。 - 解耦利器,但别拿它当核心链路的保证手段。


浙公网安备 33010602011771号