← 返回题目列表

Spring 的事件机制是怎样的?@EventListener 怎么用?事件是同步还是异步?

中等 第 22 / 30 题 更新于 2026/07/26
事件机制ApplicationEventEventListener观察者模式

简化版

Spring 事件机制是观察者模式的实现,用于解耦——一个 Bean 做完某件事(如用户注册成功)后发布一个事件,其他关心这件事的 Bean 监听并处理(发短信、送优惠券),发布方不用知道谁在监听。核心三角:事件(ApplicationEvent)+ 发布器(ApplicationEventPublisher)+ 监听器(@EventListener 或 ApplicationListener)。默认是同步的——发布事件后,当前线程会依次执行所有监听器,全部执行完才继续;想异步就给监听器加 @Async(并配置线程池)。

详细版

三个核心角色

// 1. 事件(Spring 4.2+ 可以是任意对象,不必继承 ApplicationEvent)
public class UserRegisteredEvent {
    private final String userId;
    public UserRegisteredEvent(String userId) { this.userId = userId; }
    public String getUserId() { return userId; }
}

// 2. 发布者:注入 ApplicationEventPublisher,调 publishEvent
@Service
public class UserService {
    @Autowired ApplicationEventPublisher publisher;
    public void register(String userId) {
        // ... 保存用户
        publisher.publishEvent(new UserRegisteredEvent(userId));  // 发布事件
    }
}

// 3. 监听器:@EventListener 注解(推荐),方法参数就是事件类型
@Component
public class NotificationListener {
    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        sendSms(event.getUserId());   // 处理事件:发短信
    }
}

同步 vs 异步

默认同步:publishEvent() 会阻塞,依次调用所有监听器,全部执行完才返回
  → 监听器抛异常会影响发布方(在同一个调用栈里)
异步:监听器方法加 @Async(需 @EnableAsync + 线程池)
  → publishEvent() 立即返回,监听器在别的线程执行,不阻塞发布方

监听器的执行顺序:多个监听器可用 @Order 控制顺序(值越小越先)。

事务绑定事件 @TransactionalEventListener:可以让监听器在事务提交后才执行(phase = AFTER_COMMIT),避免「事务还没提交就发了通知,结果事务回滚了但通知已发出」的问题。

⚠️ 默认同步机制有个坑:监听器和发布方在同一个事务、同一个线程里。如果监听器里抛异常,会导致发布方的事务回滚(本来只想「顺便发个通知」,结果通知失败把主流程也搞挂了)。所以「非核心的旁路逻辑」(发短信、记日志)建议用 @Async 异步或 @TransactionalEventListener(AFTER_COMMIT),与主流程解耦。

完整版教学

一、为什么需要事件机制:解耦

设想「用户注册成功后」要做一堆事:发欢迎短信、送新人优惠券、记录埋点、同步到数据仓库。如果全写在 register() 方法里:

void register(User user) {
    saveUser(user);
    smsService.send(...);        // 注册逻辑耦合了短信
    couponService.grant(...);    // 耦合了优惠券
    analyticsService.track(...); // 耦合了埋点
    // 每加一个新需求,都要改 register 方法 → 违反开闭原则
}

问题:register 和一堆「后续动作」强耦合,每加一个需求就要改它,且这些动作和「注册」本身没有逻辑关系。事件机制解决这个——register 只管发一个「用户注册了」的事件,谁关心谁自己去监听

void register(User user) {
    saveUser(user);
    publisher.publishEvent(new UserRegisteredEvent(user));  // 只发事件,不关心后续
}
// 加新需求 = 加一个新监听器,register 一个字都不用改

这就是事件机制的价值:发布方和处理方解耦,符合开闭原则(对扩展开放、对修改关闭)。发布方不知道、也不需要知道有哪些监听器——这正是观察者模式的精髓。

二、观察者模式的 Spring 实现

Spring 事件机制是观察者模式的标准实现,三个角色对应得很清楚:

观察者模式          Spring 事件机制
主题(Subject)   →  ApplicationEventPublisher(发布器,通常是 ApplicationContext)
观察者(Observer)→  ApplicationListener / @EventListener(监听器)
通知(Notify)    →  publishEvent()(发布事件)
事件(Event)     →  ApplicationEvent 或任意对象(携带数据)

底层由 ApplicationEventMulticaster(事件多播器) 驱动:发布事件时,多播器找到所有匹配该事件类型的监听器,依次调用它们。Spring 4.2 之前事件必须继承 ApplicationEvent,之后支持任意 POJO 作为事件(更简洁)。理解「这就是观察者模式」,就理解了它的解耦本质——主题维护观察者列表,状态变化时通知所有观察者,观察者之间互不知情。

三、发布与监听的两种写法

监听器有两种写法,推荐注解式:

// 写法1:@EventListener 注解(推荐,Spring 4.2+)
@Component
public class MyListener {
    @EventListener
    public void handle(UserRegisteredEvent event) { ... }

    @EventListener(condition = "#event.userId != null")  // 支持 SpEL 条件过滤
    public void handleWithCondition(UserRegisteredEvent event) { ... }
}

// 写法2:实现 ApplicationListener 接口(老式,一个类只能监听一种事件)
@Component
public class MyListener implements ApplicationListener<UserRegisteredEvent> {
    public void onApplicationEvent(UserRegisteredEvent event) { ... }
}

@EventListener 的优势:一个类里可以写多个监听方法(监听不同事件)、参数类型即监听的事件类型(无需泛型接口)、支持 SpEL 条件过滤condition 属性,满足条件才触发)。所以现在基本都用 @EventListener。它由 EventListenerMethodProcessor(一个 Bean 后置处理器)在容器启动时扫描注册。

四、同步机制的隐患:事务和异常传染

默认事件是同步的——这是最容易踩坑的点。同步意味着监听器和发布方在同一个线程、同一个调用栈、同一个事务里执行:

register() 方法(有 @Transactional):
  saveUser()                        // 事务内
  publishEvent(event)               // 同步!在同一线程调用监听器
    → 监听器 sendSms() 执行          // 还在同一个事务、同一个线程里
      → 如果 sendSms 抛异常
        → 异常传回 register
          → @Transactional 捕获异常 → 整个事务回滚!用户没注册成功!

问题很严重:本来只想「注册成功后顺便发个短信」,结果短信服务挂了,把用户注册这个主流程也回滚了。这是同步事件的典型坑——旁路逻辑的失败不应该影响主流程,但同步机制让它们绑在一起。解决方案有两个(下节讲):异步执行、或事务提交后执行。

五、异步与事务绑定:解耦旁路逻辑

针对同步的隐患,两个方案:

// 方案1:@Async 异步执行(需 @EnableAsync + 配置线程池)
@Component
public class NotificationListener {
    @Async
    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        sendSms(event.getUserId());   // 在独立线程执行,不阻塞、不影响发布方事务
    }
}

// 方案2:@TransactionalEventListener 事务提交后才执行
@Component
public class NotificationListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onUserRegistered(UserRegisteredEvent event) {
        sendSms(event.getUserId());   // 事务提交成功后才发短信
    }
}
  • @Async:监听器在线程池的独立线程执行,发布方 publishEvent 立即返回,监听器异常不影响主流程(但要注意异步下事务、ThreadLocal 上下文丢失)。
  • @TransactionalEventListener:绑定事务阶段——AFTER_COMMIT(提交后,最常用)保证「事务真的成功了才发通知」,避免「发了短信但事务回滚了」的数据不一致。默认情况下如果没有活动事务,事件不会被处理(可用 fallbackExecution=true 改变)。

选型:发通知/记日志等非核心旁路 → @Async(不阻塞主流程);要求「主流程成功后才做」的动作 → @TransactionalEventListener(AFTER_COMMIT);两者可组合(异步 + 事务提交后)。

六、Spring 内置事件与实战场景

Spring 容器自己也用事件机制,有一批内置事件,可以监听来做启动/关闭逻辑:

内置事件触发时机
ContextRefreshedEvent容器初始化/刷新完成(常用于「启动后执行一次」的逻辑)
ContextClosedEvent容器关闭
ApplicationReadyEvent(Spring Boot)应用完全启动就绪(可对外提供服务了)
ContextStartedEvent/ContextStoppedEvent容器启动/停止

实战场景:@EventListener(ApplicationReadyEvent.class) 常用于「应用启动完成后加载缓存、注册到注册中心、发启动通知」。业务上则常用自定义事件做领域事件解耦(订单创建、支付成功等触发一系列后续动作),配合 @Async + @TransactionalEventListener 让主流程和旁路逻辑彻底分离。这比消息队列轻量(进程内),适合「同一应用内的模块解耦」;跨服务解耦才上 MQ。

记忆钩子:「Spring 事件 = 观察者模式:发布器 publishEvent + 事件对象 + @EventListener 监听,解耦发布方和处理方;默认同步(同线程同事务,监听器异常会连累主流程回滚);@Async 异步、@TransactionalEventListener(AFTER_COMMIT) 事务提交后执行,用来隔离旁路逻辑」

七、常见误区与追问

  • 误区:Spring 事件是异步的。 默认同步——发布方线程依次执行所有监听器,全执行完才返回;要异步得给监听器加 @Async 并配线程池。
  • 误区:监听器抛异常不影响发布方。 同步模式下监听器和发布方在同一线程同一事务,监听器异常会传回发布方、可能导致其事务回滚。
  • 误区:@EventListener 一个类只能监听一种事件。 一个类可以写多个 @EventListener 方法监听不同事件;老式的 ApplicationListener 接口才是一个类监听一种。
  • 误区:事件发布后监听器一定能拿到已提交的数据。 同步默认在事务提交前执行,此时数据还没提交;要「提交后处理」用 @TransactionalEventListener(AFTER_COMMIT)。
  • 追问:Spring 事件机制底层是什么设计模式? 观察者模式——发布器(主题)维护监听器(观察者)列表,事件发生时通过 ApplicationEventMulticaster 通知所有匹配的监听器。
  • 追问:@TransactionalEventListener 解决什么问题? 让监听器在事务的特定阶段(默认 AFTER_COMMIT)才执行,避免「事务还没提交就发通知、结果事务回滚了通知却已发出」的数据不一致。
  • 追问:异步事件监听器要注意什么? 需 @EnableAsync + 配置线程池;异步线程拿不到原线程的事务和 ThreadLocal 上下文,且异常不会传回发布方(要自己在监听器里处理异常)。

八、加强记忆

Spring 事件机制是观察者模式的实现,核心价值是解耦——发布方做完某事只管 publishEvent 发一个事件,谁关心谁自己 @EventListener 监听,发布方无需知道有哪些监听器(符合开闭原则)。三角色:发布器 ApplicationEventPublisher + 事件对象(4.2+ 可为任意 POJO)+ 监听器(@EventListener 推荐,一个类可多方法、支持 SpEL 条件),底层由 ApplicationEventMulticaster 驱动。最大的坑是默认同步——监听器和发布方在同一线程、同一事务、同一调用栈,监听器抛异常会连累发布方事务回滚(旁路逻辑失败拖垮主流程)。两个解耦方案:@Async 让监听器在独立线程执行、不阻塞不影响主流程;@TransactionalEventListener(AFTER_COMMIT) 让监听器在事务提交成功后才执行、避免「发了通知但事务回滚」的不一致。Spring 内置事件(ContextRefreshedEventApplicationReadyEvent)可做启动逻辑。它是进程内轻量解耦,跨服务才用 MQ。一句话「观察者模式解耦发布与处理、默认同步同事务(监听器异常连累主流程)、@Async 异步、@TransactionalEventListener 提交后执行」。