Skip to content

Spring 循环依赖三级缓存源码级解析

问题

Spring 如何解决构造器注入和 setter 注入的循环依赖?三级缓存各存什么?

循环依赖是什么

循环依赖指 Bean A 依赖 Bean B,Bean B 又依赖 Bean A(或更复杂的链路 A → B → C → A)。如果不做特殊处理,容器在创建 A 时发现需要 B,去创建 B 时发现需要 A,形成死锁。

Spring 并非对所有循环依赖都能解决,它只解决 setter 注入(字段注入也算 setter 注入的一种) 的循环依赖,构造器注入的循环依赖会直接报错

三级缓存的定义

三级缓存定义在 DefaultSingletonBeanRegistry 中,三个 ConcurrentHashMap:

java
// 一级缓存:完全初始化好的单例 bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:提前暴露的早期 bean(半成品,已完成实例化但未完成属性注入和初始化)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

// 三级缓存:ObjectFactory 工厂,用于生成早期 bean 的代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

为什么需要三级缓存

要理解三级缓存的设计,先看一个循环依赖场景:

java
@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

只能用一级缓存 —— 不行

如果只有一级缓存(成品库存),A 创建到一半时发现需要 B,但 B 还没创建,死锁。A 不可能把自己"半成品"放进去,因为一级缓存只装成品。

只用两级缓存 —— 勉强可行,但有性能问题

如果只有一级 + 二级缓存,流程会是:

  1. A 实例化,放入二级缓存(半成品)
  2. A 注入属性,发现需要 B
  3. B 实例化,放入二级缓存
  4. B 注入属性,从二级缓存拿到 A 的早期引用
  5. B 完成初始化,放入一级缓存
  6. A 从二级缓存移到一级缓存

看起来可以?问题出在 AOP 代理上。假设 A 需要被 AOP 增强(比如加了 @Transactional),那么:

  • 在步骤 2 中,A 的早期引用(从二级缓存拿到的)还没有被代理
  • 当 B 把 A 的原始引用存入自己的字段后,后续 A 在步骤 6 生成了 AOP 代理对象
  • 最终 B 持有的是 A 的原始对象,而不是 AOP 代理对象 —— 事务/缓存等增强全部失效

如果去掉二级缓存,每次直接从二级缓存拿"代理后的对象",那所有 bean 即使没有循环依赖,也要提前生成代理对象,浪费性能。

三级缓存的设计 —— 延迟代理

三级缓存通过 ObjectFactory 延迟代理的创建,核心方法:

java
// 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) {
            // 3. 查三级缓存,执行 ObjectFactory 生成早期引用
            synchronized (this.singletonObjects) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    // 放入二级缓存,删除三级缓存,避免重复创建
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

三级缓存的 ObjectFactory 在哪里放进去的?在 doCreateBean() 方法中:

java
// AbstractAutowireCapableBeanFactory.doCreateBean()
// 实例化后,在属性注入之前,判断是否允许提前暴露
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences
        && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

getEarlyBeanReference 是关键入口——它会调用所有 SmartInstantiationAwareBeanPostProcessorgetEarlyBeanReference 方法。Spring AOP 的代理对象就是在这里生成的

java
// AbstractAutoProxyCreator.getEarlyBeanReference()
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
    // 如果有循环依赖,提前创建代理对象
    if (this.earlyProxyReferences.add(beanName)) {
        return wrapIfNecessary(bean, beanName, null);
    }
    return bean;
}

完整流程演示

用 A → B → A 的循环依赖走一遍完整流程:

1. createBean(A) 开始
   → A 实例化(反射,分配内存)
   → addSingletonFactory(A, factory)  // A 放入三级缓存
   → populateBean(A) 开始注入属性
     → 发现需要 B
     → getBean(B)

2. createBean(B) 开始
   → B 实例化
   → addSingletonFactory(B, factory)  // B 放入三级缓存
   → populateBean(B) 开始注入属性
     → 发现需要 A
     → getSingleton(A)  // 尝试获取 A
       → 一级缓存没有 A(A 还没完成初始化)
       → 二级缓存没有 A
       → 三级缓存有 A 的 ObjectFactory
       → 执行 factory.getObject() → getEarlyBeanReference(A)
         → 如果 A 需要 AOP,生成 AOP 代理
       → 得到 A 的早期引用(可能是代理对象)
       → 放入二级缓存,删除三级缓存
     → 将 A 的早期引用注入到 B 的 a 字段
   → B 完成属性注入
   → B 执行初始化回调(@PostConstruct, InitializingBean 等)
   → addSingleton(B)  // B 放入一级缓存,删除二三级缓存

3. 回到 A 的 populateBean
   → 注入 B 属性(从一级缓存拿到 B 的完整实例)
   → A 完成属性注入
   → A 执行初始化回调
   → addSingleton(A)  // A 放入一级缓存,删除二三级缓存

构造器注入为什么不行

java
@Component
public class A {
    public A(B b) {}  // 构造器注入
}

@Component
public class B {
    public B(A a) {}  // 构造器注入
}

构造器注入在 createBeanInstance() 阶段(实例化)就卡住了。此时:

  • 三级缓存还没建立(addSingletonFactory 在实例化之后才执行)
  • Spring 在实例化 A 时发现需要 B,去创建 B;B 实例化时发现需要 A,再次尝试创建 A
  • 进入死循环,直到 singletonsCurrentlyInCreation 集合检测到重复创建,抛出 BeanCurrentlyInCreationException

源码位置:DefaultSingletonBeanRegistry.beforeSingletonCreation() 中检查 singletonsCurrentlyInCreation 是否已包含当前 beanName,有则抛异常。

三级缓存常见面试追问

为什么三级缓存不用两级?用两级配合 AOP 提前代理不行吗?

两级缓存方案:一级放成品,二级放"代理后的半成品"。所有 bean 实例化后都默认生成代理对象并放入二级缓存。

问题在于:大多数 bean 没有循环依赖,也不需要 AOP。如果全部提前生成代理,每个 bean 实例化后都要走一遍 wrapIfNecessary,对于几千个 bean 的大项目,这完全是浪费。Spring 通过三级缓存 + ObjectFactory 实现了按需代理——只有真正发生循环依赖时,才会触发 getEarlyBeanReference 生成代理。

@Async 为什么会导致循环依赖报错?

@Async 通过 AsyncAnnotationBeanPostProcessor 生成代理,但这个 PostProcessor 的 getEarlyBeanReference 方法没有实现(只实现了 postProcessAfterInitialization)。所以当循环依赖发生时,getEarlyBeanReference 返回的是原始对象,而不是代理对象。后续在 postProcessAfterInitialization 中会再次生成代理,但 B 已经持有了 A 的原始引用,导致 @Async 不生效。

@Async 的循环依赖直接报错是 Spring 的主动防御机制——与其让用户拿到一个失效的代理,不如直接报错。

解法有哪些?

最推荐:重构代码,消除循环依赖。循环依赖往往是设计上耦合过重的信号,常见的重构手法:

  • 将 A 依赖 B 的字段移到方法参数中,通过方法调用传递
  • 引入中间层(如 EventPublisher),用事件驱动解耦
  • 使用 @Lazy 延迟加载——@Lazy 会生成一个懒加载代理,真正用到时才去容器中获取
java
@Component
public class A {
    @Lazy
    @Autowired
    private B b;  // 通过懒加载代理解决循环依赖
}

@Lazy 的原理是生成一个 JDK/CGLIB 代理,当真正调用 B 的方法时,代理才去容器中 getBean。这样 A 的创建不会被 B 阻塞,B 创建时 A 已经完成初始化和代理。

总结

Spring 三级缓存的设计是性能与正确性的平衡

缓存内容用途
一级 singletonObjects完全初始化好的 bean正常获取 bean
二级 earlySingletonObjects提前暴露的早期 bean循环依赖时获取半成品
三级 singletonFactoriesObjectFactory 工厂延迟创建 AOP 代理,避免无谓的代理生成

理解三级缓存不能只背"三个 map",关键是理解每个 map 的职责边界为什么需要这个边界。一级管成品,二级管早期引用缓存,三级管延迟代理——少一级功能不完整,多一级纯属浪费。

参考资料:Spring 源码 — DefaultSingletonBeanRegistry.getSingleton()AbstractAutowireCapableBeanFactory.doCreateBean()AbstractAutoProxyCreator.getEarlyBeanReference()

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。