第 05 篇 · Spring 核心原理
本篇抓住 IoC + AOP 两条主轴:容器与 Bean 生命周期、三级缓存、代理/事务边界、扩展点与 SpEL/绑定,落到事务后异步时序。
骨架见 M12.05。本篇只补领域细枝,框在 核心方法论 · 开篇。
Spring 主线是「容器生命周期与 BeanDefinition → 依赖/循环依赖与代理时机 → 横切(AOP/事务/事件)→ 扩展点与表达式/绑定 → 事务提交后副作用」,用边界解释「注解为何失效」。
- IoC 启动与 Bean 生命周期:refresh 标准化步骤 + BeanDefinition→实例→初始化→销毁;图纸元数据是排障切面。
- 三级缓存与循环依赖:early 暴露与 ObjectFactory 保证「代理只创建一次」;构造器/prototype/Boot 2.6+ 默认收紧解释失败边界。
- AOP 代理与事务失效:JDK/CGLIB 选择、自调用、访问级别、异常类型决定 @Transactional 是否生效;传播与挂起/savepoint 要精确。
- 事件与设计模式:发布/监听/广播三角色,事务绑定事件;框架机制映射到工厂、代理、模板、观察者等模式。
- 扩展点与容器层次:BFPP 改图纸、BPP 改实例;BeanFactory 与 ApplicationContext 能力差决定企业级默认形态。
- 绑定、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 必须以事务结果为门闩,时序反了必出脏通知。
这道题考察对 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 启动的核心模板方法,必须能说出关键步骤的顺序与职责:
其中 第 5 步(BFPP)和 第 11 步(实例化)是两个最核心阶段:前者决定"有哪些 Bean、怎么造",后者决定"造出来并装配好"。ConfigurationClassPostProcessor 就在第 5 步解析 @Configuration/@ComponentScan,把扫描到的类注册成 BeanDefinition。
单个 Bean 生命周期(核心 5 大阶段):
关键源码链路: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 证据→修复与回归。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题是 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 代理)为例的完整时序:
内含 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) 放入一级缓存
核心源码: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 标记和代码审查跟踪,最终通过重构消除。
这道题考察 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,最后才调用目标方法。
切面执行顺序由 @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。
这道题考察三层:① 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 大场景与根因:
- 自调用(同类方法互调):不经过代理对象,事务拦截器不触发。解决:
@Autowired self或AopContext.currentProxy()。 - 方法非 public:
@Transactional默认只对 public 方法生效(Spring 5.1 起可通过@EnableTransactionManagement(publicMethodsOnly=false)扩展到 protected/package 方法,但不推荐——丧失 AOP 代理语义的一致性)。 - 异常被 catch 吞掉:异常未抛出到
TransactionInterceptor,拦截器认为正常返回而提交。解决:catch 后 rethrow,或手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。 - 异常类型不匹配:抛出受检异常但未配
rollbackFor,默认不回滚。解决:@Transactional(rollbackFor = Exception.class)。 - 类非 Spring Bean:
new出来的对象不被 IoC 管理,无代理。解决:交给容器管理。 - 数据库引擎不支持事务:MySQL MyISAM 无事务,需 InnoDB。
-
传播行为配错:内层
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 证据→修复与回归。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察 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:广播器,容器中只能有一个(
applicationEventMulticasterbean)。publishEvent实际委托给它。默认实现SimpleApplicationEventMulticaster。 - ApplicationListener:监听器,注册到 Multicaster,按事件类型(
ResolvableType)匹配。
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
@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。
这道题不是简单的"模式背诵",而是考察候选人能否将框架机制与设计模式对应,理解"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 内置实现 | 自研中间件对应场景 |
|---|---|---|
| 工厂模式 | 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 织入;这里用了责任链,是为了拦截器链执行"。设计自研中间件时,用工厂 + 代理模式做声明式入口,用责任链 + 策略模式做规则引擎,用观察者模式做事件通知,用模板方法模式固化流程骨架——这就是"模式在框架中的真实价值"。
这道题是 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 扩展点完整时序(一张图理解所有扩展点何时触发):
(动态注册 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
关键扩展点职责:
- 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 会跳过整个实例化,容易导致属性未注入。
这道题考察 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 的实现。
容器接口层次:
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
核心区别对比:
| 维度 | 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 同时继承两者。
这道题考察 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
等特殊类型解析。
核心接口关系:
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
数据绑定流程(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不支持。
这道题考察 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 可缓存复用,避免重复解析。
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
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 引用的类型注册反射配置。
这道题考察事务与异步的时序边界:当业务在数据库事务中需要"提交成功后再异步推送通知 / 发 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。
封装通用工具类,全站一行调用,避免重复样板代码:
@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 / 事务事件能保送达:宕机即丢,强可靠场景必须本地消息表。