Spring 事件机制与条件装配详解
定位:讲透应用内事件体系(发布/监听/异步/事务事件)、容器内置事件、条件装配注解族,以及条件装配作为 Boot 自动配置基石的原理
适用版本:Spring Framework 6.x(JDK 17+)
目录
一、事件体系
1.1 三要素与基本用法
Spring 内置观察者模式实现,用于应用内解耦:
java
// ① 定义事件(6.x 起普通对象即可,传统上继承 ApplicationEvent)
public record OrderCreatedEvent(Long orderId, BigDecimal amount) {}
// ② 发布(注入发布者)
@Service
public class OrderService {
@Autowired private ApplicationEventPublisher publisher;
public void createOrder(...) {
// ...业务
publisher.publishEvent(new OrderCreatedEvent(id, amount));
}
}
// ③ 监听
@Component
public class OrderListeners {
@EventListener
public void onCreated(OrderCreatedEvent e) { /* 发短信/记审计 */ }
}
广播由 ApplicationEventMulticaster 完成(容器刷新第⑧步初始化):按事件类型匹配监听器并逐个调用。
1.2 同步与异步
| 模式 | 行为 | 适用 |
|---|---|---|
| 同步(默认) | 在发布者线程内执行所有监听器 | 轻量逻辑、需与发布者同事务 |
| 异步 | @Async @EventListener,提交线程池 |
耗时旁路逻辑(发通知、推送) |
java
@Async
@EventListener
public void onCreated(OrderCreatedEvent e) { /* 线程池执行 */ }
异步注意:监听器异常不再影响发布者;上下文(事务、ThreadLocal)不自动传播。
1.3 事务事件(高频考点)
java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onCreated(OrderCreatedEvent e) { /* 事务提交后才执行 */ }
| 阶段 | 触发时机 | 用途 |
|---|---|---|
| AFTER_COMMIT(默认) | 事务提交后 | 发通知/发消息,避免"事务回滚了通知已发出" |
| BEFORE_COMMIT | 提交前 | 与主事务一起完成的收尾 |
| AFTER_ROLLBACK | 回滚后 | 补偿记录 |
| AFTER_COMPLETION | 完成后(无论结果) | 清理 |
价值 :把"副作用类动作"延迟到数据真正落库之后------这是事件 + 事务组合的黄金用法。注意无事务上下文时默认不触发(可配 fallbackExecution = true)。
1.4 条件监听与顺序
@EventListener 可用 SpEL 条件:@EventListener(condition = "#e.amount > 10000")
多监听器顺序:@Order
二、内置事件
容器自身在生命周期关键点发布事件,应用可监听挂钩:
| 事件 | 时机 | 典型用途 |
|---|---|---|
ContextRefreshedEvent |
全部单例就绪 | 初始化自检、预热缓存 |
ContextStartedEvent |
start() 后 | 较少用 |
ContextStoppedEvent |
stop() 后 | 较少用 |
ContextClosedEvent |
关闭前 | 优雅停机:停定时任务、排空队列 |
@EventListener 同样可监听这些内置事件:
java
@EventListener(ContextClosedEvent.class)
public void onShutdown() { /* 资源清理 */ }
三、事件机制的边界
| 适用 | 不适用 |
|---|---|
| 应用内模块解耦(下单 → 审计/通知/积分) | 跨进程通信(用消息队列) |
| 生命周期挂钩 | 要求可靠投递/重试的业务(内存事件进程死即丢) |
| 替代"服务直接调用另一个服务的副作用方法" | 需要严格顺序/幂等保证的复杂编排 |
纪律:应用内事件负责解耦,跨服务可靠性交给消息中间件;异步事件只做"丢了可接受"的事,或配套补偿。
四、条件装配
4.1 @Conditional 模型
Bean 是否注册,由条件(Condition#matches)决定:
java
@Configuration
@Conditional(MyCondition.class) // matches 返回 true 才注册
public class SomeConfig { }
4.2 常用派生注解
| 注解 | 条件 | 典型场景 |
|---|---|---|
| @Profile | 激活环境匹配 | 多环境数据源(04 篇) |
| @ConditionalOnProperty | 配置属性满足 | 功能开关:@ConditionalOnProperty("feature.x.enabled") |
| @ConditionalOnClass / OnMissingClass | classpath 存在/缺少某类 | 按依赖决定装配(有 Redis 依赖才装 Redis 配置) |
| @ConditionalOnBean / OnMissingBean | 容器有/无某 Bean | "用户没自定义就给默认"(自动配置核心) |
| @ConditionalOnExpression | SpEL 表达式 | 复杂组合条件 |
4.3 条件评估的时机与陷阱
- 条件在 Bean 定义注册阶段评估(refresh 第⑤步),不是实例化阶段;
@ConditionalOnBean依赖"目标 Bean 定义已被处理"------评估顺序导致过经典坑:用户配置类先于自动配置类处理,因此自动配置里的 OnMissingBean 能看到用户的定义;自定义配置类之间乱序使用 OnBean 则可能误判;- 理解这一点才能解释"为什么我的 Bean 没被识别为已存在"。
五、自动装配的底层依据
条件装配是 Spring Boot 自动配置的基石(机制全解在 BT-02),此处建立连接:
自动配置类 ≈ @Configuration + @ConditionalOnXxx 组合
套路:
@ConditionalOnClass(DataSource.class) ← 有这个依赖才考虑
@ConditionalOnMissingBean(DataSource.class) ← 用户没定义才注册默认
public DataSource dataSource() { ... }
效果:"约定大于配置"------用户什么都不配,默认生效;用户定义了,自动配置让位。OnMissingBean 是"不冲突"的保证,OnClass 是"不乱装"的保证。
六、总结
- 事件三要素:事件对象(6.x 普通对象即可)、ApplicationEventPublisher 发布、@EventListener 监听;Multicaster 按类型广播。
- 同步默认、异步可选:@Async 让监听进线程池,代价是异常与上下文不再传播。
- @TransactionalEventListener:把副作用延迟到事务提交后(默认 AFTER_COMMIT),避免"回滚了通知已发出";无事务时默认不触发。
- 内置事件:ContextRefreshed/Closed 用于初始化自检与优雅停机挂钩。
- 边界:应用内解耦用它,跨进程可靠性交给消息队列;异步事件只做可丢失的事。
- 条件装配:@Conditional 家族在注册阶段决定 Bean 存废;OnProperty 开关、OnClass 按依赖、OnMissingBean 让位用户定义------这是 Boot 自动配置的底层逻辑。
七、常见高频面试题
1. Spring 事件机制怎么使用?和消息队列有什么区别?
要点:定义事件对象(6.x 普通类即可),注入 ApplicationEventPublisher 发布,@EventListener 按类型监听,由 ApplicationEventMulticaster 广播。与消息队列的区别:Spring 事件是进程内内存机制,无持久化、无重试、进程死即丢,用于应用内模块解耦;MQ 跨进程、可靠投递、削峰填谷。选型纪律:应用内解耦用事件,跨服务可靠性用 MQ。
2. @TransactionalEventListener 解决什么问题?
要点:解决"事务未提交副作用先发生"的问题。默认在事务提交后(AFTER_COMMIT)触发监听,保证只有数据真正落库后才发通知/消息;还可选 BEFORE_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION。典型场景:下单事务提交后才发优惠券/通知。注意:无事务上下文默认不触发(可配 fallbackExecution);AFTER_COMMIT 阶段已脱离事务,其中的写操作是新事务。
3. @EventListener 如何异步执行?有什么代价?
要点:配合 @Async(需开启异步支持)让监听器在任务线程池执行,不阻塞发布者。代价:① 监听器异常不再传播给发布者;② 发布者的事务、ThreadLocal、安全上下文不自动传播到监听线程;③ 执行顺序不可控。所以异步事件只用于"失败可接受"的旁路逻辑,关键逻辑要么同步要么交给可靠消息。
4. Spring 有哪些内置容器事件?
要点:ContextRefreshedEvent(容器刷新完成、全部单例就绪,常用于初始化自检与预热)、ContextStartedEvent/ContextStoppedEvent(start/stop,少用)、ContextClosedEvent(关闭前,用于优雅停机:停任务、排空队列、释放资源)。用 @EventListener 监听即可挂钩容器生命周期。
5. @Conditional 及其派生注解有哪些?各自判断什么?
要点:@Conditional 接受自定义 Condition 实现(编程式判断);派生注解覆盖常用场景:@Profile 环境匹配;@ConditionalOnProperty 配置属性开关;@ConditionalOnClass/OnMissingClass 按 classpath 是否存在某类;@ConditionalOnBean/OnMissingBean 按容器是否已有某 Bean;@ConditionalOnExpression SpEL 表达式。条件在 Bean 定义注册阶段评估。
6. @ConditionalOnMissingBean 在自动配置中起什么作用?
要点:实现"用户优先、默认兜底":自动配置类中注册默认 Bean 前检查容器是否已有该类型/名称的 Bean,没有才注册。这保证了用户自定义定义永远覆盖自动配置的默认值而不冲突------是 Spring Boot"约定大于配置"的关键机制。搭配 @ConditionalOnClass(有依赖才装配)构成自动配置的基本套路。
7. @ConditionalOnBean 为什么有时会误判?
要点:条件在 Bean 定义注册阶段按"当时已处理的定义"评估,依赖处理顺序。Boot 中自动配置类刻意排在用户配置类之后处理,所以自动配置里的 OnBean/OnMissingBean 能可靠看到用户定义;但自定义配置类之间没有这种顺序保证,乱序时可能看不到对方导致误判。解法:避免在自定义配置间用 OnBean 互相依赖,或显式用 @AutoConfigureAfter/@DependsOn 控制。
8. 事件发布是同步的还是异步的?多个监听器的顺序?
要点:默认同步------在发布者线程内按注册顺序逐个执行监听器,任一监听器抛异常会影响后续与发布者;多监听器顺序用 @Order(或 Ordered)控制。需要异步加 @Async 提交线程池。同步语义意味着事件不适合放重逻辑(拖慢主流程),重活应异步化或移出。
9. 如何用事件机制实现"下单后发短信、加积分、记审计"的解耦?
要点:订单服务只发布 OrderCreatedEvent,不直接调用短信/积分/审计服务;三个模块各自 @EventListener 监听,互不感知彼此存在。进一步:耗时动作加 @Async;对一致性敏感的动作用 @TransactionalEventListener 等事务提交后执行;新增一个副作用模块只需新增监听器,订单代码零改动。跨服务或要求可靠投递时改走消息队列。
10. 为什么说条件装配是 Spring Boot 自动配置的基石?
要点:自动配置的本质是一批带条件的配置类:@ConditionalOnClass 保证"classpath 有相应依赖才装配"(不乱装);@ConditionalOnProperty 支持开关控制;@ConditionalOnMissingBean 保证"用户定义了就让位"(不冲突)。三者组合实现了"开箱即用又处处可覆盖"。理解条件评估时机(注册阶段)与顺序,是排查自动配置不生效问题的基础。
