Lesson 57 · Spring 生态核心原理

Spring 循环依赖:三级缓存解决方案

深度·🔥 极高·#Spring·#核心·#面试高频

第 1 站

面试官:Spring 是怎么解决循环依赖的?

"A 依赖 B,B 依赖 A,Spring 为什么没报 StackOverflow?三级缓存每一级分别存什么?为什么两级不够?构造器注入为什么解决不了?" —— 这道题区分度极高,能一口气讲清楚的人不超过 10%。

循环依赖(Circular Dependency):两个或多个 Bean 之间形成闭环的依赖关系。

循环依赖:A → B → A ServiceA @Autowired ServiceB ServiceB @Autowired ServiceA 依赖 依赖 三种形式:构造器注入 / Setter 注入 / 字段注入(@Autowired)
图 1 最简单的循环依赖:A 和 B 互相注入

循环依赖有三种注入方式,Spring 能解决的只有后两种:

注入方式Spring 能否解决原因
构造器注入❌ 不能对象还没 new 出来,无法放入缓存
Setter 注入✅ 三级缓存对象已实例化,可以提前暴露引用
字段注入(@Autowired)✅ 三级缓存同 Setter,对象已实例化
第 2 站

为什么循环依赖会导致问题?

Spring 创建 Bean 的标准流程是 实例化 → 属性填充 → 初始化。循环依赖会让这个流程陷入死循环:

死循环推演 · 无缓存时的创建过程
// Spring 尝试创建 A
1. new ServiceA()  // 实例化 A(空对象)
2. 填充 A 的属性 → 发现需要 ServiceB
3.   new ServiceB()  // 实例化 B
4.   填充 B 的属性 → 发现需要 ServiceA
5.     new ServiceA()  // 又去创建 A → 回到步骤 1 → 无限循环!

问题的根源:创建 A 需要 B,创建 B 又需要 A,形成递归。如果不做任何处理,Spring 容器启动时就会 StackOverflowError。

破解思路:能不能在 A 还没完全初始化时,先把 A 的"半成品"引用给 B?等 B 创建完了,A 再完成剩余初始化?—— 这就是"提前暴露引用"的核心思想。

Spring 的解决方案:三级缓存(Three-level Cache)。通过提前暴露"半成品"Bean 的引用,打断循环依赖的递归链。

第 3 站

三级缓存逐步拆解

Spring 在 DefaultSingletonBeanRegistry 中定义了三个 Map:

DefaultSingletonBeanRegistry.java · Spring 5.x
public class DefaultSingletonBeanRegistry {

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

    /** 二级缓存:存放提前暴露的半成品 Bean(已实例化,未填充属性) */
    private final Map<String, Object> earlySingletonObjects
            = new ConcurrentHashMap<>(16);

    /** 三级缓存:存放 Bean 工厂对象(ObjectFactory),用于生成早期引用 */
    private final Map<String, ObjectFactory<?>> singletonFactories
            = new HashMap<>(16);
}
Spring 三级缓存 一级缓存 singletonObjects 完全初始化的成品 Bean 可直接使用 二级缓存 earlySingletonObjects 提前暴露的半成品 Bean 已实例化,未填充属性 三级缓存 singletonFactories ObjectFactory<?> 工厂 用于生成代理对象 Bean 的"升级"路径 ① 实例化后 → 放入三级缓存 ② 被引用时 → 升级到二级缓存 ③ 初始化完成 → 升级到一级缓存 getSingleton() 查找顺序 一级缓存 → 二级缓存 → 三级缓存(调用工厂后升级到二级) 找到即返回,不再继续往下查
图 2 三级缓存结构与 Bean 升级路径

下面用 A → B → A 的循环依赖场景,逐步推演三级缓存的工作过程:

完整推演 · A 依赖 B,B 依赖 A
========== 第一步:创建 A ==========

1. 实例化 A:new ServiceA()  → 得到空对象 A'
2. 将 A' 包装成 ObjectFactory,放入 三级缓存:
   singletonFactories.put("serviceA", () -> getEarlyBeanReference(A'))

========== 第二步:填充 A 的属性 ==========

3. 发现 A 需要 ServiceB → 去获取 B
4. 实例化 B:new ServiceB()  → 得到空对象 B'
5. 将 B' 包装成 ObjectFactory,放入 三级缓存

========== 第三步:填充 B 的属性(关键!) ==========

6. 发现 B 需要 ServiceA → 去获取 A
7. getSingleton("serviceA"):
   一级缓存?没有
   二级缓存?没有
   三级缓存?有!调用 ObjectFactory.getObject()
   → 如果需要 AOP,这里返回代理对象;否则返回 A'
   → 将结果放入 二级缓存,从三级缓存移除
8. B 拿到了 A 的早期引用(A' 或代理),完成属性填充和初始化
9. B 初始化完成 → 放入 一级缓存

========== 第四步:A 完成创建 ==========

10. A 拿到完整的 B → 完成属性填充
11. A 初始化完成 → 放入 一级缓存
12. 清理二级缓存中 A 的早期引用
核心要点
三级缓存的查找是"降级"的:一级 → 二级 → 三级。三级缓存命中后会调用 ObjectFactory 生成对象,并将结果升级到二级缓存,同时从三级缓存移除。

下面是 getSingleton() 的源码,展示三级缓存的查找逻辑:

DefaultSingletonBeanRegistry.java · getSingleton 三级查找
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // ① 先查一级缓存:成品 Bean
    Object singletonObject = this.singletonObjects.get(beanName);

    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // ② 查二级缓存:提前暴露的半成品
        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) {
                        // ③ 查三级缓存:调用工厂生成对象
                        ObjectFactory<?> factory =
                                this.singletonFactories.get(beanName);
                        if (factory != null) {
                            // 关键:这里会触发 AOP 代理创建
                            singletonObject = factory.getObject();
                            // 升级到二级缓存
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            // 从三级缓存移除
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

注意同步块和双重检查的设计:三级缓存的工厂只会调用一次,生成结果后立即升级到二级缓存,后续查找直接从二级缓存返回。

第 4 站

为什么两级缓存不够?—— AOP 代理的难题

"一级缓存存成品、二级缓存存半成品,不就够了吗?为什么还要三级缓存?" —— 这是面试中最常见的追问,答案只有一个词:AOP

如果没有 AOP,两级缓存确实够用。问题出在:当 Bean 需要被 AOP 代理时,其他 Bean 注入的应该是代理对象,而不是原始对象

问题场景 · 如果只有两级缓存
// 假设 ServiceA 被 @Transactional 标注,需要生成代理

// 只有二级缓存的情况:
1. 实例化 A → A'(原始对象)
2. 放入二级缓存:earlySingletonObjects.put("serviceA", A')
3. 创建 B,B 需要 A → 从二级缓存拿到 A'(原始对象)
4. A 初始化时生成代理 → ProxyA
5. 问题!B 持有的是 A',而不是 ProxyA → 事务失效!

三级缓存的精妙之处在于:三级缓存存的不是 Bean 本身,而是一个 ObjectFactory(工厂)。这个工厂在 getEarlyBeanReference() 中判断是否需要创建代理:

AbstractAutowireCapableBeanFactory.java · getEarlyBeanReference
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object actualBean = bean;
    if (bean != null) {
        // 检查缓存中是否已有早期引用
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.containsKey(cacheKey)) {
            // 关键!遍历所有 SmartInstantiationAwareBeanPostProcessor
            // 如果 Bean 需要 AOP 代理,这里会返回代理对象
            actualBean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
        }
    }
    return actualBean; // 可能是原始对象,也可能是代理对象
}
三级缓存如何解决 AOP 代理问题 ① 实例化 A' 放入三级缓存(工厂) ② B 需要 A 调用工厂.getObject() ③ 工厂判断 需要 AOP? 是 → 返回 ProxyA ④ 升级到二级缓存 ProxyA 存入二级 ⑤ B 拿到 ProxyA B 持有的是代理对象,AOP 生效! 关键:代理对象的创建被"延迟"到了真正需要的时候
图 3 三级缓存的 ObjectFactory 延迟创建代理对象
为什么需要三级
三级缓存的本质是将"是否创建代理"的判断延迟到真正需要早期引用的时刻。如果没有循环依赖,Bean 正常走完初始化流程,在初始化后阶段创建代理;如果发生循环依赖,三级缓存的工厂会提前触发代理创建,确保其他 Bean 拿到的是代理对象而非原始对象。
第 5 站

构造器注入为什么无法解决循环依赖?

"为什么构造器注入的循环依赖 Spring 解决不了?Setter 和字段注入就可以?" —— 理解这一点,需要回到 Bean 的创建时间线。

三级缓存的前提是:Bean 必须先完成实例化(new 出来),才能将引用放入缓存

构造器注入的死结
@Service
public class ServiceA {
    private final ServiceB b;
    public ServiceA(ServiceB b) { this.b = b; } // 构造器注入
}

@Service
public class ServiceB {
    private final ServiceA a;
    public ServiceB(ServiceA a) { this.a = a; } // 构造器注入
}

// 创建过程:
1. 要 new ServiceA → 需要 ServiceB 作为参数
2. 要 new ServiceB → 需要 ServiceA 作为参数
3. 要 new ServiceA → ...  → BeanCurrentlyInCreationException!

根本原因:构造器注入要求在 new 对象的时候就拿到依赖,此时对象还不存在,无法放入任何缓存

Bean 生命周期与注入时机 构造器注入: 实例化(需要B!) 对象还没创建,无法放入缓存 Setter/字段注入: 实例化 ✓ 放入三级缓存 ✓ 属性填充 初始化 构造器注入:实例化和注入是同一个动作 → 无法"提前暴露"
图 4 构造器注入在实例化阶段就需要依赖,无法提前暴露
破解构造器循环依赖
可以使用 @Lazy 注解:Spring 不会立即注入真实对象,而是注入一个代理,等到第一次调用方法时才去获取真实 Bean。
@Lazy 打破构造器循环
@Service
public class ServiceA {
    private final ServiceB b;
    public ServiceA(@Lazy ServiceB b) {
        this.b = b; // 注入的是代理,不会触发 B 的创建
    }
}
第 6 站

Spring Boot 2.6+ 默认禁止循环依赖

从 Spring Boot 2.6 开始,循环依赖默认被禁止,应用启动时会直接报错:

启动报错 · Spring Boot 2.6+
***************************
APPLICATION FAILED TO START
***************************

The dependencies of some of the beans in the application context
form a cycle:

   serviceA
      ↓
   serviceB
      ↓
   serviceA
// BeanCurrentlyInCreationException

Spring 团队认为循环依赖是设计缺陷的信号,不应被默默容忍。禁止循环依赖可以:

  • 迫使开发者重新审视类职责划分
  • 避免隐藏的初始化顺序问题
  • 提高代码可维护性和可测试性
application.yml · 如果确实需要临时允许
spring:
  main:
    allow-circular-references: true  # 不推荐,仅作为迁移过渡
最佳实践
不要依赖 allow-circular-references=true 来掩盖问题。循环依赖几乎总是意味着类职责耦合,应该通过重构来消除。
第 7 站

替代方案与面试总结

消除循环依赖的常用手段:

方案做法适用场景
提取公共类将 A、B 共同依赖的部分抽到 CA、B 共享数据或逻辑
事件驱动A 发布事件,B 监听,解耦直接依赖通知型依赖(如创建后回调)
@Lazy延迟注入代理对象必须用构造器注入的遗留代码
Setter 注入 + @Autowired(required=false)将构造器注入改为 Setter依赖非必须的场景
ApplicationContext.getBean()运行时动态获取极端情况,不推荐
重构:提取公共类消除循环依赖 Before: A ⇄ B A B After: A → C ← B A B C C 承载 A、B 的共同逻辑,A 和 B 不再直接互相依赖
图 5 通过提取公共类消除循环依赖
面试回答模板
  • 是什么:循环依赖指 Bean 之间形成闭环依赖,Spring 通过三级缓存解决 Setter / 字段注入的循环依赖
  • 三级缓存:一级 singletonObjects(成品)→ 二级 earlySingletonObjects(半成品)→ 三级 singletonFactories(工厂)
  • 为什么三级:二级缓存无法处理 AOP 代理场景。三级缓存通过 ObjectFactory 延迟创建代理对象,确保其他 Bean 拿到的是代理而非原始对象
  • 构造器注入:不行,因为构造器注入时对象还未实例化,无法放入缓存。可用 @Lazy 缓解
  • Spring Boot 2.6+:默认禁止循环依赖,鼓励通过重构消除设计缺陷