Fork me on GitHub

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,兼容性更好(需要 import org.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

十、常见坑

  1. 同步异常的传染性:默认同步,某个监听器抛异常会一路传回 publishEvent 的调用方。不想因为一个下游崩了影响主流程,要么给监听器自己 try-catch,要么改成异步。
  2. 监听器里再发事件导致循环:监听器 A 发布事件 B,监听器 B 又发布事件 A——无限递归。发布下游事件时要想清楚因果链。
  3. 异步丢上下文:线程池里拿不到原线程的事务、ThreadLocal、登录态。需要的话手动传递(如 TaskDecorator 透传 MDC)。
  4. @TransactionalEventListener 静默不执行:不在事务内发布(且没设 fallbackExecution=true)时,监听器不会跑,排查半天以为没生效。
  5. 事件对象若被包装:直接 publishEvent(普通POJO) 会被 PayloadApplicationEvent 包一层;监听器参数写 POJO 类型即可正常匹配,但若监听器写的是 PayloadApplicationEvent 之外的基类,要注意类型。
  6. @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
  • 解耦利器,但别拿它当核心链路的保证手段。
posted @ 2026-08-31 21:26  秋夜雨巷  阅读(10)  评论(0)    收藏  举报