AI 模拟面试实战:Spring 循环依赖终极拷问:三级缓存中 ObjectFactory 的提前曝光机制与 AOP 代理动态织入底层
在 Java 后端面试与 Spring 源码考核中,“Spring 是如何解决循环依赖(Circular Dependency)的?”堪称面试题库中登场频率最高、但也最容易被面试官连环追问至“源码细节崩溃”的经典大杀器。
很多初学者在背八股文时,通常能说出:
- “A 依赖 B,B 又依赖 A”;
- “Spring 使用了三级缓存:
singletonObjects、earlySingletonObjects、singletonFactories”。
但当大厂技术专家直视你的眼睛,深入追问底层的生命周期时序与设计哲学:
“如果仅仅是为了解决普通的循环依赖,二级缓存明明就已经足够了,为什么 Spring 架构师一定要引入第三级缓存
singletonFactories(即ObjectFactory<?>)?三级缓存到底为哪个核心特性做出了不可替代的妥协?如果一个 Bean 被@Async异步代理或@Transactional事务代理(动态 AOP)修饰,Spring 在哪个生命周期阶段生成代理对象?为什么 Spring 默认在初始化之后(postProcessAfterInitialization)才创建 AOP 代理,却被迫在发生循环依赖时通过SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference提前创建代理对象?为什么基于构造函数的循环依赖 Spring 默认无法解决?”
很多没有精读过 SpringDefaultSingletonBeanRegistry与AbstractAutowireCapableBeanFactory源码的同学就会在时序冲突上当场卡壳。
今天我们通过 AI 模拟面试官的硬核推演视角,把 Spring 三级缓存的生命周期时序、提前曝光与 AOP 代理织入机制彻底讲透。
Spring 三大单例缓存定义与职责速查
在DefaultSingletonBeanRegistry源码中,三大缓存的定义如下:
graph TD subgraph Spring 单例三级缓存架构 (DefaultSingletonBeanRegistry) C1["一级缓存: Map<String, Object> singletonObjects<br>【完整成品单例池】: 经历了实例化 -> 属性填充 -> 初始化 -> AOP 代理的最终可用 Bean"] C2["二级缓存: Map<String, Object> earlySingletonObjects<br>【半成品单例池】: 已经实例化、但尚未完成属性填充的早期暴露对象 (纯内存, 解决多次循环依赖重复创建代理问题)"] C3["三级缓存: Map<String, ObjectFactory<?>> singletonFactories<br>【单例工厂池】: 存储 ObjectFactory lambda 表达式, 用于在需要时【提前生成 AOP 动态代理对象】!"] end一、终极拷问:为什么“二级缓存”解决不了 AOP 代理的循环依赖?
这是面试中最关键、最能体现技术深度的核心问题!
假设场景:如果只有二级缓存(没有第三级工厂缓存)
设 Bean A 依赖 Bean B,Bean B 依赖 Bean A,且Bean A 是一个被@Transactional或@Aspect修饰的需要被 AOP 代理的类。
graph TD subgraph 只有二级缓存的严重架构缺陷 A1[1. Bean A 实例化 (得到原始裸对象 rawA)] --> A2[2. 直接将 rawA 放入二级缓存] A2 --> B1[3. Bean A 注入属性 B -> 触发 Bean B 实例化与属性填充] B1 --> B2[4. Bean B 从二级缓存中拿到原始对象 rawA 并完成注入] B2 --> B3[5. Bean B 初始化完成, 成为成品进入一级缓存!] B3 --> A3[6. 流程回到 Bean A -> Bean A 继续执行初始化后置处理器 (postProcessAfterInitialization)] A3 --> A4[🚨 此时 AOP 代理器介入, 根据 rawA 生成了一个全新的动态代理对象 proxyA!] A4 --> A5[7. 将 proxyA 放入一级缓存!] A5 --> Fail[🚨 致命灾难: Bean B 内部持有的是 rawA, 而最终暴露给全站使用的是 proxyA! 事务与切面拦截在 B 内部彻底失效! 严重破坏单例性!] end为什么不能一上来就直接在第 1 步无脑创建 AOP 代理?
有人会问:“那我为什么不能在 Bean A 一实例化完,就立马生成proxyA放进二级缓存?”
Spring 架构设计原则决不允许这么做!
Spring 的核心设计原则是:“Bean 应该在完整执行完所有的属性填充(DI)与初始化方法(@PostConstruct / init-method)之后,在生命周期的最后一步才生成 AOP 代理(符合开闭原则与后置处理规范)!”
只有在**“真的检测到发生了循环依赖”**这种万不得已的异常场景下,Spring 才被迫“提前介入创建代理”!
这就是引入第三级缓存singletonFactories(ObjectFactory延迟回调)的最高设计哲学!
二、三级缓存运作完整时序图(AOP 循环依赖完美自愈)
sequenceDiagram participant A as Bean A (声明了 @Transactional) participant B as Bean B participant L3 as 三级缓存 (singletonFactories) participant L2 as 二级缓存 (earlySingletonObjects) participant L1 as 一级缓存 (singletonObjects) Note over A: 1. A 实例化得到 rawA A->>L3: 2. 提前曝光工厂: put("A", () -> getEarlyBeanReference(rawA)) Note over A: 3. A 填充属性 B -> 触发 getBean("B") Note over B: 4. B 实例化得到 rawB B->>L3: 5. B 提前曝光工厂: put("B", ...) Note over B: 6. B 填充属性 A -> 触发 getBean("A") B->>L1: 查 L1 (未命中) B->>L2: 查 L2 (未命中) B->>L3: 查 L3 命中 A 的工厂! L3->>A: 7. 🔥 执行 A 的 ObjectFactory.getObject() -> 调用 getEarlyBeanReference() Note over A: 提前为 A 生成 AOP 动态代理对象 proxyA! A->>L2: 8. 将 proxyA 存入二级缓存 L2, 并从三级缓存 L3 中移除 A L2-->>B: 9. 将 proxyA 成功注入给 B! Note over B: 10. B 完成属性填充与初始化 B->>L1: 11. B 作为完整成品存入一级缓存 L1! Note over A: 12. 流程回到 A, A 成功拿到成品 B 完成注入 Note over A: 13. A 执行 postProcessAfterInitialization Note over A: 核心判断: 发现 proxyA 已经在早期提前创建过了, 此处直接返回原始 rawA 不再重复代理! A->>L2: 14. 从二级缓存 L2 取出提前生成的 proxyA A->>L1: 15. 将 proxyA 存入一级缓存 L1! (所有单例引用完美闭环一致!)三、三级缓存核心源码剖析(doCreateBean与getSingleton)
1. 三级缓存提前曝光工厂(AbstractAutowireCapableBeanFactory.doCreateBean):
// 步骤 1: 实例化后的提前曝光 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 核心:向三级缓存注册一个 ObjectFactory 匿名 lambda 表达式 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }2. 三级缓存三层检索逻辑(DefaultSingletonBeanRegistry.getSingleton):
protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 尝试从一级缓存(成品池)获取 Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 2. 尝试从二级缓存(半成品池)获取 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 3. 尝试从三级缓存(工厂池)获取 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 执行工厂回调:提前生成 AOP 代理或返回原始对象! singletonObject = singletonFactory.getObject(); // 升维放入二级缓存,防止后续其他依赖者重复触发 AOP 代理生成! this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }四、为什么构造函数注入的循环依赖无法被解决?
- 物理根因:
三级缓存提前曝光的前提是——Bean 的 Java 构造方法必须先执行完毕(完成物理内存分配new出来裸对象)! - 如果采用构造函数注入(
public A(B b)和public B(A a)):
在执行new A(b)的那一瞬间,必须先拿到b的实例;而为了new B(a)又必须先拿到a的实例。
双方在 Java 虚拟机层面连基本的裸对象内存空间都无法分配出来,三级缓存根本无从介入,只能抛出BeanCurrentlyInCreationException!
模拟面试复盘总结
回答 Spring 循环依赖,严守“三层递进”表达:
- 第一层(概念定性):清晰阐明一级缓存存成品、二级缓存存早期半成品、三级缓存存工厂回调;
- 第二层(核心矛盾):点出只有二级缓存无法在保证 AOP 正常生命周期的前提下解决循环依赖;
- 第三层(源码穿透):推演
getEarlyBeanReference提前织入 AOP 代理与存入二级缓存防重复代理的时序。
层次分明、因果严密,直击 Spring 框架设计哲学的灵魂深处。