CHAPTER 05 / SPRING CORE PRINCIPLES

第 05 篇 · Spring 核心原理

本篇抓住 IoC + AOP 两条主轴:容器与 Bean 生命周期、三级缓存、代理/事务边界、扩展点与 SpEL/绑定,落到事务后异步时序。

骨架见 M12.05。本篇只补领域细枝,框在 核心方法论 · 开篇。

题目数:11 | 高级 ×9 | 架构 ×1 | 专家 ×1 | 预计复习:154 分钟
核心知识链

Spring 主线是「容器生命周期与 BeanDefinition → 依赖/循环依赖与代理时机 → 横切(AOP/事务/事件)→ 扩展点与表达式/绑定 → 事务提交后副作用」,用边界解释「注解为何失效」。

  1. IoC 启动与 Bean 生命周期:refresh 标准化步骤 + BeanDefinition→实例→初始化→销毁;图纸元数据是排障切面。
  2. 三级缓存与循环依赖:early 暴露与 ObjectFactory 保证「代理只创建一次」;构造器/prototype/Boot 2.6+ 默认收紧解释失败边界。
  3. AOP 代理与事务失效:JDK/CGLIB 选择、自调用、访问级别、异常类型决定 @Transactional 是否生效;传播与挂起/savepoint 要精确。
  4. 事件与设计模式:发布/监听/广播三角色,事务绑定事件;框架机制映射到工厂、代理、模板、观察者等模式。
  5. 扩展点与容器层次:BFPP 改图纸、BPP 改实例;BeanFactory 与 ApplicationContext 能力差决定企业级默认形态。
  6. 绑定、SpEL 与事务后异步:Converter/Formatter/DataBinder 与 SpEL 求值链;提交后再发 MQ/通知用同步钩子四方案,禁止事务内瞎开线程。
工程化要点
  • 关键 Bean 与配置变更走集成测试;事务与幂等在服务边界显式设计,回归覆盖自调用与异常回滚。
  • 循环依赖不得靠「碰巧能启动」:能拆则拆;必须保留时书面化依赖方向与代理需求,并对 Boot 默认禁止策略有例外审批。
  • AOP/事务失效清单进代码评审:自调用、非 public、错误异常匹配、同类内调用;静态检查或 ArchUnit 能拦则拦。
  • 事件消费与事务边界绑定:需「提交后投递」用 @TransactionalEventListener 或同步钩子,禁止事务未提交就发外部副作用。
  • 自定义 BPP/BFPP 变更视为平台级改动:顺序、兼容性与启动耗时进回归;禁止业务仓随意插入全局后置处理器。
  • 类型转换与配置绑定失败要有可读错误与校验;@ConfigurationProperties 约束进启动失败快策略。
  • SpEL 用于动态关键路径时做注入与复杂度门禁;缓存 key/安全表达式写进评审关注点。
  • 事务提交后异步四方案(同步钩子/事件/事务消息/本地消息表)选型表化,默认禁止事务线程里直接 new Thread。
本篇思维模式
  • 代理边界决定事务:先画调用是否经过代理,再查传播与回滚规则;边界错了注解全失效。
  • 图纸先于实例:多数启动问题出在 BeanDefinition/条件装配阶段,而不是业务方法本身。
  • 三级缓存服务代理:理解「为何三级」比背三个 Map 名字更重要——为了提前暴露与单次代理。
  • 扩展点按时序:BFPP 与 BPP 介入时机不同,放错阶段会静默改错对象。
  • 事件是解耦不是省略:异步与事务绑定要显式设计,否则就是丢失的副作用。
  • 绑定是基础设施:MVC 参数、配置属性、SpEL 都走转换体系;类型问题先查绑定再查业务。
  • 提交后再副作用:外部通知/MQ 必须以事务结果为门闩,时序反了必出脏通知。
C05.01
Spring IoC 容器的启动流程与 Bean 生命周期?高级P0
本质问题

这道题考察对 Spring 容器两段式生命周期的精确理解:一是容器自身的启动流程(refresh() 的 13 个标准化步骤),二是单个 Bean 从 BeanDefinition 到销毁的完整链路。更深一层,它考察"BeanDefinition 是 IoC 的元数据核心"这一认知——IoC 容器操作的不是类,而是 BeanDefinition(Bean 的图纸),实例化只是图纸落地的最后一步。理解生命周期,才能回答所有"为什么我的 @PostConstruct 没生效""AOP 在哪一步生效""循环依赖在哪一步打破"等问题。

演进脉络

Spring 2.x(XML 时代):BeanDefinition 由 XmlBeanDefinitionReader 解析 <bean> 标签生成,全量 XML 配置,ClassPathXmlApplicationContext 是主流入口。容器启动即解析 XML 注册 BeanDefinition。

Spring 3.x(注解时代):引入 @Component + ClassPathBeanDefinitionScanner 扫描包路径生成 BeanDefinition;AnnotationConfigApplicationContext 取代 XML 入口。BeanDefinition 的来源从"手写 XML"演变为"扫描注解"。

Spring 4.x(Boot 时代):@SpringBootApplication + 自动配置,BeanDefinition 来源扩展为三类:扫描的、自动配置类 @Bean 方法注册的、SPI 注册的。容器仍是 AnnotationConfigApplicationContext 的 Web 变体。

Spring 5.x(响应式时代):新增 GenericApplicationContext 的 registerBean() 函数式注册 API,BeanDefinition 可用 Lambda 声明式构造,适配响应式与 Kotlin。生命周期本身未变。

Spring 6.x(AOT/Native 时代):AOT 阶段在构建期提前执行 BeanFactoryInitializationAotContribution,将 BeanDefinition 解析、条件评估冻结为静态代码,运行期跳过反射扫描。生命周期"前移"——部分初始化逻辑在编译期完成,这是为 GraalVM Native Image 服务的范式转移。

第一性原理

容器启动:AbstractApplicationContext.refresh() 13 步。这是 IoC 启动的核心模板方法,必须能说出关键步骤的顺序与职责:

flowchart TD A["1、prepareRefresh() 上下文准备,校验必要属性"] --> B["2、obtainFreshBeanFactory() 刷新并获取 BeanFactory"] B --> C["3、prepareBeanFactory() 配置 BeanFactory(ClassLoader/后置处理器)"] C --> D["4、postProcessBeanFactory() 子类扩展点"] D --> E["5、invokeBeanFactoryPostProcessors() 调用 BFPP,修改 BeanDefinition"] E --> F["6、registerBeanPostProcessors() 注册 BPP 但不执行"] F --> G["7、initMessageSource() 国际化"] G --> H["8、initApplicationEventMulticaster() 事件广播器"] H --> I["9、onRefresh() 子类初始化特殊Bean(Tomcat等)"] I --> J["10、registerListeners() 注册监听器"] J --> K["11、finishBeanFactoryInitialization() 实例化所有非懒加载单例"] K --> L["12、finishRefresh() 发布 ContextRefreshedEvent"] L --> M["13、resetCommonCaches() 清理反射缓存"] style E fill:#dbeafe,stroke:#2563eb style K fill:#fef3c7,stroke:#d97706 style L fill:#dcfce7,stroke:#059669
图 5.1-1 refresh() 13 步启动流程:第 5 步 BFPP 改图纸,第 11 步实例化 Bean,第 12 步发布事件

其中 第 5 步(BFPP)和 第 11 步(实例化)是两个最核心阶段:前者决定"有哪些 Bean、怎么造",后者决定"造出来并装配好"。ConfigurationClassPostProcessor 就在第 5 步解析 @Configuration/@ComponentScan,把扫描到的类注册成 BeanDefinition。

单个 Bean 生命周期(核心 5 大阶段):

flowchart LR A["BeanDefinition 注册"] --> B["实例化 createBeanInstance"] B --> C["属性填充 populateBean"] C --> D["初始化 initializeBean"] D --> E["使用"] E --> F["销毁 destroy"] subgraph B [实例化阶段] B1["InstantiationAwareBPP.postProcessBeforeInstantiation"] --> B2["doCreateBean: 反射构造"] B2 --> B3["applyMergedBeanDefinitionPostProcessor"] B3 --> B4["addSingletonFactory 三级缓存暴露早期引用"] end subgraph C [属性填充] C1["InstantiationAwareBPP.postProcessAfterInstantiation"] C1 --> C2["autowire: @Autowired/@Value 注入"] C2 --> C3["InstantiationAwareBPP.postProcessProperties"] end subgraph D [初始化] D1["Aware 回调 BeanName/BeanFactory/ApplicationContext"] D1 --> D2["BPP.postProcessBeforeInitialization @PostConstruct"] D2 --> D3["InitializingBean.afterPropertiesSet"] D3 --> D4["init-method"] D4 --> D5["BPP.postProcessAfterInitialization AOP代理"] end style B4 fill:#fef3c7,stroke:#d97706 style D5 fill:#dbeafe,stroke:#2563eb style D1 fill:#dcfce7,stroke:#059669
图 5.1-2 Bean 生命周期全流程:实例化→属性填充→初始化(含 Aware 回调、BPP 前后置、AOP 代理生成)→销毁

关键源码链路:getBean → doGetBean → getSingleton(三级缓存查询) → createBean → doCreateBean。其中 doCreateBean 的三步是高频考点:

// AbstractAutowireCapableBeanFactory.doCreateBean 核心三段
protected Object doCreateBean(...) throws BeansException {
    // 1. 实例化:通过反射调用构造方法得到"裸对象"
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // 2. 判断是否允许提前暴露(单例 + 允许循环依赖 + 当前正在创建)
    boolean earlySingletonExposure = (mbd.isSingleton()
            && this.allowCircularReferences()
            && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        // 把 ObjectFactory 放入第三级缓存 singletonFactories
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // 3. populateBean 注入属性 + initializeBean 初始化(AOP 代理在此生成)
    Object exposedObject = bean;
    populateBean(beanName, mbd, instanceWrapper);              // @Autowired/@Value 在此注入
    exposedObject = initializeBean(beanName, exposedObject, mbd); // AOP 代理在此返回
    return exposedObject;
}

初始化回调的执行顺序(initializeBean 内部)必须记牢:① Aware 接口回调(BeanNameAware→BeanClassLoaderAware→BeanFactoryAware)→ ② BeanPostProcessor.postProcessBeforeInitialization(@PostConstruct 由 CommonAnnotationBeanPostProcessor 在此触发)→ ③ InitializingBean.afterPropertiesSet → ④ 自定义 init-method → ⑤ BeanPostProcessor.postProcessAfterInitialization(AOP 代理由 AbstractAutoProxyCreator 在此生成)。

工程实践

三种初始化回调的选型:实践中优先用 @PostConstruct(JSR-250 标准,解耦于 Spring),其次 InitializingBean(侵入框架接口,不推荐),最后 init-method(XML 时代遗留,注解场景用 @Bean(initMethod=...))。三者执行顺序固定,不要依赖顺序写逻辑。

// 推荐:@PostConstruct,标准注解,与 Spring 解耦
@Component
public class OrderService {
    @Autowired private OrderMapper mapper;     // populateBean 阶段注入

    @PostConstruct
    public void init() {                    // BPP 前置阶段触发,mapper 已就绪
        this.cache = mapper.loadAll();
    }

    @PreDestroy
    public void cleanup() {                  // 销毁阶段,释放资源
        this.cache.clear();
    }
}
架构经验
  • 重活放到 SmartInitializingSingleton:所有单例 Bean 创建完成后(finishBeanFactoryInitialization 末尾)触发,此时所有 Bean 就绪,适合做全局校验与预热,比 @PostConstruct 更安全(后者可能引用的 Bean 还未创建)。
  • 构造器中不要依赖注入的属性:构造器在 createBeanInstance 阶段执行,此时 @Autowired 属性尚未注入(在 populateBean),强行使用必 NPE。
  • Aware 接口的工程价值:实现 ApplicationContextAware 可在普通 Bean 中拿到容器引用(用于动态获取 Bean);但更推荐注入 ApplicationContext 本身,避免 Aware 接口侵入。
深度追问

追问 1:finishBeanFactoryInitialization 之前已经创建了哪些 Bean?

第 11 步之前,已创建的 Bean 包括:所有 BeanFactoryPostProcessor(第 5 步调用前必须实例化)、所有 BeanPostProcessor(第 6 步注册前必须实例化)、以及它们直接依赖的 Bean。这就是为什么 BPP 必须提前实例化——否则普通 Bean 在第 11 步创建时就拿不到 BPP。这也意味着 BPP 之间不能依赖普通 Bean,会导致普通 Bean 被迫提前实例化而错过某些 BPP。

追问 2:BeanDefinition 是什么?为什么 IoC 操作的是它而非 Class?

BeanDefinition 是 Bean 的"图纸",包含类名、作用域、构造参数、属性值、初始化/销毁方法、是否懒加载、依赖、是否 primary 等元数据。IoC 操作 BeanDefinition 而非 Class 的原因:① 同一个 Class 可以注册多个不同配置的 BeanDefinition(如 @Bean 多实例);② BeanDefinition 可被 BeanFactoryPostProcessor 动态修改(如占位符替换、注册新 Bean),这是配置可编程性的基础;③ 解耦"声明"与"实现",AOT 阶段可在无运行期反射时冻结 BeanDefinition。

追问 3:为什么 BeanPostProcessor.postProcessAfterInitialization 之后返回的对象可能不是原 Bean?

因为 AOP 代理就在这一步生成。AbstractAutoProxyCreator.postProcessAfterInitialization 会判断该 Bean 是否需要被代理(是否有切面匹配),若是则用 ProxyFactory 创建代理对象并返回。容器后续持有的就是这个代理对象,注入到其他 Bean 的也是代理。这就是为什么"自调用不走代理"——代理对象内部调用的是 this(目标对象本身),而非代理对象。

常见陷阱
易错点
  • 混淆 BeanFactoryPostProcessor 与 BeanPostProcessor 时机:BFPP 在第 5 步操作 BeanDefinition(造图纸),BPP 在第 11 步操作 Bean 实例(造产品)。记成"工厂后置=改图纸,Bean 后置=改产品"。
  • 构造器中使用 @Autowired 字段:构造器在属性填充之前,此时注入字段为 null。需用构造器注入(参数注入)而非字段注入。
  • 误以为 @PostConstruct 在构造器前:顺序是 构造器→属性注入→@PostConstruct。@PostConstruct 中可安全使用注入的属性。
  • prototype 作用域不走完整生命周期:prototype Bean 不缓存、不管理销毁,@PreDestroy 不会触发,需手动清理。
  • 懒加载 Bean 的初始化时机:懒加载 Bean 在 finishBeanFactoryInitialization 不会实例化,首次 getBean 才创建。
生产实践

上线门禁:上下文刷新耗时、循环依赖与启动失败演练。表达提纲:生命周期阶段→配置/Bean 证据→修复与回归。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C05.02
三级缓存如何解决循环依赖?为何需要三级?专家P1
本质问题

这道题是 Spring 源码考察的"皇冠题",直接检验候选人是否真正读过 AbstractAutowireCapableBeanFactory 的 getSingleton/createBean 链路。它考察三层认知:① 三级缓存(singletonObjects/earlySingletonObjects/singletonFactories)各自存什么、如何协作打破环;② 为什么是三级而不是二级——核心是 AOP 代理的"提前暴露"与"只创建一次代理"的保证;③ 哪些循环依赖无法解决(构造器、prototype、Spring Boot 2.6+ 的默认收紧)。能讲清第二点,才算触及源码本质。

演进脉络

早期 Spring:用一级成品缓存 singletonObjects + 二级半成品缓存 earlySingletonObjects 解决 setter 循环依赖;A 实例化后立即放入二级缓存,再填充属性时发现需要 B;B 实例化后填充属性需要 A,从二级缓存拿到 A 的半成品,B 完成;A 继续完成。

引入三级缓存(AOP 时代):当 A 需要被 AOP 代理时,二级缓存存的是 A 的原始对象,而 B 注入的应是 A 的代理对象——若 B 注入了原始对象,代理失效。于是引入第三级 singletonFactories(ObjectFactory),为保证代理对象正确暴露,在需要时才调用 getEarlyBeanReference 提前生成代理,并升级到二级缓存。这是为 AOP 服务的精细设计。

Spring Boot 2.6+ 收紧(2021):spring.main.allow-circular-references 默认 false,Boot 启动时若有循环依赖直接报错。注意这是 Spring Boot 层面的默认收紧(Boot 2.6 引入),不是 Spring Framework 6 的框架层变更。这是"用框架强制纠错"的演进——循环依赖本质是设计缺陷,官方态度从"尽力解决"转为"默认禁止,需显式开启"。同时 spring.main.allow-bean-definition-overriding 也默认 false。架构师应理解:未来循环依赖是 code smell,而非可依赖的特性。

第一性原理

三级缓存的定义(DefaultSingletonBeanRegistry):

缓存 字段 存放内容 访问时机
一级 singletonObjects 完全初始化好的成品 Bean(单例池) getBean 优先查此,命中即返回
二级 earlySingletonObjects 提前暴露的半成品(可能是早期代理) 循环依赖时从三级升级而来
三级 singletonFactories ObjectFactory(lambda: getEarlyBeanReference) 实例化后立即放入,被引用时执行

以 A 依赖 B、B 依赖 A(均需 AOP 代理)为例的完整时序:

sequenceDiagram participant C as Container participant A as Bean A participant B as Bean B participant L1 as 一级 singletonObjects participant L2 as 二级 earlySingletonObjects participant L3 as 三级 singletonFactories C->>A: 1. getBean(A) 开始创建 C->>A: 2. createBeanInstance 实例化裸对象A C->>L3: 3. addSingletonFactory(A, ObjectFactory) Note over L3: 三级缓存放入 A 的 ObjectFactory
内含 getEarlyBeanReference(会生成代理) C->>A: 4. populateBean 填充属性,发现需要 B C->>B: 5. getBean(B) 开始创建 B C->>B: 6. createBeanInstance 实例化裸对象B C->>L3: 7. addSingletonFactory(B, ObjectFactory) C->>B: 8. populateBean 填充属性,发现需要 A C->>L3: 9. getSingleton(A) 查一级(无)→查二级(无)→查三级(命中!) L3->>L3: 10. 执行 ObjectFactory.getObject = getEarlyBeanReference(A) Note over L3: 10. 此时提前为 A 生成 AOP 代理对象 A_proxy L3-->>L2: 11. 将 A_proxy 升级放入二级缓存,并从三级移除 L2-->>B: 12. 返回 A_proxy,B 完成属性填充 C->>B: 13. initializeBean(B) 生成 B 的代理 B_proxy C->>L1: 14. B_proxy 放入一级缓存,清空 B 的二三级 L1-->>A: 15. B 完成,回到 A 的 populateBean,B_proxy 注入 A C->>A: 16. initializeBean(A) Note over C: 16. 关键!检测到 A 已提前暴露代理
直接复用第10步生成的 A_proxy,不再重新代理 C->>L1: 17. A(实际是 A_proxy) 放入一级缓存
图 5.2-1 三级缓存打破循环依赖时序:三级缓存存 ObjectFactory,被引用时提前生成代理并升级到二级,确保全程只有一个代理

核心源码:getSingleton 的三级查询

// DefaultSingletonBeanRegistry.getSingleton 三级查询逻辑
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 一级缓存:查成品
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 2. 二级缓存:查半成品(已提前暴露的早期引用)
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 双重检查
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 3. 三级缓存:查 ObjectFactory 并执行
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();  // 触发 getEarlyBeanReference,生成代理
                            this.earlySingletonObjects.put(beanName, singletonObject); // 升级到二级
                            this.singletonFactories.remove(beanName);        // 移除三级
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

为何需要三级而非二级(核心答案):

若只有二级缓存(singletonObjects + earlySingletonObjects),且 earlySingletonObjects 直接存"半成品原始对象",则当 A 需要 AOP 代理时:B 注入的是 A 的原始对象,而最终容器持有的是 A 的代理对象——B 调用的 A 方法不走代理,AOP(含事务)失效。

引入三级缓存后,三级存的是 ObjectFactory(一个工厂 lambda),只有被其他 Bean 引用时才执行 getEarlyBeanReference 提前生成代理。这带来两个关键保证:

  • 代理只生成一次:第一次被引用时执行 ObjectFactory 生成代理并升级到二级,后续引用直接从二级取。后续 initializeBean 阶段检测到已暴露早期引用(earlySingletonReference != null),直接复用该代理,不重复代理。
  • 没有循环依赖时不提前代理:若无循环依赖,A 的代理在正常 postProcessAfterInitialization 阶段生成,三级缓存的 ObjectFactory 从未执行——避免"为所有 Bean 提前生成代理"的性能开销,这是用工厂延迟到"真正需要时"的精妙之处。

一句话总结:三级缓存的本质是"用 ObjectFactory 延迟代理生成时机",既保证循环依赖时 B 注入的是代理对象,又保证无循环依赖时不付出提前代理的代价,并保证代理对象全局唯一。

工程实践

构造器注入循环依赖无法解决:构造器在 createBeanInstance 阶段执行,此时 Bean 还未实例化完成,无法放入三级缓存(三级缓存在实例化后才放入)。A 构造时需要 B,B 构造时需要 A,两者都没进入缓存,Spring 直接抛 BeanCurrentlyInCreationException。

解决方案:@Lazy 注入代理。@Lazy 会在注入点生成一个代理对象(而非真实 Bean),首次调用方法时才从容器获取真实 Bean,从而打破构造期的强依赖。

// 构造器循环依赖:直接报错
@Component
public class A {
    public A(B b) {}   // B 构造时又需要 A → BeanCurrentlyInCreationException
}

// 解决:@Lazy 注入代理,打破构造期强依赖
@Component
public class A {
    public A(@Lazy B b) {  // 注入的是 B 的代理,首次调用 b.xxx() 才真实 getBean(B)
        this.b = b;
    }
}
架构建议

循环依赖是设计缺陷的信号。Spring Boot 2.6+ 默认禁止循环依赖是正确方向。架构治理应:① 构建期用 Actuator 的 /beans 端点检测循环依赖;② 通过重构抽取公共依赖(引入第三个 Bean 承载共享逻辑)消除环;③ 实在无法消除的 legacy 代码,用 @Lazy 作为过渡方案并加注释与 TODO,而非依赖框架特性。推崇"构造器注入"——它让循环依赖在编译期暴露,强制解决,而非隐藏到运行期。

深度追问

追问 1:三级缓存里 ObjectFactory 的 getEarlyBeanReference 具体做了什么?

它遍历所有 SmartInstantiationAwareBeanPostProcessor(核心是 AbstractAutoProxyCreator),调用 getEarlyBeanReference 判断是否需要代理,需要则用 ProxyFactory 创建代理返回。这就是"提前 AOP"的实现。如果 Bean 不需要 AOP(无切面匹配),getEarlyBeanReference 原样返回原始对象,三级缓存退化为"存原始对象的工厂"。

追问 2:如果用二级缓存 + earlySingletonObjects 直接存代理,能否替代三级?

理论上有一种方案:实例化后立即判断是否需要代理,需要则立即生成代理存入二级。但这有两个问题:① 性能损耗——所有 Bean 都要在实例化后立即做 AOP 判断并可能生成代理,即使绝大多数 Bean 不会发生循环依赖,这是无谓的开销;② 语义侵入——正常情况下代理应在 postProcessAfterInitialization 生成(生命周期末尾),提前到实例化后会让"代理生成时机"分裂为两处,增加心智负担。三级缓存用 ObjectFactory 把"是否生成代理"的决策延迟到真正被引用时,巧妙兼顾了正确性与性能。

追问 3:@Async 标注的 Bean 发生循环依赖为什么失效?

@Async 由 AsyncAnnotationBeanPostProcessor 处理,它同样在 postProcessAfterInitialization 生成代理。但与 AOP 不同,@Async 的代理在 initializeBean 阶段才生成,不参与三级缓存的 getEarlyBeanReference。因此当 B 通过三级缓存拿到 A 的"AOP 代理"(不含 @Async 增强)后,A 在 initializeBean 阶段又会被 @Async 后置处理器再次代理,导致 B 持有的是"旧代理"而容器持有"新代理"——@Async 失效。Spring 通过 BeanCurrentlyInCreationException 或注入不匹配暴露此问题。解决:用 @Lazy 或重构消除环。

常见陷阱
易错点
  • 误以为三级缓存能解决构造器循环依赖:构造器阶段 Bean 未实例化,无法进入任何缓存,必报错。
  • 误以为 prototype 作用域有三级缓存:prototype 不使用缓存,每次 getBean 都新建,循环依赖直接报错。
  • 忽略 Spring Boot 2.6+ 默认禁止循环依赖:升级到 Spring Boot 2.6 及以上后循环依赖默认报错,需显式 spring.main.allow-circular-references=true,但这是技术债,应重构消除。
  • 三级缓存 = 解决性能问题:三级缓存的根本动机是 AOP 代理正确性(保证 B 注入代理),性能优化只是副作用,不要本末倒置。
  • 误以为 @Lazy 是"延迟到调用时才创建整个 Bean":@Lazy 注入的是代理,首次方法调用触发 getBean,但 Bean 的创建仍是完整的(含三级缓存机制)。
扩展补全

三级缓存解决了 setter/field 注入的循环依赖,但构造器注入的循环依赖仍需 @Lazy 兜底。这一层深入 @Lazy 的代理机制、与三级缓存的本质区别、以及循环依赖的重构方向。

  • @Lazy 的代理机制:Spring 在注入时不会真正实例化依赖 Bean,而是通过 ProxyFactory 生成一个代理对象注入。代理对象内部持有 BeanFactory 引用,首次调用方法时才触发 getBean 获取真实 Bean,后续调用直接委托给已初始化的目标。
  • 与三级缓存的关系:@Lazy 是"注入代理"策略,三级缓存是"提前暴露 ObjectFactory"策略,两者可叠加但不是同一机制。@Lazy 在注入点生效(构造器参数、setter 参数、field),三级缓存在 getSingleton 查询链中生效。即使关闭循环依赖允许开关,@Lazy 仍可工作。
  • 构造器注入的主要方案:构造器注入的循环依赖无法被三级缓存解决(实例化阶段未进入缓存),@Lazy 是主要方案——注入代理对象使构造器参数满足,延迟真实依赖到方法调用时。
  • Spring Boot 2.6+ 语境下的 @Lazy:spring.main.allow-circular-references=false(默认)时,setter/field 循环依赖直接报错,但 @Lazy 标注的注入点仍可工作——因为代理对象打破了构造期的强依赖,Spring 不再检测到循环。
  • @Lazy 的陷阱:代理对象调用方法时才初始化,若初始化失败会在业务调用时报错而非启动时报错,延迟了问题发现时机。生产环境中应避免对核心依赖使用 @Lazy,仅作为 legacy 代码的过渡方案。
  • 重构方向一:引入中间层:将 A↔B 的循环依赖拆为 A→C←B,把共享逻辑提取到第三方 Bean C,A 和 B 各自单向依赖 C,消除环。
  • 重构方向二:事件驱动解耦:A 和 B 通过 ApplicationEvent 交互,A 发布事件、B 监听事件,不直接依赖 B 的 Bean,用 ApplicationEventPublisher + @EventListener 实现松耦合。
  • 重构方向三:接口隔离:A 依赖 B 的接口(BInterface),B 实现该接口但不依赖 A;或引入 ObjectProvider<B> 延迟获取,语义比 @Lazy 更显式。
深入解析

@Lazy 代理与三级缓存的 ObjectFactory 代理虽然都生成"代理对象",但时机和机制完全不同。三级缓存的 getEarlyBeanReference 是在 Bean 实例化后、被其他 Bean 引用时触发,代理的是已实例化的半成品;而 @Lazy 代理是在注入时由 ContextAnnotationAutowireCandidateResolver 检测到 @Lazy 注解后,用 ProxyFactory 创建的完全独立的代理,目标 Bean 甚至还未开始实例化。这意味着 @Lazy 可以打破三级缓存无法处理的构造器循环——因为代理对象不依赖目标 Bean 的任何生命周期阶段。但代价是:首次方法调用的延迟(需触发完整 Bean 创建)、初始化错误的延迟暴露、以及代理方法的调用开销。架构师应将 @Lazy 视为"代码异味的技术债",配合 TODO 标记和代码审查跟踪,最终通过重构消除。

C05.03
Spring AOP 的 JDK 动态代理与 CGLIB 如何选择?高级P1
本质问题

这道题考察 AOP 两种代理实现机制的底层差异(JDK Proxy 基于接口反射调用 vs CGLIB 基于继承生成子类字节码)、Spring 的自动选择策略演进、以及代理链的执行模型(ReflectiveMethodInvocation 责任链)。更深一层,它考察"代理是横切逻辑织入的载体"——理解代理对象如何替代目标对象被注入,才能回答所有"AOP 失效"类问题。

演进脉络

Spring 2.x:默认 JDK 动态代理(需目标实现接口);若要用 CGLIB 需 <aop:config proxy-target-class="true">。此时 CGLIB 还需额外引入 cglib 与 asm 依赖。

Spring 3.2:CGLIB 与 asm 被打包进 spring-core(org.springframework.cglib 命名空间重打包),无需额外依赖,降低使用门槛。

Spring 4.x:策略不变——目标类实现接口则用 JDK Proxy,否则用 CGLIB。需显式配置 @EnableAspectJAutoProxy(proxyTargetClass=true) 强制 CGLIB。

Spring Boot 2.x(自动配置收紧):Boot 自动配置默认改为 CGLIB(spring.aop.proxy-target-class=true)。注意这是 Spring Boot 2.x 的 AopAutoConfiguration 默认值,不是 Spring Framework 的通用默认——Framework 原生 @EnableAspectJAutoProxy 仍默认 JDK 代理。原因是 JDK Proxy 基于接口,注入时只能用接口类型,否则报 BeanNotOfRequiredTypeException,开发者困扰多。CGLIB 基于子类,注入类类型或接口类型均可,更符合直觉。

超越 Spring AOP:AspectJ 编译时织入。Spring AOP 是运行时代理(代理对象在 Bean 生命周期生成),有局限:只能代理 public 方法、自调用不走代理、性能有反射开销。AspectJ 提供编译时织入(CTW)和加载时织入(LTW),在字节码层面织入切面,无代理对象,支持字段/构造器/非 public 方法。性能敏感或需要细粒度织入时考虑 AspectJ,但复杂度高,Spring AOP 已覆盖 90% 场景。

第一性原理

两种代理的底层实现对比:

维度 JDK 动态代理 CGLIB 代理
原理 基于接口,Proxy.newProxyInstance 运行时生成实现接口的代理类 基于继承,Enhancer 用 ASM 生成目标类的子类字节码
要求 目标必须实现至少一个接口 目标类不能是 final,方法不能是 final
调用机制 InvocationHandler.invoke 反射调用目标方法 MethodInterceptor.intercept 通过 FastClass 索引调用(避免反射)
性能 创建快,调用稍慢(反射) 创建慢(生成字节码),调用快(FastClass 索引)
注入类型 只能用接口类型注入 接口或类类型均可注入
Boot 2.x+ 默认 否 是(spring.aop.proxy-target-class=true)

Spring 的选择逻辑(DefaultAopProxyFactory.createAopProxy):

// DefaultAopProxyFactory 核心判断逻辑(简化)
public AopProxy createAopProxy(AdvisedSupport config) {
    if (config.isOptimize() || config.isProxyTargetClass()
            || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);   // 目标本身就是接口/已是代理 → JDK
        }
        return new ObjenesisCglibAopProxy(config); // 否则 CGLIB
    }
    return new JdkDynamicAopProxy(config);          // 有接口且未强制 CGLIB → JDK
}

// Boot 2.x 自动配置:spring.aop.proxy-target-class 默认 true → 走 CGLIB 分支

代理链执行模型(ReflectiveMethodInvocation 责任链):当多个切面(@Around/@Before/@After)作用于同一方法,Spring 用 ReflectiveMethodInvocation 组织成责任链。代理对象调用方法时,依次执行各 Aspect 的 Advice,最后才调用目标方法。

flowchart LR C["调用方"] --> P["代理对象 proxy.method()"] P --> M1["ReflectiveMethodInvocation 责任链"] M1 --> A1["Aspect1 @Around (前)"] A1 --> A2["Aspect2 @Before"] A2 --> T["目标方法 target.method()"] T --> A3["Aspect2 @AfterReturning"] A3 --> A4["Aspect1 @Around (后)"] A4 --> R["返回结果"] style P fill:#dbeafe,stroke:#2563eb style T fill:#fef3c7,stroke:#d97706
图 5.3-1 AOP 代理链执行:多个切面按责任链顺序织入,目标方法在链末执行

切面执行顺序由 @Order 或 Ordered 接口决定,值越小优先级越高(外层)。@Around 必须显式调用 ProceedingJoinPoint.proceed() 才会继续链,否则目标方法不执行。

工程实践

选型建议:Spring Boot 2.x+ 默认 CGLIB,无需纠结。仅在以下场景显式用 JDK Proxy:① 目标类是 final 无法被 CGLIB 继承;② 强制要求代理对象只能以接口形态暴露(架构约束);③ 对代理创建速度敏感且调用频率低(JDK 创建更快)。

// JDK 动态代理:基于接口
UserService proxy = (UserService) Proxy.newProxyInstance(
    loader, new Class[]{UserService.class},
    (p, method, args) -> {
        before();
        Object ret = method.invoke(target, args);  // 反射调用
        after();
        return ret;
    });

// CGLIB:基于子类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    before();
    Object ret = proxy.invokeSuper(obj, args);   // FastClass 索引调用,非反射
    after();
    return ret;
});
UserServiceImpl cglibProxy = (UserServiceImpl) enhancer.create();
自调用失效的解决

类内部 methodA() 调用本类 methodB()(被 @Transactional 标注)不走代理,事务失效。解决方案:① 注入自身代理 @Autowired private XxxService self; 后 self.methodB();② AopContext.currentProxy() 获取当前代理(需 @EnableAspectJAutoProxy(exposeProxy=true));③ 抽取 methodB 到独立 Bean。推荐方案①,最简洁。

深度追问

追问 1:CGLIB 的 FastClass 机制如何避免反射?

CGLIB 为目标类和代理类各生成一个 FastClass 子类,为每个方法分配一个 index。调用时通过 index 直接调用对应的 switch 分支方法(invoke(index, obj, args) 内部是 switch-case 直接调用方法),而非 Method.invoke 反射。这是 CGLIB 调用快于 JDK Proxy 的根本原因。代价是多生成两个 FastClass 类,增加类加载开销。

追问 2:Spring AOP 与 AspectJ 的本质区别?

Spring AOP 是运行时代理——在 Bean 生命周期 postProcessAfterInitialization 用 ProxyFactory 生成代理对象,代理对象包裹目标对象,调用时通过责任链织入切面。它依赖 IoC 容器,只能作用于 Spring Bean 的 public 方法。AspectJ 是编译时/加载时织入——ajc 编译器在字节码生成阶段直接将切面代码插入目标方法(CTW),或 LTW 用 ClassLoader 在类加载时修改字节码。AspectJ 无代理对象,支持字段/构造器/非 public 方法/静态方法,性能更高但需 ajc 编译器或 javaagent。Spring 集成 AspectJ 的注解语法(@Aspect/@Pointcut),但默认仍用 Spring AOP 运行时代理实现。

追问 3:为什么 final 方法无法被 CGLIB 代理?

CGLIB 通过生成目标类的子类并重写方法来织入切面。final 方法不可被子类重写,故无法代理。同理 final 类不可被继承,无法用 CGLIB。Kotlin 的类默认 final,需用 open 关键字修饰才能被 CGLIB 代理,这是 Kotlin + Spring 的常见坑。

常见陷阱
易错点
  • 自调用不走代理:类内方法互调是 this(目标对象),非代理对象,AOP/事务失效。
  • final 类/方法无法 CGLIB:Kotlin 类默认 final,需 open;Java 工具类常 final。
  • 注入类型不匹配:JDK Proxy 下 @Autowired XxxServiceImpl 报错(代理是实现接口的,非 Impl 类型),应注入接口 XxxService。CGLIB 下可注入 Impl。
  • @Around 忘记 proceed():不调用 proceed() 则目标方法与后续链都不执行,业务逻辑被吞。
  • 误以为 AOP 能代理 private 方法:Spring AOP 基于代理,private 方法对代理对象不可见,无法织入。需 AspectJ。
C05.04
@Transactional 的 7 种传播行为与失效场景?高级P0
本质问题

这道题考察三层:① 7 种传播行为的精确语义(是否复用/挂起/新建事务);② 事务代理失效的根因(自调用/异常不匹配/非 public/非 Spring Bean);③ 事务挂起与 savepoint 的实现。@Transactional 本质是 AOP 的应用——事务逻辑通过 TransactionInterceptor(一个 MethodInterceptor)织入,失效场景本质都是"代理未生效"或"异常未正确传播到拦截器"。

演进脉络

编程式事务(TransactionTemplate/PlatformTransactionManager):早期 Spring 通过 TransactionTemplate.execute 手动控制事务边界,侵入业务代码。

XML 声明式事务:TransactionProxyFactoryBean 或 <tx:advice> + <aop:config>,用 AOP 把事务逻辑织入,业务代码无感知,但配置繁琐。

@Transactional 注解(Spring 2.5+):注解驱动的声明式事务,<tx:annotation-driven/> 或 @EnableTransactionManagement 开启,TransactionInterceptor 解析注解属性(传播/隔离/回滚)。这是当前主流。

Spring 5+ / Boot:自动配置 DataSourceTransactionManager,@Transactional 开箱即用。Spring 6 新增 @Transactional 的 label 属性用于自定义标签,且对 reactive 事务(ReactiveTransactionManager)支持完善。

第一性原理

7 种传播行为(Propagation 枚举),核心是回答"当前有/无事务时怎么办":

传播行为 当前有事务 当前无事务 典型场景
REQUIRED(默认) 加入当前事务 新建事务 绝大多数业务方法
REQUIRES_NEW 挂起当前事务,新建独立事务 新建事务 日志/审计,独立提交不受外层影响
NESTED 在当前事务内创建 savepoint 子事务 新建事务 部分回滚(子事务回滚到 savepoint,外层可继续)
SUPPORTS 加入当前事务 非事务执行 查询方法,有无事务皆可
NOT_SUPPORTED 挂起当前事务,非事务执行 非事务执行 耗时操作不希望占用事务连接
MANDATORY 加入当前事务 抛异常 强制要求调用方有事务
NEVER 抛异常 非事务执行 强制要求无事务

REQUIRED vs REQUIRES_NEW vs NESTED 的本质区别:

  • REQUIRED:内外是同一个事务,内层异常会导致外层也回滚(除非外层 catch 住,但事务已标记 rollback-only,外层提交时仍抛 UnexpectedRollbackException)。
  • REQUIRES_NEW:内层是独立的新事务,使用新的数据库连接。外层挂起(保存当前连接与事务状态),内层提交/回滚不影响外层。注意:会占用两个连接,连接池不足时可能死锁。
  • NESTED:内外是同一个事务(同一连接),但内层开始时创建 savepoint。内层异常回滚到 savepoint(局部回滚),外层可选择继续或整体回滚。依赖数据库 savepoint 支持(MySQL InnoDB 支持)。

事务执行流程(TransactionInterceptor):

// TransactionInterceptor.invoke 简化逻辑
public Object invoke(MethodInvocation invocation) throws Throwable {
    TransactionAttribute txAttr = computeTransactionAttribute(invocation); // 解析 @Transactional
    PlatformTransactionManager tm = determineTransactionManager();
    // 1. 获取或创建事务(按传播行为决定)
    TransactionInfo txInfo = tm.getTransaction(txAttr);
    Object retVal;
    try {
        retVal = invocation.proceed();          // 2. 执行业务方法
    } catch (Throwable ex) {
        completeTransactionAfterThrowing(txInfo, ex); // 3. 按回滚规则判断是否回滚
        throw ex;
    } finally {
        cleanupTransactionInfo(txInfo);  // 恢复旧事务状态(挂起恢复)
    }
    commitTransactionAfterReturning(txInfo); // 4. 提交
    return retVal;
}

回滚规则:默认对 RuntimeException 和 Error 回滚,对受检异常(checked)不回滚。可通过 rollbackFor / noRollbackFor 定制。这是最易踩的坑。

工程实践

事务失效的 7 大场景与根因:

  1. 自调用(同类方法互调):不经过代理对象,事务拦截器不触发。解决:@Autowired self 或 AopContext.currentProxy()。
  2. 方法非 public:@Transactional 默认只对 public 方法生效(Spring 5.1 起可通过 @EnableTransactionManagement(publicMethodsOnly=false) 扩展到 protected/package 方法,但不推荐——丧失 AOP 代理语义的一致性)。
  3. 异常被 catch 吞掉:异常未抛出到 TransactionInterceptor,拦截器认为正常返回而提交。解决:catch 后 rethrow,或手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
  4. 异常类型不匹配:抛出受检异常但未配 rollbackFor,默认不回滚。解决:@Transactional(rollbackFor = Exception.class)。
  5. 类非 Spring Bean:new 出来的对象不被 IoC 管理,无代理。解决:交给容器管理。
  6. 数据库引擎不支持事务:MySQL MyISAM 无事务,需 InnoDB。
  7. 传播行为配错:内层 NOT_SUPPORTED/NEVER 导致非事务执行。
// 典型失效:自调用 + 异常被吞
@Service
public class OrderService {
    @Transactional
    public void createOrder() {
        saveOrder();          // 1. 自调用 this.saveOrder() 不走代理;若 saveOrder 标注 @Transactional 则注解失效
        try {
            deductStock();     // 2. 自调用,异常被吞
        } catch (Exception e) {
            log.error("库存扣减失败", e);  // 3. 异常未 rethrow,外层事务提交 → 脏数据
        }
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deductStock() {} // 期望新事务,但自调用未走代理,实际在外层事务
}

// 正确:注入自身代理
@Service
public class OrderService {
    @Autowired private OrderService self;  // 注入的是代理对象
    @Transactional
    public void createOrder() {
        self.deductStock();       // 通过代理调用,REQUIRES_NEW 生效
    }
}
最佳实践
  • 统一规范:@Transactional(rollbackFor = Exception.class) 作为团队默认,避免受检异常不回滚的坑。
  • 事务方法不要 try-catch 吞异常:要么 rethrow,要么手动标记回滚。
  • REQUIRES_NEW 慎用:占用额外连接,高并发下可能耗尽连接池。日志审计可用异步或独立数据源。
  • 事务范围最小化:长事务持有锁久、连接占用久,把查询/外部调用移出事务方法。
深度追问

追问 1:REQUIRED 下内层异常被 catch,外层提交为何还报 UnexpectedRollbackException?

因为内外是同一事务,内层抛异常时 TransactionInterceptor 已将事务标记为 rollback-only(即使外层 catch 住异常,事务标记无法清除)。外层方法正常返回后 commit 时检测到 rollback-only,抛 UnexpectedRollbackException 提示"事务已被标记回滚"。这是 REQUIRED 的强一致性保证——要么全成功,要么全回滚。若要内层失败不影响外层,用 REQUIRES_NEW(独立事务)或 NESTED(savepoint 局部回滚)。

追问 2:REQUIRES_NEW 挂起当前事务时,连接如何处理?会死锁吗?

挂起时保存当前事务的 ConnectionHolder 并解绑(从 TransactionSynchronizationManager 的 ThreadLocal 移除),新事务从 DataSource 获取新连接。因此内外层持有不同连接。死锁风险:若外层事务已锁某行,内层 REQUIRES_NEW 事务要操作同一行,会因锁等待死锁(外层等待内层释放,内层等待外层释放锁)。设计上避免 REQUIRES_NEW 操作外层已锁的同一行。

追问 3:NESTED 的 savepoint 如何实现?与数据库 savepoint 关系?

NESTED 通过 Connection.setSavepoint() 创建 JDBC savepoint,内层异常时 rollback(savepoint) 只回滚到该点,外层可继续。关键:内外层是同一事务同一连接,savepoint 是数据库层面的部分回滚机制。MySQL InnoDB 支持 savepoint。注意:NESTED 不能脱离外层事务独立提交——外层回滚则 savepoint 也回滚。这与 REQUIRES_NEW 的独立提交有本质区别。

常见陷阱
易错点
  • 误以为 catch 住异常事务就不回滚:REQUIRED 下内层已标记 rollback-only,外层提交必报错。
  • 受检异常不回滚:抛 IOException 默认不回滚,需 rollbackFor。
  • REQUIRES_NEW 死锁:内外层操作同一行导致锁等待。
  • 混淆 NESTED 与 REQUIRES_NEW:NESTED 是同事务 savepoint(外层回滚则全回滚),REQUIRES_NEW 是独立事务(独立提交)。
  • 事务方法内远程调用:RPC 持有事务连接与锁,超时放大故障。应移出事务或降级。
生产实践

上线门禁:上下文刷新耗时、循环依赖与启动失败演练。表达提纲:生命周期阶段→配置/Bean 证据→修复与回归。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C05.05
Spring 事件机制(ApplicationEvent)原理?高级P2
本质问题

这道题考察 Spring 事件机制的三角色协作(发布者 ApplicationEventPublisher / 监听者 ApplicationListener / 广播器 ApplicationEventMulticaster),以及从编程式到注解式的演进。更深一层,它考察"事件是观察者模式 + 解耦手段"的认知,以及事务事件(@TransactionalEventListener)如何把"事件投递"与"事务提交"绑定,解决"事务未提交就发消息"的经典问题。

演进脉络

Spring 早期(ApplicationEvent 必须继承):自定义事件必须继承 ApplicationEvent,监听器必须实现 ApplicationListener 接口,通过 context.publishEvent 发布。编程式、侵入性强。

Spring 4.2(@EventListener 注解):引入 @EventListener,任意 Bean 的任意 public 方法可标注为监听器,方法参数即事件类型。同时事件不再必须继承 ApplicationEvent,任意 POJO 都可作为事件。大幅降低使用门槛。

Spring 4.2(@TransactionalEventListener):解决"事务提交后才发消息"问题。监听器可绑定事务阶段(AFTER_COMMIT/BEFORE_COMMIT/AFTER_ROLLBACK/AFTER_COMPLETION),默认 AFTER_COMMIT。事务未提交则不触发,避免脏数据外泄。

异步事件(@Async + 配置):默认事件是同步广播(发布线程阻塞执行所有监听器)。通过 @Async 标注监听器方法,或配置 SimpleApplicationEventMulticaster 的 Executor,实现异步广播。Spring Boot 自动配置的 Multicaster 默认同步。

Spring 5+:事件支持泛型(ResolvableType),可按泛型类型精确路由监听器。

第一性原理

核心三角色:

  • ApplicationEventPublisher:发布接口,ApplicationContext 继承它,故 context.publishEvent(event) 或注入 ApplicationEventPublisher 发布。
  • ApplicationEventMulticaster:广播器,容器中只能有一个(applicationEventMulticaster bean)。publishEvent 实际委托给它。默认实现 SimpleApplicationEventMulticaster。
  • ApplicationListener:监听器,注册到 Multicaster,按事件类型(ResolvableType)匹配。
flowchart LR P["发布者 publishEvent(event)"] --> AC["ApplicationContext"] AC --> MC["ApplicationEventMulticaster
SimpleApplicationEventMulticaster"] MC --> E{"Executor 是否为 null?"} E -- "是(默认) 同步" --> S1["当前线程遍历监听器"] E -- "否 配置了线程池" --> S2["线程池异步执行监听器"] S1 --> L1["Listener1.onApplicationEvent"] S1 --> L2["@EventListener 方法"] S1 --> L3["@TransactionalEventListener
(事务提交后)"] S2 --> L1 MC -.注册.-> R["EventListenerMethodProcessor
扫描 @EventListener 注册为 ApplicationListener"] style MC fill:#dbeafe,stroke:#2563eb style E fill:#fef3c7,stroke:#d97706
图 5.5-1 事件广播流程:ApplicationContext 委托 Multicaster,按 Executor 是否为空决定同步/异步

@EventListener 的解析原理:EventListenerMethodProcessor(一个 BeanFactoryPostProcessor + BeanPostProcessor)在 Bean 创建后扫描所有方法上的 @EventListener,用 ApplicationListenerMethodAdapter 包装成 ApplicationListener 注册到 Multicaster。方法参数类型即监听的事件类型。

@TransactionalEventListener 的原理:它是 @EventListener 的特化,额外实现 TransactionalApplicationListener。监听器收到事件后不立即执行,而是通过 TransactionSynchronizationManager 注册一个 TransactionSynchronization,在事务的指定阶段(afterCommit 等)回调。若发布时无事务,默认不触发(fallbackExecution=false),可设 fallbackExecution=true 在无事务时直接执行。

// 事件定义:任意 POJO,无需继承 ApplicationEvent(Spring 4.2+)
public record OrderCreatedEvent(long orderId, String userId) {}

// 发布:注入 ApplicationEventPublisher
@Service
public class OrderService {
    @Autowired private ApplicationEventPublisher publisher;

    @Transactional
    public void create(OrderDTO dto) {
        Order order = save(dto);
        publisher.publishEvent(new OrderCreatedEvent(order.getId(), dto.getUserId()));
        // 事件在事务提交后才被 @TransactionalEventListener 处理
    }
}

// 监听:事务提交后执行(避免事务未提交就发消息/调外部)
@Component
public class OrderEventListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Async                                    // 异步执行,不阻塞主事务
    public void onOrderCreated(OrderCreatedEvent event) {
        sendNotification(event);              // 发短信/推送
        publishMqMessage(event);             // 发 MQ
    }
}
工程实践

异步事件配置:默认 Multicaster 同步,要让 @Async 生效需 @EnableAsync。或全局配置 Multicaster 的 Executor:

// 方式1:全局异步(所有事件异步)
@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster multicaster() {
    SimpleApplicationEventMulticaster m = new SimpleApplicationEventMulticaster();
    m.setTaskExecutor(asyncExecutor());   // 所有事件异步执行
    return m;
}

// 方式2:单方法异步(推荐,精细控制)
@EventListener @Async
public void onEvent(MyEvent e) { ... }
实战经验
  • 事务后发消息用 @TransactionalEventListener(AFTER_COMMIT):避免事务未提交就发 MQ 导致消费端查不到数据。这是生产环境高频坑。
  • 异步事件注意上下文丢失:@Async 切换线程,ThreadLocal/MDC 上下文丢失,需用 TaskDecorator 透传 TraceId。
  • 事件异常隔离:同步事件中某监听器抛异常会中断后续监听器并影响发布者;异步事件异常不影响发布者但需配置异常处理器(AsyncUncaughtExceptionHandler)。
  • 避免事件循环依赖:监听器又发布同类事件形成环。设计上事件应单向流转。
深度追问

追问 1:@TransactionalEventListener 在无事务环境下如何表现?

默认 fallbackExecution=false,无事务时事件被丢弃(不执行监听器,仅 DEBUG 日志)。这是因为该注解设计初衷是绑定事务,无事务则无意义。若希望无事务时也执行,设 fallbackExecution=true。常见坑:单元测试无事务环境,事件监听器不触发,误以为代码有 bug。

追问 2:早期事件(容器启动前发布)会被监听器收到吗?

不会。监听器 Bean 在 registerListeners(第 10 步)注册,但 @EventListener 解析依赖 Bean 实例化(第 11 步)。容器启动过程中发布的事件(如 ContextRefreshedEvent)会被早期注册的 ApplicationListener 收到,但 @EventListener 方法若监听启动事件,需确保其 Bean 已创建。Spring 通过 earlyApplicationEvents 队列缓存启动早期事件,待 Multicaster 就绪后补发。

追问 3:事件机制与 MQ 的区别?何时用事件,何时用 MQ?

Spring 事件是进程内的观察者模式,同步或线程池异步,无持久化、无跨进程、应用重启即丢失。MQ 是跨进程的可靠消息,持久化、可重试、可削峰。选型:进程内解耦(如订单创建后刷新缓存、发邮件)用 Spring 事件;跨服务/需可靠投递/需削峰用 MQ。两者常结合:Spring 事件(@TransactionalEventListener AFTER_COMMIT)保证事务提交后投递,监听器内再发 MQ 保证可靠。

常见陷阱
易错点
  • 误以为事件默认异步:默认同步广播,监听器异常会阻塞发布者。
  • 事务未提交就发消息:用普通 @EventListener 在事务方法内发布,监听器立即执行但事务可能回滚,导致脏数据外泄。应用 @TransactionalEventListener(AFTER_COMMIT)。
  • @TransactionalEventListener 无事务时不触发:fallbackExecution 默认 false,测试环境无事务会"失效"。
  • 异步事件丢 TraceId:@Async 换线程,MDC 上下文丢失。
  • 事件监听器异常未处理:同步事件异常传播给发布者;异步事件需配置 AsyncUncaughtExceptionHandler。
C05.06
Spring 中用到了哪些设计模式?高级P1
本质问题

这道题不是简单的"模式背诵",而是考察候选人能否将框架机制与设计模式对应,理解"Spring 是 GoF 设计模式的集大成者"。优秀回答应指出模式名 + Spring 应用点 + 解决的问题,并体现"框架通过模式实现可扩展性与解耦"的架构思维。这是高级开发与架构师的分水岭——前者会用框架,后者理解框架为何这样设计。

演进脉络

GoF 设计模式(1994):23 种经典模式,是面向对象设计的经验总结。Spring 1.x 起大量应用工厂、单例、模板、代理等模式构建核心骨架。

Spring 1.x~2.x(XML 时代):工厂模式(BeanFactory)+ 单例(单例池)+ 模板方法(JdbcTemplate)+ 代理(AOP)+ 观察者(事件)构成核心。

Spring 3.x+(注解与扩展):策略模式(代理选择)、责任链(拦截器链)、适配器(HandlerAdapter)、装饰器(BeanWrapper)逐步完善。

Spring 5.x(函数式与响应式):函数式接口(Supplier/Function 注册 Bean)、响应式 Adapter 模式适配不同 Reactive Streams 实现。设计模式的应用随框架演进持续扩展。

第一性原理

Spring 中的设计模式应用全景:

设计模式 Spring 应用点 解决的问题
简单工厂/工厂方法 BeanFactory(getBean)、FactoryBean(getObject) 解耦对象的创建与使用,Bean 由容器创建
单例 singletonObjects 单例池,默认 scope=singleton 全局唯一实例,减少创建开销与状态不一致
代理 AOP(JDK Proxy/CGLIB)、@Transactional、@Async、@Cacheable 横切逻辑织入,不侵入业务代码
模板方法 JdbcTemplate/RestTemplate/TransactionTemplate/AbstractApplicationContext.refresh 定义骨架,子类重写可变步骤
观察者 ApplicationEvent/ApplicationListener/ApplicationEventMulticaster 事件解耦,发布者无需知道监听者
责任链 HandlerInterceptor 链、AOP 代理链(ReflectiveMethodInvocation)、Filter 链 多处理器按序处理,可中断
适配器 HandlerAdapter(适配不同 Controller 类型)、AdvisorAdapter 统一接口适配不同实现
装饰器 BeanWrapper、HttpHeadResponseDecorator 动态增强对象功能
策略 Resource 接口(ClassPathResource/FileSystemResource)、ProxyFactory 代理选择、InstantiationStrategy 同一接口多种实现可切换
建造者 BeanDefinitionBuilder、UriComponentsBuilder 分步构建复杂对象
原型 prototype 作用域,@Lookup 方法注入 每次获取新实例
委派 DispatcherServlet.doDispatch 委派给 HandlerAdapter 委派者负责任务分发,不亲自执行

典型示例:模板方法模式(JdbcTemplate):

// JdbcTemplate.execute 是模板方法:固定连接获取/释放/异常处理,可变部分由回调实现
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    Connection con = DataSourceUtils.getConnection(this.dataSource);  // 固定步骤1:获取连接
    Statement stmt = null;
    try {
        stmt = con.createStatement();
        return action.doInStatement(stmt);          // 可变步骤:由回调决定 SQL 与结果提取
    } catch (SQLException ex) {
        throw getExceptionTranslator().translate(...);  // 固定步骤2:统一异常转换
    } finally {
        JdbcUtils.closeStatement(stmt);                 // 固定步骤3:释放资源
        DataSourceUtils.releaseConnection(con, ...);
    }
}

// 使用者只需写可变部分
jdbcTemplate.query("SELECT * FROM user WHERE id=?", (rs, i) -> new User(rs.getLong(1)), 1L);
工程实践

理解模式背后的原则比记住模式更重要。Spring 的扩展性来自"开闭原则"(对扩展开放,对修改关闭)的实现:BeanPostProcessor/BeanFactoryPostProcessor 是开放扩展点,新增逻辑无需改框架代码;FactoryBean 让用户控制 Bean 创建;模板方法让用户填充可变步骤。架构师设计中间件时,应模仿 Spring 的扩展点设计——用接口定义扩展契约,用责任链/策略组合扩展实现,而非 if-else 堆砌。

回答技巧

不要只列模式名,要用"模式 + Spring 位置 + 设计意图"三段式回答。例如:"工厂模式——BeanFactory 作为统一工厂,getBean 方法创建 Bean,意图是解耦对象的创建与使用,让业务代码只声明依赖不关心创建细节"。再举 3-4 个深入的即可,不必 23 种全列。

深度追问

追问 1:IoC/DI 与工厂模式的关系?

IoC 容器本质是一个超级工厂(BeanFactory),但它反转了控制——传统工厂由调用方主动调用工厂方法获取对象,IoC 由容器主动把对象注入到需要的地方(DI)。DI 是 IoC 的实现方式,工厂是 IoC 的底层载体。可以说 IoC = 工厂模式 + 控制反转 + 依赖注入的组合升华。

追问 2:Spring 的单例 Bean 与单例模式是同一个东西吗?

不是。单例模式(GoF)是一个类在 JVM 中只有一个实例,通常用 private 构造器 + 静态字段实现,类自身控制唯一性。Spring 单例 Bean 是每个 ApplicationContext 中每个 BeanDefinition 对应一个实例,是容器级别的单例,用 singletonObjects 缓存实现。同一类可注册多个 BeanDefinition(多个 Bean 名),就有多个实例;多个 ApplicationContext 各有独立的单例。所以 Spring 单例是"容器级单例",不是"类级单例"。

追问 3:FactoryBean 与 BeanFactory 的区别?

BeanFactory 是 IoC 容器的根接口(工厂本身)。FactoryBean 是一个特殊的 Bean(工厂 Bean),它本身被容器管理,但 getBean(factoryBeanName) 返回的是 FactoryBean.getObject() 生产的对象,而非 FactoryBean 本身(要获取 FactoryBean 本身需前缀 &)。典型应用:MyBatis 的 SqlSessionFactoryBean、Spring AOP 的 ProxyFactoryBean。FactoryBean 用于"创建逻辑复杂的 Bean",是对简单工厂的扩展。

常见陷阱
易错点
  • 混淆单例模式与单例 Bean:前者类级,后者容器级。
  • 混淆 FactoryBean 与 BeanFactory:前者是生产对象的 Bean,后者是容器。
  • 只背模式名不结合 Spring:重点是讲清"模式在 Spring 哪里用、解决什么问题"。
  • 误以为 AOP 只是代理模式:AOP 还用了责任链(拦截器链)、策略(代理选择)、适配器(AdvisorAdapter)。
  • 忽略模板方法的广泛应用:JdbcTemplate 只是典型,refresh() 本身就是模板方法。
生产实践:Spring 设计模式在中间件与框架扩展中的落地

设计模式的价值不在于"背了多少种",而在于理解 Spring 如何用它们构建可扩展的框架骨架。架构师设计自研中间件时,往往沿用同样的模式组合。

设计模式 Spring 内置实现 自研中间件对应场景
工厂模式 BeanFactory / FactoryBean RPC 框架的代理 Bean 工厂(如 Dubbo ReferenceBean)
代理模式 AOP Proxy(JDK/CGLIB) 声明式事务、声明式缓存、声明式限流的统一入口
责任链模式 HandlerInterceptor 链 / Filter 链 网关 Filter 链、中间件拦截器链(鉴权→限流→灰度)
模板方法模式 JdbcTemplate / refresh() 流程骨架 自研 ORM / 消息中间件的固定流程骨架 + 可覆写钩子
策略模式 Resource 加载策略 / AopProxy 策略 配置源策略(本地/远程/加密)、序列化策略切换
观察者模式 ApplicationEvent / ApplicationListener 配置变更通知、服务上下线事件广播
适配器模式 AdvisorAdapter / HandlerAdapter 多协议 RPC 适配、多消息队列连接器适配
关键结论

回答"Spring 用了哪些设计模式",不应只背模式名,而是能指着 Spring 源码说明"这里用了代理模式,是为了 AOP 织入;这里用了责任链,是为了拦截器链执行"。设计自研中间件时,用工厂 + 代理模式做声明式入口,用责任链 + 策略模式做规则引擎,用观察者模式做事件通知,用模板方法模式固化流程骨架——这就是"模式在框架中的真实价值"。

C05.07
BeanPostProcessor 与 BeanFactoryPostProcessor 的区别与扩展?架构P1
本质问题

这道题是 Spring 扩展点体系的架构级考察,区分"会用框架"与"懂框架设计"的人。它要求精确区分两个核心扩展点的介入时机(BFPP 在 BeanDefinition 阶段改图纸,BPP 在 Bean 实例阶段改产品)、职责边界、以及 Spring 在此基础上的细分扩展点体系(InstantiationAwareBPP/MergedBeanDefinitionPostProcessor/SmartInitializingSingleton/BeanDefinitionRegistryPostProcessor)。架构师理解这套体系,才能设计中间件与框架级扩展。

演进脉络

Spring 1.x(基础扩展点):BeanFactoryPostProcessor(改 BeanDefinition)与 BeanPostProcessor(改 Bean 实例)两个基础扩展点,满足基本的"配置后处理"与"实例后处理"需求。

Spring 2.x(注解驱动扩展):引入 InstantiationAwareBeanPostProcessor(细化实例化前后)和 MergedBeanDefinitionPostProcessor(合并 BeanDefinition 后处理,用于收集 @Autowired/@Resource 元数据),支撑注解注入。

Spring 3.x(注册型扩展):BeanDefinitionRegistryPostProcessor 继承 BFPP,新增 postProcessBeanDefinitionRegistry,可在 BeanDefinition 注册阶段动态注册新 BeanDefinition(ConfigurationClassPostProcessor 由此扫描注册 @Configuration)。SmartInitializingSingleton 用于所有单例就绪后的回调。

Spring 5/6(AOT 与优先级体系):扩展点优先级体系成熟(PriorityOrdered > Ordered > 无序),AOT 处理器基于扩展点实现构建期代码生成。Spring 6 新增 BeanRegistrationAotContribution 用于 AOT 冻结扩展点逻辑。

第一性原理

两个扩展点的核心区别:

维度 BeanFactoryPostProcessor BeanPostProcessor
操作对象 BeanDefinition(图纸) Bean 实例(产品)
调用时机 refresh 第5步 invokeBeanFactoryPostProcessors(所有单例创建前) refresh 第11步 finishBeanFactoryInitialization 内(每个 Bean 初始化前后)
方法 postProcessBeanFactory(ConfigurableListableBeanFactory) postProcessBeforeInitialization / postProcessAfterInitialization
能否改 BeanDefinition 能(修改/注册/移除) 不能(实例已创建)
能否改 Bean 实例 不能(实例未创建) 能(甚至返回新对象,如代理)
执行次数 容器启动一次 每个 Bean 两次(before+after)
典型实现 ConfigurationClassPostProcessor、PropertySourcesPlaceholderConfigurer AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、AbstractAutoProxyCreator

Spring 扩展点完整时序(一张图理解所有扩展点何时触发):

flowchart TD S1["refresh 第5步: invokeBeanFactoryPostProcessors"] --> S2["BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry
(动态注册 BeanDefinition,如扫描 @Configuration)"] S2 --> S3["BeanFactoryPostProcessor.postProcessBeanFactory
(修改已有 BeanDefinition,如占位符替换)"] S3 --> S4["refresh 第6步: registerBeanPostProcessors
(注册 BPP 实例,不执行)"] S4 --> S5["refresh 第11步: finishBeanFactoryInitialization"] S5 --> S6["遍历每个非懒加载单例 Bean"] S6 --> S7["InstantiationAwareBPP.postProcessBeforeInstantiation
(可短路:返回非null则跳过实例化)"] S7 --> S8["createBeanInstance 反射构造"] S8 --> S9["MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition
(收集 @Autowired/@Resource 元数据缓存)"] S9 --> S10["addSingletonFactory 三级缓存暴露"] S10 --> S11["populateBean: InstantiationAwareBPP.postProcessAfterInstantiation"] S11 --> S12["populateBean: InstantiationAwareBPP.postProcessProperties
(实际执行 @Autowired 注入)"] S12 --> S13["initializeBean: Aware 回调"] S13 --> S14["BPP.postProcessBeforeInitialization
(触发 @PostConstruct)"] S14 --> S15["InitializingBean.afterPropertiesSet / init-method"] S15 --> S16["BPP.postProcessAfterInitialization
(生成 AOP 代理)"] S16 --> S17["所有单例就绪"] S17 --> S18["SmartInitializingSingleton.afterSingletonsInstantiated
(全局就绪回调)"] style S2 fill:#f0f3f7,stroke:#6b7280 style S3 fill:#f0f3f7,stroke:#6b7280 style S16 fill:#dbeafe,stroke:#2563eb style S18 fill:#dcfce7,stroke:#059669
图 5.7-1 Spring 扩展点完整时序:BFPP 改图纸在前,BPP 改产品在后,InstantiationAwareBPP 细化实例化阶段,SmartInitializingSingleton 收尾

关键扩展点职责:

  • ConfigurationClassPostProcessor(BFPP/BeanDefinitionRegistryPostProcessor):解析 @Configuration/@ComponentScan/@Import/@Bean,注册扫描到的 BeanDefinition。是注解驱动的核心。
  • PropertySourcesPlaceholderConfigurer(BFPP):替换 BeanDefinition 中的 ${...} 占位符为实际配置值。
  • AutowiredAnnotationBeanPostProcessor(MergedBeanDefinitionPostProcessor + InstantiationAwareBPP):收集 @Autowired/@Value 元数据,并在 postProcessProperties 完成注入。
  • CommonAnnotationBeanPostProcessor:处理 @PostConstruct/@PreDestroy/@Resource。
  • AbstractAutoProxyCreator(SmartInstantiationAwareBPP):在 postProcessAfterInitialization 生成 AOP 代理,在 getEarlyBeanReference 支持循环依赖提前代理。
工程实践

自定义扩展点的典型场景:

  • BFPP 场景:动态注册 Bean(如扫描某注解自动注册 RPC 代理 Bean)、修改 BeanDefinition(如统一设置某个 Bean 的 lazy-init)、配置解密(替换加密占位符)。
  • BPP 场景:为特定注解的 Bean 织入逻辑(如自定义 @RateLimit 注解的限流代理)、属性后处理(如对 String 字段脱敏)、日志记录 Bean 创建。
// 自定义 BFPP:扫描 @RpcService 注解,动态注册 RPC 代理 Bean
@Component
public class RpcServiceRegistrar implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        for (Class<?> iface : scanRpcInterfaces()) {
            RootBeanDefinition bd = new RootBeanDefinition();
            bd.setBeanClass(RpcProxyFactoryBean.class);  // FactoryBean 生成代理
            bd.getConstructorArgumentValues().addGenericArgumentValue(iface);
            registry.registerBeanDefinition(iface.getName(), bd);
        }
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {}
}

// 自定义 BPP:为 @RateLimit 注解的方法生成限流代理
@Component
public class RateLimitProxyPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String name) {
        if (AnnotationUtils.findAnnotation(bean.getClass(), RateLimit.class) != null) {
            return ProxyFactory.getProxy(bean, new RateLimitAdvice(bean));
        }
        return bean;
    }
}
架构经验

设计中间件时,优先用 BeanFactoryPostProcessor 做"静态注册"(BeanDefinition 层面),用 BeanPostProcessor 做"动态增强"(实例层面)。注意 BPP 的过早依赖陷阱:若 BPP A 依赖普通 Bean B,则 B 会在第6步(注册 BPP 时)被提前实例化,此时其他 BPP(如 AutowiredAnnotationBPP)可能未注册,导致 B 的 @Autowired 失效。Spring 会对此打 WARN 日志。中间件设计应避免 BPP 依赖业务 Bean,用 BeanFactory.getBean 延迟获取。

深度追问

追问 1:BeanDefinitionRegistryPostProcessor 与 BeanFactoryPostProcessor 的区别?

BeanDefinitionRegistryPostProcessor 继承 BeanFactoryPostProcessor,新增 postProcessBeanDefinitionRegistry 方法。区别:BFPP 的 postProcessBeanFactory 操作已注册的 BeanDefinition(修改),而 BDRPP 的 postProcessBeanDefinitionRegistry 可动态注册新的 BeanDefinition。执行顺序上,BDRPP 先于普通 BFPP 执行,因为注册新 BeanDefinition 必须在修改之前。Spring 内置的 ConfigurationClassPostProcessor 就是 BDRPP,它扫描注解注册新 BeanDefinition。

追问 2:BeanPostProcessor 何时被实例化?为什么 BPP 不能依赖普通 Bean?

BPP 在 refresh 第6步 registerBeanPostProcessors 实例化,早于第11步的普通 Bean。这是为了让普通 Bean 创建时所有 BPP 就绪。但若 BPP A 依赖普通 Bean B(如 @Autowired B),B 会被迫在第6步提前实例化,此时其他 BPP(如处理 @Autowired 的 AutowiredAnnotationBPP)可能尚未注册,导致 B 的注入失效。Spring 对此打 WARN:"Bean X is not eligible for getting processed by all BeanPostProcessors"。设计原则:BPP 只依赖 BeanFactory 本身(用 getBean 延迟获取),不直接 @Autowired 业务 Bean。

追问 3:InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation 有何特殊用途?

追问 4:不同类型 BeanPostProcessor 的执行顺序和优先级关系?

BPP 执行顺序的完整链条:Spring 通过 PriorityOrdered > Ordered > 无序的优先级体系控制扩展点执行顺序:

  • PriorityOrdered 优先级最高:如 ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor,核心框架内置,保证基础功能先执行。
  • Ordered 次之:用户自定义可设置 Ordered.getOrder(),数值越小越优先执行。
  • 无序最后:未实现 Ordered 接口,按 BeanDefinition 注册顺序执行。

不同类型 BPP 的协作关系:Spring Boot 的自动装配依赖这个顺序链,正确顺序是:ConfigurationClassPostProcessor(扫描@Config)→ AutowiredAnnotationBeanPostProcessor(收集@Autowired)→ CommonAnnotationBeanPostProcessor(处理@Resource)→ AbstractAutoProxyCreator(AOP代理)。这个顺序决定了能否正确处理注解注入和AOP增强。

SmartInstantiationAwareBeanPostProcessor 的特殊地位:它继承了 InstantiationAwareBeanPostProcessor,在getEarlyBeanReference方法中支持循环依赖的提前代理。当Bean处于"提前引用"状态时,只有实现了 SmartInstantiationAware 的BPP能参与代理创建,这是AOP与循环依赖能够同时工作的关键。普通的BeanPostProcessor只能在初始化阶段(postProcessAfterInitialization)看到完整代理。

它在实例化之前调用,若返回非 null 对象,则跳过 Spring 的实例化流程,直接用返回的对象。典型应用:Spring AOP 的 AbstractAutoProxyCreator 利用它实现 @Aspect 的 target 缓存;MyBatis 用它为 Mapper 接口直接生成代理(Mapper 无实现类,无法反射实例化)。这是一个"短路"扩展点,慎用——返回的对象不会经历 populateBean/initializeBean,需自行完成装配。

常见陷阱
易错点
  • 混淆 BFPP 与 BPP 时机:BFPP 改图纸(BeanDefinition),BPP 改产品(实例)。
  • BPP 依赖业务 Bean:导致业务 Bean 提前实例化,错过其他 BPP 增强或注解注入失效。
  • 误以为 BPP 在 Bean 创建前执行:BPP 在实例化后执行,before/after 是"初始化前后"非"实例化前后"。
  • 忽略扩展点优先级:PriorityOrdered > Ordered > 无序,依赖顺序的场景需显式声明。
  • postProcessBeforeInstantiation 滥用:返回非 null 会跳过整个实例化,容易导致属性未注入。
C05.08
BeanFactory 与 ApplicationContext 的区别?高级P1
本质问题

这道题考察 IoC 容器的接口层次设计。BeanFactory 是最基础的容器接口(提供 getBean 等核心能力),ApplicationContext 是其子接口,在企业级场景下扩展了预实例化、事件、国际化、资源加载、AOP、注解等能力。理解两者的区别,才能回答"何时用哪个""为什么 Boot 用的是 ApplicationContext""BeanFactory 的懒加载特性"等问题。

演进脉络

Spring 1.x(BeanFactory 时代):XmlBeanFactory 是最早的具体容器,基于 XML 配置,懒加载——首次 getBean 才实例化 Bean。

Spring 1.x(ApplicationContext 引入):ClassPathXmlApplicationContext/FileSystemXmlApplicationContext 在 BeanFactory 基础上增加预实例化(启动即创建非懒加载单例)、事件、国际化、资源加载,是企业级应用推荐容器。

Spring 3.x(注解容器):AnnotationConfigApplicationContext 取代 XML 容器,基于注解配置。

Spring Boot(Web 容器变体):AnnotationConfigServletWebServerApplicationContext(Servlet)与 AnnotationConfigReactiveWebServerApplicationContext(Reactive),内嵌 Tomcat/Jetty/Netty,是 Boot 默认容器。它们都是 ApplicationContext 的实现。

第一性原理

容器接口层次:

flowchart TD BF["BeanFactory 根接口
getBean/containsBean"] BF --> LBF["ListableBeanFactory
枚举 Bean(getBeanNamesForType)"] BF --> HBF["HierarchicalBeanFactory
父子容器(parentBeanFactory)"] LBF --> AC["ApplicationContext
环境/事件/资源/国际化/AOP"] HBF --> AC AC --> CPAC["ClassPathXmlApplicationContext"] AC --> ACAC["AnnotationConfigApplicationContext"] AC --> ACSWSAC["AnnotationConfigServletWebServerApplicationContext
Boot 默认"] AC --> GAC["GenericApplicationContext
Spring 5 函数式基础"] style BF fill:#fef3c7,stroke:#d97706 style AC fill:#dbeafe,stroke:#2563eb
图 5.8-1 容器接口层次:BeanFactory 是根,ApplicationContext 扩展企业级能力,Boot 用其 Web 变体

核心区别对比:

维度 BeanFactory ApplicationContext
定位 基础 IoC 容器,最小能力 企业级容器,开箱即用
实例化时机 懒加载(首次 getBean 才创建) 预实例化(启动时创建所有非懒加载单例)
BeanPostProcessor 需手动注册,懒注册可能漏执行 自动注册并保证在 Bean 创建前就绪
事件机制 无 内置 ApplicationEventMulticaster,支持 publishEvent
国际化 无 内置 MessageSource,支持 i18n
资源加载 无统一资源抽象 实现 ResourcePatternResolver,支持 classpath:/通配符
环境抽象 无 内置 Environment,支持 profile/属性源
AOP/注解 需手动配置 自动启用(@EnableAspectJAutoProxy 等)
典型实现 XmlBeanFactory(已废弃)、DefaultListableBeanFactory AnnotationConfigApplicationContext、Boot Web 变体

懒加载的陷阱:BeanFactory 懒加载意味着 Bean 的配置错误(如找不到类、属性引用错误)不会在启动时暴露,而在运行期首次 getBean 时才报错,增加生产事故风险。ApplicationContext 预实例化则fail-fast——启动时即验证所有 Bean 配置,问题早暴露。这是 ApplicationContext 成为默认选择的根本原因。

工程实践

实际项目中 99% 用 ApplicationContext。Spring Boot 的入口 SpringApplication.run 创建的就是 AnnotationConfigServletWebServerApplicationContext。BeanFactory 接口在底层仍存在(ApplicationContext 内部委托 DefaultListableBeanFactory 作为 BeanFactory 实现),但用户无需直接使用。

父子容器场景:Spring MVC 经典模式是 父子容器——父容器(ContextLoaderListener 创建)管理 Service/DAO,子容器(DispatcherServlet 创建)管理 Controller。子容器可访问父容器的 Bean,反之不可。Spring Boot 简化为单一容器,父子容器模式逐渐淡出。

掌握要点

回答时强调三点:① ApplicationContext 是 BeanFactory 的子接口,扩展了企业级能力;② 核心差异是预实例化 vs 懒加载与自动注册 BPP,这决定了 fail-fast 与正确性;③ BeanFactory 作为底层接口仍存在,ApplicationContext 内部委托其实现。不要说"BeanFactory 已废弃"——它以 DefaultListableBeanFactory 的形式活跃在每个 ApplicationContext 内部。

深度追问

追问 1:ApplicationContext 内部的 BeanFactory 是什么?

ApplicationContext 本身不直接管理 Bean,而是委托给内部的 DefaultListableBeanFactory(通过 getBeanFactory() 获取)。GenericApplicationContext 持有一个 DefaultListableBeanFactory 字段,所有 getBean/registerBeanDefinition 调用都委托给它。所以 ApplicationContext 是 BeanFactory 的"门面 + 增强器",底层仍是 BeanFactory 实现。

追问 2:为什么 BeanFactory 懒加载会导致 BeanPostProcessor 漏执行?

BeanFactory 不会自动注册 BPP,需手动 addBeanPostProcessor。若在 getBean 创建某 Bean 后才注册某 BPP,该 Bean 就错过了这个 BPP 的增强。ApplicationContext 在 refresh 第6步统一注册所有 BPP,保证第11步创建的 Bean 都能被所有 BPP 处理。这是 ApplicationContext 自动注册 BPP 的核心价值。

追问 3:GenericApplicationContext 为何成为 Spring 5+ 基础?

GenericApplicationContext 不固定配置方式(XML/注解/函数式),通过 registerBean 函数式 API 可用 Lambda 注册 BeanDefinition,适配 AOT 与响应式。AnnotationConfigApplicationContext 继承 GenericApplicationContext,Boot 的 Web 容器变体也基于它。它取代了早期按配置方式区分的容器(如 XmlWebApplicationContext),是现代 Spring 的统一基础。

常见陷阱
易错点
  • 误以为 BeanFactory 已废弃:它以 DefaultListableBeanFactory 形式存在于每个 ApplicationContext 内部。
  • 误以为两者完全独立:ApplicationContext 继承 BeanFactory,是子接口。
  • 忽略懒加载风险:BeanFactory 懒加载导致配置错误延迟暴露。
  • 混淆 ListableBeanFactory 与 HierarchicalBeanFactory:前者枚举 Bean,后者父子容器,ApplicationContext 同时继承两者。
C05.09
Spring 类型转换与数据绑定机制?高级P2
本质问题

这道题考察 Spring 的类型转换体系(PropertyEditor→Converter→Formatter 三代演进)与数据绑定机制(DataBinder)。这是 Spring MVC 参数解析、@ConfigurationProperties 配置绑定、SpEL 求值的基础设施。理解它,才能回答"为什么前端传 String 后端能收到 Date/LocalDate""自定义参数怎么解析""@ConfigurationProperties 如何松绑定"等问题。

演进脉络

第一代:PropertyEditor(JDK 提供,Spring 早期复用):基于 java.beans.PropertyEditor 接口,通过 setAsText/getAsText 在 String 与目标类型间转换。问题:有状态且线程不安全(实例字段存中间状态),需每个转换新建实例或加锁;API 设计面向 GUI(JavaBeans 规范),不适合服务端高并发。典型:CustomDateEditor。

第二代:Converter SPI(Spring 3.0):引入 Converter<S,T> 接口,无状态、线程安全,适合服务端。配套 ConverterFactory(一组类型转换,如 String→Number 子类)与 GenericConverter(多源多目标,带上下文)。统一由 ConversionService(默认 DefaultFormattingConversionService)管理。

第三代:Formatter(Spring 3.0):面向本地化格式化的转换,Formatter<T> 在 String 与 T 间转换并带 Locale。适用于日期/货币等需本地化的场景。Formatter 内部会被适配为 Converter 注册到 ConversionService。

Spring Boot 松绑定(Relaxed Binding):在 @ConfigurationProperties 场景下,属性名支持松散匹配(kebab-case/camelCase/snake_case 互通),并复用 ConversionService 完成类型转换与 Duration/DataSize 等特殊类型解析。

第一性原理

核心接口关系:

flowchart LR PE["PropertyEditor (JDK,有状态,线程不安全)"] CV["Converter S->T (无状态,线程安全)"] CF["ConverterFactory (一对多,如 String->Enum)"] GC["GenericConverter (多对多,带上下文)"] FM["Formatter T (String<->T,带 Locale)"] CV --> CS["ConversionService
DefaultFormattingConversionService"] CF --> CS GC --> CS FM --> CS CS --> DB["DataBinder
绑定 + 校验"] DB --> MVC["Spring MVC 参数解析"] DB --> CP["@ConfigurationProperties 绑定"] CS --> SP["SpEL 求值类型转换"] style CS fill:#dbeafe,stroke:#2563eb style DB fill:#fef3c7,stroke:#d97706
图 5.9-1 类型转换体系:Converter/Formatter 注册到 ConversionService,供 DataBinder/SpEL/MVC 复用

数据绑定流程(DataBinder):DataBinder.bind(PropertyValues) 把键值对绑定到目标对象的属性上,内部对每个属性:① 用 PropertyAccessor 设置属性值;② 若类型不匹配,调用 ConversionService 转换;③ 绑定后调用 Validator 校验。Spring MVC 的 WebDataBinder 扩展了它,支持 @InitBinder 自定义。

自定义 Converter 示例:

// 自定义 Converter:String -> Money(自定义值对象)
public class StringToMoneyConverter implements Converter<String, Money> {
    @Override
    public Money convert(String source) {
        String[] parts = source.split(" ");
        return new Money(new BigDecimal(parts[0]), Currency.getInstance(parts[1]));
    }
}

// 注册到容器(Boot 自动加入 ConversionService)
@Configuration
public class ConvConfig {
    @Bean
    public Converter<String, Money> moneyConverter() {
        return new StringToMoneyConverter();
    }
}

// Spring MVC 自动用 ConversionService 把 "100 CNY" 转成 Money
@GetMapping("/pay")
public void pay(@RequestParam Money amount) { ... }
工程实践

@InitBinder 自定义参数绑定:在 Controller 内用 @InitBinder 注册自定义 PropertyEditor/Formatter,作用于当前 Controller。

// 用 @InitBinder 注册自定义日期格式(作用于当前 Controller)
@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.addCustomFormatter(new Formatter<LocalDate>() {
        @Override public LocalDate parse(String text, Locale locale) {
            return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        }
        @Override public String print(LocalDate o, Locale l) { return o.toString(); }
    });
}
最佳实践
  • 优先用 Converter 而非 PropertyEditor:Converter 无状态线程安全,PropertyEditor 有状态需谨慎。
  • 全局 Converter 注册为 Bean:Boot 自动加入 DefaultFormattingConversionService,全局生效。
  • @ConfigurationProperties 绑定自动复用 ConversionService:支持 Duration("30s")、DataSize("10MB")、Enum 等特殊类型。
  • 校验与绑定分离:DataBinder 同时做绑定与校验,但生产中推荐 @Valid/@Validated 显式触发校验,职责清晰。
深度追问

追问 1:Converter 与 GenericConverter 的区别?

Converter 是一对一(单一源类型→单一目标类型),最简单。GenericConverter 是多对多,可声明多个ConvertiblePair,并接收 TypeDescriptor(带泛型信息),适合需要根据泛型上下文决策的转换(如 Collection<String>→Collection<Integer>,需知道元素类型)。ConditionalGenericConverter 还可加 matches 条件判断是否处理。优先用 Converter,复杂场景才用 GenericConverter。

追问 2:Spring Boot 的松绑定如何实现?

松绑定(Relaxed Binding)由 ConfigurationPropertyName 实现:将属性名统一规范化(忽略大小写、连字符/下划线等价),匹配时按规范化形式比较。如 server.max-http-header-size 可对应 maxHttpHeaderSize 字段。绑定值时复用 ConversionService 做类型转换,并内置对 Duration("30s"/"1h")、DataSize("10MB")的特殊支持。

追问 3:PropertyEditor 线程不安全为何 Spring MVC 仍能用?

Spring MVC 在每个请求的 DataBinder 中新建 PropertyEditor 实例(通过 PropertyEditorRegistrar),避免共享。但 PropertyEditor 的有状态特性仍是隐患,Spring 3+ 已尽量迁移到 Converter,PropertyEditor 主要为兼容旧代码与 JavaBeans 规范保留。新代码应避免自定义 PropertyEditor。

常见陷阱
易错点
  • PropertyEditor 线程不安全:共享实例会导致并发数据错乱,需新建或用 Converter。
  • Converter 未注册:定义了 Converter 但未注册为 Bean,转换不生效。
  • @InitBinder 作用域误判:仅作用于当前 Controller,全局需配置 WebMvcConfigurer。
  • 松绑定的边界:仅 @ConfigurationProperties 支持松绑定,@Value 不支持。
C05.10
SpEL(Spring表达式语言)的原理与应用?高级P1
本质问题

这道题考察 SpEL 的解析执行模型(ExpressionParser→AST→EvaluationContext)及其在 Spring 各场景的应用(@Value/@Cacheable/@Conditional/@Secured)。SpEL 是 Spring 的"内嵌 DSL",理解它能回答"@Value 怎么解析 #{}""缓存 key 如何动态生成""条件装配如何写表达式"等问题,并能识别 SpEL 注入风险。

演进脉络

Spring 3.0(SpEL 引入):为 @Value 与 XML 配置提供表达式能力,支持字面量、属性访问、方法调用、集合操作、类型引用(T(java.lang.Math))。

Spring 3.x 增强:支持赋值、三元运算、正则匹配、Bean 引用(@beanName)、#root/#this 变量。@Cacheable/@Conditional 开始用 SpEL 动态求值。

Spring 4.x:SpEL 支持编译为字节码(SpelCompiler),高频表达式可编译提升性能;新增赋值表达式、选区投影(![])。

Spring 5/6:AOT 优化 SpEL 表达式预编译,减少运行期解析开销;对 GraalVM Native Image 友好(避免反射时用 T() 引用类型需注册反射)。

第一性原理

SpEL 三大核心组件:

  • ExpressionParser:解析表达式字符串为 AST(Expression 对象)。默认 SpelExpressionParser。
  • Expression:抽象语法树,可多次求值。支持 getValue(EvaluationContext) 与 setValue。
  • EvaluationContext:求值上下文,提供根对象、变量、函数、类型定位器、构造器。默认 StandardEvaluationContext;只读且安全的场景用 SimpleEvaluationContext(禁用类型引用/构造器,防注入)。

解析执行流程:表达式字符串 → 词法分析(Tokenizer 分词)→ 语法分析(Parser 生成 AST,如 PropertyOrFieldReference/MethodReference/Indexer 等节点)→ 求值(递归遍历 AST,按 EvaluationContext 解析引用与调用方法)。AST 可缓存复用,避免重复解析。

flowchart LR S["表达式字符串
user.name.toUpperCase()"] --> T["Tokenizer 词法分析"] T --> P["SpelParser 语法分析"] P --> AST["AST 抽象语法树
(MethodRef->PropertyRef->VariableRef)"] AST --> EC["EvaluationContext 求值"] EC --> R["结果"] AST -.缓存.-> CACHE["Expression 复用"] style AST fill:#dbeafe,stroke:#2563eb style EC fill:#fef3c7,stroke:#d97706
图 5.10-1 SpEL 解析执行流程:字符串→词法→语法→AST→上下文求值,AST 可缓存复用

SpEL 在 Spring 中的应用场景:

场景 语法 求值上下文
@Value 注入 @Value("#{systemProperties['user.home']}") BeanFactory(可引用 Bean、环境属性)
@Cacheable key @Cacheable(key="#user.id") 方法参数(#参数名)+ 返回值 #result
@Cacheable condition condition="#name.length()>3" 方法参数,决定是否缓存
@Conditional 条件装配 ConditionalOnExpression("#{...}") Environment + BeanFactory
@PreAuthorize/@Secured @PreAuthorize("hasRole('ADMIN')") 认证信息 + 方法参数 #param
@Scheduled @Scheduled(fixedDelayString="#{...}") BeanFactory

代码示例:

// 1. @Value 注入:SpEL 引用 Bean 与属性
@Value("#{systemProperties['user.home']}")
private String home;

@Value("#{@orderConfig.timeout}")  @orderConfig 引用名为 orderConfig 的 Bean
private long timeout;

// 2. @Cacheable 动态 key + condition
@Cacheable(value="user", key="#id", condition="#id>0", unless="#result==null")
public User getUser(long id) { ... }

// 3. 编程式 SpEL 求值
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext ctx = new StandardEvaluationContext(user);
ctx.setVariable("prefix", "Mr.");
String result = parser.parseExpression(
    "#prefix + name + ':' + T(Math).PI").getValue(ctx, String.class);

特殊语法:#paramName 引用方法参数;#root 根对象;#this 当前引用;@beanName 引用 Bean;T(java.lang.Math) 引用静态类型;{1,2,3} 内联 List;{key:'val'} 内联 Map;.?[...] 过滤、.![...] 投影。

工程实践

典型应用:动态缓存 key 与条件。@Cacheable(key="#user.id + ':' + #user.tenantId") 组合多参数生成复合 key,避免跨租户缓存污染;condition="#user.vip" 仅缓存 VIP 用户,unless="#result==null" 不缓存空结果(防缓存穿透用布隆过滤器更佳,这里 SpEL 控制缓存策略)。

安全实践
  • 防 SpEL 注入:绝不要把用户输入直接拼入 SpEL 表达式字符串。若必须用用户数据,用 setVariable 作为变量传入,表达式本身保持静态。
  • 只读场景用 SimpleEvaluationContext:禁用 T() 类型引用与构造器调用,避免执行任意类方法。
  • 缓存 Expression 对象:SpelExpressionParser.parseExpression 返回的 Expression 可缓存复用,避免重复解析。Spring 内部对 @Value 表达式已缓存。
深度追问

追问 1:SpEL 的性能瓶颈在哪?如何优化?

瓶颈在反射——SpEL 默认用反射访问属性/调用方法,比直接调用慢 10-100 倍。优化需分层看待:@Value 中的 SpEL 在启动时求值一次,属于低频场景,无需编译优化;@ConditionalOnExpression 是 Boot 条件注解,在 BeanDefinition 注册阶段求值(每个匹配的 Bean 一次),也不是高频热点;@Cacheable key/condition 等运行时每次调用都求值的场景才是高频热点,需要缓存 Expression 并启用 SpelCompiler 编译为字节码(SpelParserConfiguration.setCompilerMode(IMMEDIATE))。注意 AOT/Native Image 限制:GraalVM Native Image 不支持运行时字节码生成,SpelCompiler 在 AOT 模式下不可用,SpEL 会降级为纯反射求值,且需为 SpEL 引用的类型注册反射元数据——这是 Spring 6 AOT 对 SpEL 表达式做预编译处理的原因。

追问 2:SpEL 注入漏洞如何发生与防范?

若把用户输入拼入 SpEL 表达式(如 parser.parseExpression(userInput)),攻击者可构造 T(Runtime).getRuntime().exec('rm -rf /') 执行任意命令。防范:① 表达式完全静态,用户数据通过 setVariable 作为变量传入,表达式内用 #userInput 引用;② 用 SimpleEvaluationContext.forReadOnlyDataBinding(),禁用类型引用与构造器,从上下文层面杜绝危险操作;③ 永不 new StandardEvaluationContext() 求值不可信表达式。

追问 3:@Value 中的 ${} 与 #{} 有何区别?

${...} 是属性占位符,由 PropertySourcesPlaceholderConfigurer 解析,从 Environment 属性源取值替换,只做字符串替换不做运算。#{...} 是 SpEL 表达式,由 SpelExpressionParser 求值,支持运算、方法调用、Bean 引用。两者可组合:@Value("#{${my.timeout} * 1000}")——先解析 ${} 占位符得到数字,再由 SpEL 求值乘以 1000。

常见陷阱
易错点
  • SpEL 注入漏洞:用户输入拼入表达式导致 RCE,必须用变量传入 + SimpleEvaluationContext。
  • ${} 与 #{} 混淆:前者属性占位符(字符串替换),后者 SpEL(表达式求值)。
  • 未缓存 Expression:高频求值场景每次重新 parseExpression,性能差。
  • @Cacheable key 漏写参数:key="#id" 漏掉多参数场景导致缓存冲突。
  • GraalVM 下 T() 反射失效:Native Image 需为 SpEL 引用的类型注册反射配置。
C05.11
事务提交后异步执行:afterCommit / 事务外层异步 / @TransactionalEventListener / 本地消息表 如何选型?高级P1
本质问题

这道题考察事务与异步的时序边界:当业务在数据库事务中需要"提交成功后再异步推送通知 / 发 MQ / 调外部",如何保证两件事原子——要么都成功,要么事务回滚时通知绝不发生。更深一层,它考察对 TransactionSynchronizationManager 事务同步钩子机制的理解,以及对"事务内直接开线程"经典反模式的识别能力。这是企业微信 / 钉钉通知、订单事件推送等场景的高频线上故障源。

演进脉络

反例(禁止):在 @Transactional 方法内直接 executor.execute(task)。异步线程可能早于 commit 执行(查不到刚写入的数据),且事务回滚后通知已发出,造成"数据库无记录、消息满天飞"的数据不一致。

Spring 事务同步(afterCommit):通过 TransactionSynchronizationManager.registerSynchronization 注册 afterCommit 钩子,仅当事务真正提交后才把任务丢进线程池;回滚则不触发。轻量、无中间件,是普通通知首选。

事务外层异步:把异步调用移到 @Transactional 方法之外,由调用方执行。时序正确,但强依赖异常处理——一旦 service 内部吞异常 / setRollbackOnly,事务回滚却仍发通知。

@TransactionalEventListener(AFTER_COMMIT):Spring 事件驱动的 afterCommit 封装,业务与推送解耦;无事务默认不触发(fallbackExecution=false)。

本地消息表 / 事务消息:当事务后消息不允许丢失时,afterCommit 仍会丢(宕机),需升级为"业务与消息同事务落库 + 定时任务 / MQ 补偿"的可靠消息最终一致方案(详见 C09.02 / C09.09)。

第一性原理

TransactionSynchronizationManager 把回调挂到当前线程的事务上下文上;事务提交成功后 Spring 自动回调 afterCommit(),此时才提交线程池。两个必须遵守的硬约束:

  • 自适应有无事务:用 TransactionSynchronizationManager.isActualTransactionActive() 判断;无事务(导入 / 批量)时无法注册钩子,直接提交异步任务,一套代码兼容两种场景。
  • 主线程 ThreadLocal 已销毁:afterCommit 执行时主业务方法栈已退出,原主线程 ThreadLocal / MDC 已被清空。所有需要的数据(用户 ID、租户 ID、业务 ID)必须通过 lambda 闭包传入,绝不在 task 内部再从上下文取;MDC 需手动 copy。
sequenceDiagram participant M as 主线程(事务内) participant TSM as TransactionSynchronizationManager participant EX as 线程池 participant DB as 数据库 M->>DB: 写业务数据(未提交) alt 有事务 isActualTransactionActive=true M->>TSM: registerSynchronization(afterCommit) M->>DB: commit 成功 TSM-->>EX: afterCommit 回调 执行 execute(task) else 无事务(导入/批量) M->>EX: 直接 execute(task) end Note over M,EX: 回滚时不触发 afterCommit 通知绝不发出
工程实践

封装通用工具类,全站一行调用,避免重复样板代码:

@Component
public class AsyncAfterCommitExecutor {
    @Autowired
    private ExecutorService executorService;

    /** 事务提交成功后异步执行;无事务则立即异步执行。task 内部禁止读取 ThreadLocal/MDC,所需数据须闭包传入 */
    public void executeAfterCommit(Runnable task) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            // Spring 6 / Boot 3+:TransactionSynchronizationAdapter 已移除,直接实现接口默认方法
            TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronization() {
                    @Override public void afterCommit() { safeRun(task); }
                });
        } else {
            safeRun(task);
        }
    }

    private void safeRun(Runnable task) {
        try {
            executorService.execute(task);   // task 异常只打日志,绝不抛出杀死线程池
        } catch (Exception e) {
            log.error("afterCommit async task failed", e);
        }
    }
}

// 业务调用:企业微信通知,参数闭包传入
asyncAfterCommitExecutor.executeAfterCommit(() ->
    wechatSender.sendToHeader(abnormalSeqList, classInfoId, orgId, submitterUserId));

四方案横向对比:

方案 可靠性 依赖 MQ 回滚误发 适用
事务内直接 execute 最差(禁止) 否 会 无
afterCommit + 线程池 尽力(宕机可丢) 否 不会 普通通知(企微/钉钉),允许极小概率丢失
事务外层异步 中 否 可能(吞异常坑) 调用点少、异常必上抛
@TransactionalEventListener 有坑 否 不会 已用事件驱动体系
本地消息表 / 事务消息 不丢 可选 不会 消息绝不可丢(跨系统事件/对账)
深度追问
  • 追问 1:为什么事务外层异步有"吞异常"坑?若 service 内部 catch 异常并 setRollbackOnly(或标记回滚),方法正常 return,外层照常 execute(task) → 通知发出、数据回滚,数据不一致。故外层异步要安全,必须保证异常一定向上抛、绝不内部吞掉。
  • 追问 2:afterCommit 阶段为什么拿不到 ThreadLocal?回调触发时主业务方法已出栈,主线程 ThreadLocal 被清理;线程池新线程更无继承关系。解决:闭包传参 + MDC 手动 copy(参考 C11.08 TraceId 透传)。
  • 追问 3:afterCommit 能 100% 保证消息送达吗?不能。commit 完成、正要执行 afterCommit 回调时服务宕机,任务未进线程池即丢失。它只解决"回滚误发",属尽力通知。
  • 追问 4:何时必须升级本地消息表?消息丢失会造成业务故障(跨系统事件、对账、业务回调)时,用本地消息表:业务与消息同事务落库,定时任务 / MQ 补偿,重启后可继续处理(详见 C09.02 / S02.02)。
常见陷阱
  • Spring 5 示例照抄 TransactionSynchronizationAdapter:该类在 Spring 6 已移除,Boot 3 基线须直接实现 TransactionSynchronization。
  • 事务内直接 executor.execute:时序错乱 + 回滚误发双重 bug,生产禁止。
  • task 内部读取 ThreadLocal / MDC:afterCommit 阶段主线程上下文已销毁,必得 null。
  • task 异常未捕获:抛出后可能杀死线程池,影响后续异步任务。
  • 事务外层异步但 service 内部吞异常:回滚仍发通知,最隐蔽的数据不一致。
  • 认为 afterCommit / 事务事件能保送达:宕机即丢,强可靠场景必须本地消息表。