在大型 Spring 项目中,循环依赖是一个几乎无法避免的挑战。当 Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A 时,如果没有特殊处理,就会形成“先有鸡还是先有蛋”的死锁问题,最终导致 BeanCurrentlyInCreationException 异常 。

那么,Spring 是如何优雅地破解这个难题的呢?答案就在于其设计的 三级缓存机制。

什么是循环依赖?

循环依赖是指两个或多个 Bean 相互持有对方的引用,形成一个闭环。例如:

@Component
public class BeanA {
    @Autowired
    private BeanB beanB; // A 依赖 B
}
@Component
public class BeanB {
    @Autowired
    private BeanA beanA; // B 依赖 A
}

循环依赖问题在Spring中主要有三种情况

1、通过构造器方法进行依赖注入时产生的循环依赖问题

2、通过setter方法进行依赖注入且是在多列模式下产生的循环依赖问题

3、通过setter方法或字段进行依赖注入且是在单例模式下产生的循环依赖问题

只有第三种方式的循环依赖问题被Spring解决了,在单例作用域下,Spring 可以解决 Setter 注入或字段注入(@Autowired)带来的循环依赖问题。

破解之道:三级缓存

Spring 解决循环依赖的核心是三个 Map,它们在不同的阶段存放着不同状态的 Bean 对象 。

缓存级别

缓存名称

存储内容

作用

一级缓存

singletonObjects

完全初始化好的、可用的成品 Bean

存放最终的单例 Bean,供外部直接使用

二级缓存

earlySingletonObjects

提前暴露的半成品 Bean的原始对象引用或早期代理对象引用

存放已完成实例化但未完成属性填充的原始对象,专门用于解决循环依赖

三级缓存

singletonFactories

ObjectFactory 对象工厂

存放生成 Bean 早期引用的工厂对象,用于延迟生成代理对象,这是解决循环依赖和AOP代理协作的关键

核心流程揭秘

我们以 Bean A 和 Bean B 相互依赖为例,来梳理整个流程 :

  1. 实例化 A:Spring 开始创建 Bean A,通过反射调用构造方法实例化出 A 的原始对象。

  2. 提前暴露 A:A 实例化后,Spring 会将它包装成一个 ObjectFactory,并放入三级缓存 singletonFactories 中。此时,A 已经可以被外界获取了,虽然它还是个“半成品”。

  3. 填充 A 的属性:开始对 A 进行属性填充,发现它依赖 Bean B。

  4. 实例化 B:Spring 转而去创建 Bean B,同样实例化出 B 的原始对象。

  5. 提前暴露 B:将 B 的 ObjectFactory 放入三级缓存。

  6. 填充 B 的属性:开始对 B 进行属性填充,发现它依赖 Bean A。

  7. 打破循环:B 去获取 A 的实例。此时,它会先从一级缓存找,发现没有;再从二级缓存找,也没有;最后从三级缓存中找到了 A 的 ObjectFactory。通过调用 getObject() 方法,B 拿到了 A 的早期引用(如果 A 需要 AOP 代理,这里返回的就是代理对象)。

  8. 缓存升级:拿到 A 的早期引用后,Spring 会将其放入二级缓存 earlySingletonObjects 中,并从三级缓存中移除 A 的工厂对象。这样,A 的“半成品”引用就被“升级”到了二级缓存。

  9. B 完成初始化:B 顺利注入 A 的引用后,完成自己的初始化,成为一个完整的 Bean,并被放入一级缓存 singletonObjects。

  10. A 完成初始化:B 创建完成后,A 继续执行,顺利注入 B 的完整引用,随后完成自己的初始化,也放入一级缓存。

至此,循环依赖被完美解决。

为什么需要三级缓存?二级不够吗?

这可能是关于循环依赖最经典的面试题了。

先说结论:用二级缓存理论上可以解决循环依赖,但无法在保证性能的前提下,同时支持 AOP 代理对象的“延迟生成”。三级缓存存在的核心目的,是为了将代理对象的创建时机从“Bean 实例化后立即生成”推迟到“真正发生循环依赖时再生成”。

先回顾:二级缓存能做什么?

假设我们只有两级缓存:

  • 一级缓存(成品 Map):存放完全初始化好的 Bean

  • 二级缓存(半成品 Map):存放实例化完成、但尚未填充属性/初始化的原始对象

流程是这样的:

  1. A 实例化,放入二级缓存(半成品)

  2. A 填充属性,发现依赖 B

  3. B 实例化,放入二级缓存

  4. B 填充属性,发现依赖 A

  5. B 从二级缓存拿到 A 的半成品引用,注入成功

  6. B 完成初始化,移入一级缓存

  7. A 继续完成初始化,移入一级缓存

结论:如果只处理普通 Java 对象(没有 AOP 代理),二级缓存完全够用。

那三级缓存到底解决了什么?

问题出在 AOP 代理对象上。

Spring 中很多 Bean 最终需要被代理(比如 @Transactional、@Async 或自定义切面)。代理对象不是原始对象,而是通过 CGLIB 或 JDK Proxy 生成的一个增强过的子类/实现类。

关键矛盾来了:

  • 代理对象的生成,通常发生在 Bean 初始化完成之后(因为需要调用后置处理器 BeanPostProcessor)

  • 但如果循环依赖发生时,B 需要在 A 还没完成初始化的情况下拿到 A 的引用,这个引用必须是最终的代理对象(否则 B 里存的原始对象,A 后面生成的代理对象就失效了)

如果只用二级缓存,怎么办?

那 Spring 就只能提前生成代理对象——即在 A 实例化完成后、放入二级缓存之前,就立即执行 AOP 后置处理器,生成代理对象再放进去。

这带来的问题是:

无论最终是否发生循环依赖,所有 Bean 在实例化后都要先生成代理对象。

这是巨大的性能浪费。绝大多数 Bean 并不会被循环依赖,但为了那少数情况,让所有 Bean 都提前走一遍 AOP 代理生成逻辑,显然不优雅。

三级缓存如何做到“懒加载”?

三级缓存的设计是:

缓存存什么
一级 singletonObjects最终成品 Bean
二级 earlySingletonObjects提前暴露的代理/原始对象(真正的半成品引用)
三级 singletonFactoriesObjectFactory(一个函数式接口,能按需生成代理对象)

流程关键点:

  1. A 实例化后,不立即生成代理,而是把一个 ObjectFactory 放入三级缓存。

  2. B 发现依赖 A 时,从三级缓存拿到这个工厂,调用 getObject()。

  3. 只有在此时,getObject() 内部才会去执行 AbstractAutoProxyCreator 的后置处理器,判断 A 是否需要代理,生成代理对象。

  4. 生成的代理对象被放入二级缓存,并从三级缓存移除。

  5. 后续再有人获取 A 的早期引用,直接从二级缓存拿,不会重复生成代理。

这样一来:

  • 不发生循环依赖的 Bean:全程不会走三级缓存的 getObject(),代理对象照常在其生命周期末尾生成,性能不受影响。

  • 发生循环依赖的 Bean:只在需要提前暴露时,才“按需”生成代理对象。

总结

  • 三级缓存机制:通过 singletonObjects、earlySingletonObjects 和 singletonFactories 三个 Map,在对象的不同生命周期阶段进行存储和转移。

  • 提前暴露:核心思想是在 Bean 实例化后立即暴露其引用(即使尚未完成属性注入),打破循环等待。

  • AOP 支持:三级缓存通过 ObjectFactory 实现了代理对象的延迟创建,只有在循环依赖发生时才会生成代理,保证了框架的优雅和性能。

理解 Spring 的三级缓存机制,不仅能帮你应对面试,更能让你在日常开发中更加深刻地理解框架的运行原理,避免因循环依赖导致的诡异问题。

更多推荐