面试知识库
极高 困难

循环依赖与三级缓存#

一句话答案#

Spring 三级缓存解决 setter 注入循环依赖:一级(成品 Bean)、二级(半成品)、三级(ObjectFactory,解决 AOP 代理)。

核心要点

三级缓存:

  • singletonObjects(一级):完整初始化的Bean
  • earlySingletonObjects(二级):提前曝光的半成品Bean
  • singletonFactories(三级):Bean的ObjectFactory(解决AOP代理)

流程: A创建→放三级缓存→注入B→B创建→注入A(从三级升到二级)→B完成→A完成

不能解决: 构造器注入的循环依赖

深挖:为什么必须三级,二级不够?#

三级缓存各存什么(注意存的内容本质不同):

缓存字段存什么何时放入
一级singletonObjects完整成品 Bean(已初始化、已是最终代理)Bean 全部创建完成后
二级earlySingletonObjects提前暴露的早期引用(可能是半成品原始对象,也可能是提前生成的代理)三级工厂被调用后,把产物升到这里
三级singletonFactoriesObjectFactory(lambda),内部回调 getEarlyBeanReference实例化后、属性注入前

核心问题:为什么不直接在二级缓存里放代理对象?

关键在于「代理本该在初始化之后才生成」。正常生命周期里,代理是 AbstractAutoProxyCreatorpostProcessAfterInitialization(初始化完成后)这一步才创建的。如果一个 Bean 根本没有循环依赖,它就该走正常流程、在初始化后才被代理——此时提前造代理是错误且浪费的。

三级缓存用 ObjectFactory 把「是否提前生成代理」变成被动触发:只有当这个 Bean 真的被卷入循环依赖、被别的 Bean 提前引用时,才会有人来调这个工厂的 getObject(),进而触发 getEarlyBeanReference 提前生成代理。没发生循环依赖,工厂永远不被调用,代理仍按正常时机在初始化后生成。二级缓存是「存对象」,三级缓存是「存生成对象的时机」——这就是二级不够、必须三级的根因。

怎么保证注入出去的代理和最终代理是同一个?

  • 三级工厂调用 getEarlyBeanReference 提前造代理后,会把这个代理记进 earlyProxyReferences(key 为 beanName)。
  • 该 Bean 自己走完初始化、进入 postProcessAfterInitialization 时,发现 earlyProxyReferences 里已有它,就直接返回那个已造好的代理,不再重复 wrap
  • 这样「提前注入给别人的代理」和「自己最终用的代理」是同一个对象,避免了双重代理或引用不一致。

为什么 @Async 的循环依赖仍然报错?

@Async 的代理不走 getEarlyBeanReference 那条提前暴露的链路,它的代理只在初始化后由 AsyncAnnotationBeanPostProcessor 生成。于是在循环依赖中,别人提前拿到的是未被 @Async 代理的原始早期引用,而该 Bean 自己初始化后又生成了一个新代理——两者不是同一个对象。Spring 检测到「提前暴露的引用」与「最终 Bean」不一致,直接抛 BeanCurrentlyInCreationException。这也是为什么普通 AOP(@Transactional)循环依赖能成而 @Async 不行的根本区别。

面试回答(2分钟版)

Spring通过三级缓存机制解决单例Bean的setter注入循环依赖问题。三级缓存分别是:singletonObjects一级缓存存放完整初始化的成品Bean,earlySingletonObjects二级缓存存放已实例化但未完成属性注入的半成品Bean,singletonFactories三级缓存存放Bean的ObjectFactory工厂。具体流程是这样的:假设A和B互相依赖,A创建实例后先把自己的ObjectFactory放入三级缓存,然后注入属性时发现需要B,于是去创建B。B创建过程中发现需要A,从三级缓存拿到A的ObjectFactory调用getObject获取A的早期引用,此时如果A需要AOP代理,ObjectFactory会提前生成代理对象放入二级缓存。B拿到A的引用后完成初始化,A再拿到完整的B完成自己的初始化。三级缓存的核心价值在于解决存在AOP代理时的循环依赖——如果只用两级缓存,无法在需要时提前创建代理对象。但三级缓存无法解决构造器注入的循环依赖,因为对象还没实例化根本无法放入任何缓存,这种情况需要用@Lazy注解注入代理对象来延迟初始化。

追问与易错

追问方向:

  • “为什么需要三级缓存?两级不行吗?”→ 两级缓存能解决普通循环依赖,但无法处理有 AOP 代理的情况;三级缓存存 ObjectFactory 可以延迟决定是否创建代理对象,保证注入的是正确的(代理/非代理)引用
  • “构造器注入的循环依赖能解决吗?”→ 不能,因为构造器注入在实例化阶段就需要依赖,此时对象还没创建完无法放入任何缓存;解决方案是改用 setter 注入或加 @Lazy 注解
  • “@Lazy 怎么解决循环依赖的?”→ @Lazy 不立即创建真实依赖,而是注入一个 CGLIB 代理对象作为占位符,延迟到第一次实际调用方法时才触发真实 Bean 的初始化和注入

易错点:

  • ❌ “三级缓存是为了解决循环依赖”——更准确说是为了解决「有 AOP 代理的循环依赖」
  • ❌ “所有循环依赖都能解决”——构造器注入和 prototype 作用域的循环依赖无法解决