从源码角度剖析:Spring如何用三级缓存解决循环依赖

学完本文你能解决什么问题? 彻底理解 Spring 循环依赖的底层原理,不仅能说出"三级缓存各存什么",更能从源码层面解释"为什么三级缓存必须用工厂而不是直接存对象""为什么构造器注入的循环依赖无法解决""AOP 场景下二级缓存为什么不够用"。建议配合 Spring 5.x 源码一起看。
前置要求 :你需要已理解 Spring IoC 容器和 Bean 生命周期的基础概念(BeanFactoryBeanPostProcessor、实例化 vs 初始化),知道 @Autowired@Service 的基本用法。如果你还分不清"实例化"和"初始化"的区别,建议先阅读 Spring Bean 生命周期相关文章再回来。


一、为什么这是一道"必考题"?

1.1 什么是循环依赖?

最常见的循环依赖场景:A 依赖 B,B 反过来依赖 A

java 复制代码
@Service
public class AService {
    @Autowired
    private BService bService;  // A 依赖 B
}

@Service
public class BService {
    @Autowired
    private AService aService;  // B 依赖 A
}

当 Spring 容器启动,创建 A 时发现需要 B,于是转去创建 B;B 创建时发现需要 A,又转回来创建 A------A → B → A → B... 如果不做任何处理,这就是一个永远解不开的死结,最终抛出 BeanCurrentlyInCreationException

1.2 先看结论:Spring 能解决哪些循环依赖?

场景 能否解决 原因
✅ 单例 Bean + Setter/Field 注入 三级缓存核心场景
✅ 构造器注入 + @Lazy 注解 代理延迟实例化
❌ 多例 Bean(prototype) 不能 无缓存机制
❌ 无 @Lazy 的构造器注入 不能 实例化时就需要依赖
❌ 原型 Bean 与单例 Bean 交叉循环依赖 不能 缓存机制不匹配

本文重点拆解的是第一行:单例 Bean + Setter/Field 注入的循环依赖是如何通过三级缓存解决的。

二、三级缓存在哪里?长什么样?

阅读目标:看完这一节,你能准确说出三级缓存的变量名、数据类型、各自存什么。

三级缓存全部定义在 DefaultSingletonBeanRegistry 类中,这是 Spring 解决循环依赖的"心脏":

java 复制代码
public class DefaultSingletonBeanRegistry {

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

    /** 二级缓存:存放早期暴露的 Bean(半成品,已实例化但未填充属性、未初始化) */
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

    /** 三级缓存:存放 ObjectFactory(能够产生 Bean 早期引用的工厂) */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

    /** 正在创建中的 Bean 集合,标记哪些 Bean 正在创建 */
    private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}

2.1 三级缓存逐层拆解

缓存层级 缓存名 存储内容 核心作用
一级缓存 singletonObjects 成品 Bean(已实例化 + 已注入 + 已初始化) 对外提供 Bean,全局唯一
二级缓存 earlySingletonObjects 早期暴露的 Bean 引用(半成品,已实例化但未填充属性) 保证循环依赖中多个引用指向同一个早期对象
三级缓存 singletonFactories ObjectFactory 工厂对象(一般是 Lambda 表达式) 延迟生成代理对象,在真正需要时才决定返回原始对象还是代理对象

2.2 重要提示

⚠️ "三级缓存"这个名称容易让人误解为"三层结构"。实际上,这三个缓存是平行存放在同一个类中的三个不同 Map。它们之间没有严格的层级关系,而是按生命周期状态区分存储。之所以叫"三级缓存",是因为它们共同构成了一个从"未成品"到"成品"的逐级晋升机制。

三、源码逐行追踪:A ↔ B 循环依赖完整流程

阅读目标:看完这一节,你能在脑子里画出 A 和 B 两个 Bean 的创建时序图,并指出每个时刻三个缓存里都有什么。

为了便于理解,我们用一个具体的循环依赖场景来追踪源码:

  • A 依赖 B
  • B 依赖 A
  • 都是单例,通过 @Autowired 字段注入

先记住一句话:Spring 处理循环依赖的核心思想是------允许部分初始化的 Bean 提前暴露引用。

3.1 第一步:创建 A

Spring 的 Bean 创建入口是 AbstractBeanFactory.doGetBean(),然后调用 createBean(),最终进入 AbstractAutowireCapableBeanFactory.doCreateBean()------这是三级缓存最核心的方法。

为了快速定位源码,一个小技巧:将断点打在 AbstractAutowireCapableBeanFactory.doCreateBean() 方法中,然后观察 DefaultSingletonBeanRegistry 里三个缓存的变化

java 复制代码
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
    // 阶段1: 实例化------通过反射创建对象(此时属性都是 null)
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // 阶段2: 将 ObjectFactory 放入三级缓存 ------ 循环依赖解决的关键!
    if (mbd.isSingleton()) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // 阶段3: 属性填充 ------ 可能触发循环依赖!
    populateBean(beanName, mbd, instanceWrapper);

    // 阶段4: 初始化 ------ 执行 AOP 增强等
    exposedObject = initializeBean(beanName, bean, mbd);

    return exposedObject;
}

在 A 的创建过程中:

  1. 实例化 AcreateBeanInstance 通过反射调用 A 的构造器,在堆内存中开辟空间,得到 A 的原始对象(此时 b 字段为 null)。
  2. 放入三级缓存addSingletonFactory 将 A 的 ObjectFactory 存入 singletonFactories。这个工厂是一个 Lambda 表达式,内部调用了 getEarlyBeanReference 方法。

此时三个缓存的状态:

缓存 A 的状态 B 的状态
一级 singletonObjects ❌ 无 ❌ 无
二级 earlySingletonObjects ❌ 无 ❌ 无
三级 singletonFactories ✅ 有 A 的工厂 ❌ 无

3.2 第二步:A 填充属性时发现需要 B

populateBean 方法负责属性注入。当 A 发现需要注入 B 时,容器中还没有 B,于是触发 B 的创建,进入 doGetBean(B)

3.3 第三步:创建 B

B 的创建流程与 A 完全相同:

  1. 实例化 B :得到 B 的原始对象(此时 a 字段为 null)。
  2. 放入三级缓存 :将 B 的 ObjectFactory 存入 singletonFactories
  3. 填充属性populateBean(B) 发现需要注入 A。

此时三个缓存的状态:

缓存 A 的状态 B 的状态
一级 singletonObjects ❌ 无 ❌ 无
二级 earlySingletonObjects ❌ 无 ❌ 无
三级 singletonFactories ✅ 有 A 的工厂 ✅ 有 B 的工厂

3.4 第四步:B 填充属性时发现需要 A ------ 命中三级缓存!

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<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();    // 调用工厂生成对象
                // 4. 存入二级缓存并移除三级缓存
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
    return singletonObject;
}

B 需要注入 A,调用 getSingleton("A", true)

💡 读到此处,请先猜测:B 在填充属性时需要的是 A,那么 B 自己放在三级缓存里的工厂,会在这一步被消费掉吗?写下你的判断,然后继续往下读验证。

  • 一级缓存:❌ 无
  • 二级缓存:❌ 无
  • 三级缓存:✅ 命中! ------ 拿到了 A 的 ObjectFactory
  • 调用 factory.getObject() → 执行 getEarlyBeanReference()(如果有 AOP,这里会生成 A 的代理对象)
  • 将生成的 A 对象(原始对象或代理对象)放入二级缓存 ,同时从三级缓存中删除 A 的工厂

此时三个缓存的状态(B 拿到 A 早期引用后):

缓存 A 的状态 B 的状态
一级 singletonObjects ❌ 无 ❌ 无
二级 earlySingletonObjects ✅ A 早期引用 ❌ 无
三级 singletonFactories ❌ 已删除 ✅ 有 B 的工厂

小测验 ① :此时二级缓存 earlySingletonObjects 里存的是什么?

  • A. B 的早期引用
  • B. A 的早期引用
  • C. 什么都没有

点击查看答案 答案是 B。A 的工厂在第三步被调用后,生成的早期引用(原始对象或代理对象)被放入了二级缓存。B 还没有被其他 Bean 需要过,所以 B 的早期引用不在二级缓存中。

3.5 第五步:B 完成初始化

B 拿到了 A 的早期引用,将这个引用注入到 B 的 a 字段 ,B 的属性填充完成。接着 B 执行 initializeBean 完成初始化(如果是需要 AOP 的 Bean,这里会生成最终代理对象),最后将完整的 B 放入一级缓存

此时三个缓存的状态(B 完成初始化后):

缓存 A 的状态 B 的状态
一级 singletonObjects ❌ 无 ✅ B 完整对象
二级 earlySingletonObjects ✅ A 早期引用 ❌ 无(B 从未进入二级缓存)
三级 singletonFactories ❌ 无 ❌ 无(B 的工厂已被消费?等等------仔细看:B 的工厂被消费了吗?)

⚠️ 这里有一个容易被忽视的细节 :在整个 B 的创建过程中,B 的 singletonFactories 从未被 getSingleton 方法消费过------因为 B 的属性填充需要的是 A,而不是 B 自身。只有当 B 自己作为依赖被其他 Bean 需要时,才会消费 B 的工厂。所以 B 的工厂会一直保留在三级缓存中,直到 B 完成初始化后,在 addSingleton 中被清理。

这是全文最容易产生误解的地方 :很多读者看到 B 完成初始化了,就以为 B 的工厂已经被消费了。实际上,三级缓存的工厂只有在"被其他 Bean 需要"时才会被触发 。B 自始至终没有被其他 Bean 反向依赖(只有 B 依赖 A,没有第三个 Bean 依赖 B),所以 B 的工厂从未被调用,而是静静躺在三级缓存里直到最后被清理掉。简单说:工厂是为"别人需要我"准备的,而不是为"我需要别人"准备的。

3.6 第六步:A 重新获得控制权,完成初始化

B 创建完成后,回到 A 的 populateBean 方法。现在 A 需要注入的 B 已经在容器中(一级缓存里有 B 的完整对象),直接获取并注入。A 的属性填充完成。

接着 A 执行 initializeBean 完成初始化。如果 A 需要 AOP,这里会生成 A 的最终代理对象。但这里有一个校验机制 :Spring 会检查 getSingleton 从三级缓存中暴露的早期引用和最终完成初始化的对象是否是同一个。如果不同(比如早期引用是原始对象,但最终初始化时产生了代理对象),Spring 会抛出异常。

最后,将完整的 A 放入一级缓存,并清理二级缓存中的 A 早期引用。

最终三个缓存的状态:

缓存 A 的状态 B 的状态
一级 singletonObjects ✅ A 完整对象 ✅ B 完整对象
二级 earlySingletonObjects ❌ 无 ❌ 无
三级 singletonFactories ❌ 无 ❌ 无

3.7 完整时序图

text 复制代码
时间轴 ──────────────────────────────────────────────────────────────────────►

  A 实例化 ──► A 放入三级缓存 ──► A 填充属性(需要B)
                                      │
                                      ▼
                               B 实例化 ──► B 放入三级缓存 ──► B 填充属性(需要A)
                                                               │
                                                               ▼
                                                        getSingleton(A)
                                                        从三级缓存获取 A 的工厂
                                                        生成 A 早期引用
                                                        移入二级缓存
                                                        删除三级缓存
                                                               │
                                                               ▼
                                                        A 早期引用注入 B
                                                        B 完成初始化
                                                        B 存入一级缓存
                                      │
                                      ▼
  A 拿到 B(从一级缓存)──► A 完成初始化 ──► A 存入一级缓存

四、为什么是三级缓存而不是二级?

阅读目标:看完这一节,你不仅知道"AOP 需要三级缓存",更能说出"二级缓存到底卡在哪一步"。

这是面试中区分"背答案"和"真理解"的经典问题。纯普通对象场景下,二级缓存确实够用。但加了 AOP 代理之后,问题就来了。

4.1 如果只有二级缓存:AOP 循环依赖会发生什么?

只有二级缓存的错误流程推演:

text 复制代码
步骤1:B 实例化,原始对象 B@123 → 存入二级缓存 earlySingletonObjects["b"] = B@123
步骤2:B 填充属性,发现需要 A
步骤3:A 实例化,原始对象 A@456 → 存入二级缓存 earlySingletonObjects["a"] = A@456
步骤4:A 填充属性,发现需要 B → 从二级缓存拿到 B@123 注入
步骤5:A 初始化完成 → 存入一级缓存
步骤6:B 继续初始化,执行 BeanPostProcessor
      此时才发现 B 需要 AOP 代理 → 生成代理对象 B$Proxy@789
      但问题来了:**A 里面持有的还是 B@123(原始对象),不是代理对象!**
结果:事务失效!AOP 功能失效!

核心矛盾 :对象在被注入时还不需要代理(因为 AOP 增强通常发生在初始化阶段),但当它真正需要代理时,依赖它的对象已经拿到了原始对象的引用,后期无法替换。这个悖论的根本在于:普通对象和代理对象在堆内存中是两个不同的对象,引用一旦传递出去就无法回头。

4.2 三级缓存如何解决这个问题?

三级缓存的关键设计:延迟代理对象的生成------把"生成代理"这个动作推迟到真正被需要的那一刻

三级缓存的核心代码:

java 复制代码
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    // 如果当前 Bean 需要 AOP 代理,这里会生成代理对象
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            exposedObject = ((SmartInstantiationAwareBeanPostProcessor) bp)
                    .getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

4.2.1 深入:Spring 如何判断返回代理还是原始对象?

阅读目标 :看完这一节,你会清楚知道 getEarlyBeanReference 内部到底走了什么判断逻辑,以及为什么代理只生成一次。

整个判断链路涉及两个关键角色:SmartInstantiationAwareBeanPostProcessor 接口和 earlyProxyReferences 缓存。

第一步:遍历所有 BeanPostProcessor,找到能处理早期引用的

getEarlyBeanReference 会遍历容器中所有 BeanPostProcessor,筛选出实现了 SmartInstantiationAwareBeanPostProcessor 接口的处理器。在 Spring AOP 中,AbstractAutoProxyCreator 就是这个接口的核心实现:

java 复制代码
// AbstractAutoProxyCreator.getEarlyBeanReference() 简化逻辑
public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    // 关键:将 bean 标记为"早期引用已处理"
    this.earlyProxyReferences.put(cacheKey, bean);
    // 然后走正常的代理创建逻辑
    return wrapIfNecessary(bean, beanName, cacheKey);
}

第二步:wrapIfNecessary 判断是否真的需要代理

这是决定"返回代理还是返回原始对象"的真正判据:

java 复制代码
// AbstractAutoProxyCreator.wrapIfNecessary() 核心判断
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 1. 如果已经处理过(比如已经创建过代理),直接返回原始对象
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    // 2. 如果缓存显示不该被代理,直接返回原始对象 ← 关键判断!
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    // 3. 如果是基础设施类(Advice、Pointcut、Advisor 等),不代理
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }
    // 4. 获取匹配的 Advisor(切面) ← 核心:有没有切面切到了这个类
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 有匹配的切面 → 创建代理对象
        Object proxy = createProxy(bean.getClass(), beanName,
            specificInterceptors, new SingletonTargetSource(bean));
        return proxy;
    }
    // 5. 没有匹配的切面 → 标记为不需要代理
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;  // 返回原始对象
}

判断逻辑总结成一张流程图

flowchart TD Start[getEarlyBeanReference 被调用] --> Step1{遍历 SmartInstantiationAware<br>BeanPostProcessor} Step1 --> Step2[AbstractAutoProxyCreator<br>getEarlyBeanReference] Step2 --> Mark[将 bean 标记到<br>earlyProxyReferences 缓存] Mark --> Wrap[调用 wrapIfNecessary] Wrap --> Check1{已标记为<br>targetSourcedBean?} Check1 -->|是| ReturnRaw1[返回原始对象] Check1 -->|否| Check2{advisedBeans 缓存<br>== false?} Check2 -->|是| ReturnRaw2[返回原始对象] Check2 -->|否| Check3{是基础设施类<br>或 shouldSkip?} Check3 -->|是| MarkFalse1[advisedBeans 标记 false] MarkFalse1 --> ReturnRaw3[返回原始对象] Check3 -->|否| Check4{有匹配的 Advisor<br>切面存在?} Check4 -->|无匹配| MarkFalse2[advisedBeans 标记 false] MarkFalse2 --> ReturnRaw4[返回原始对象] Check4 -->|有匹配| MarkTrue[advisedBeans 标记 true] MarkTrue --> CreateProxy[createProxy 创建代理对象] CreateProxy --> ReturnProxy[返回代理对象]

第三步:防止重复代理------earlyProxyReferences 的核心作用

这是最容易被忽略的设计:同一个 Bean 的早期引用只用三级缓存中的工厂生成一次 (存入二级缓存后工厂就被删除了),所以 getEarlyBeanReference 最多调用一次。但更关键的是后续的保护机制:

java 复制代码
// AbstractAutoProxyCreator.postProcessAfterInitialization() 简化逻辑
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 关键检查:这个 bean 的早期引用是否已经生成过代理?
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 没生成过 → 在这里正常创建代理
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
        // 生成过了 → 直接返回原始对象,不重复创建代理!
    }
    return bean;
}

这里暴露了一个精妙的衔接设计

时机 发生了什么 代理谁来生成
Bean 被其他 Bean 依赖时 触发三级缓存工厂 → getEarlyBeanReference wrapIfNecessary 创建代理(如有切面匹配)
Bean 的 initializeBean 阶段 postProcessAfterInitialization 被调用 检查 earlyProxyReferences,发现已处理 → 跳过,不重复生成
Bean 从未被其他 Bean 依赖 三级缓存工厂从未触发 postProcessAfterInitialization 中正常调用 wrapIfNecessary 创建代理

一句话总结核心机制 :Spring 通过 wrapIfNecessary 内部的 Advisor 匹配(检查是否有切面切到了这个类)来决定返回代理还是原始对象。通过 earlyProxyReferences + advisedBeans 两组缓存确保:无论早期引用和后续初始化哪个先生成代理,另一个都不会重复生成

flowchart TD A[addSingletonFactory<br>存入三级缓存] --> B{第一次被依赖方需要?} B -->|是| C[调用 getEarlyBeanReference<br>生成早期引用<br>可能是原始对象/代理对象] C --> D[早期引用存入二级缓存<br>删除三级缓存] D --> E[依赖方持有该引用] E --> F[后续所有地方使用<br>同一个对象] B -->|否| G[等待<br>直到被需要]

三层缓存各司其职

缓存 解决的核心问题
一级 singletonObjects 保证单例:全局唯一,避免重复创建
二级 earlySingletonObjects 保证对象一致性 :让多个依赖方拿到的是同一个早期引用
三级 singletonFactories 延迟代理生成:在真正被需要时才决定返回原始对象还是代理对象

小测验 ②:假设 A 需要 AOP 代理,B 不需要。在 A ↔ B 的循环依赖中,B 拿到的 A 的引用是什么?

  • A. 一定是 A 的原始对象
  • B. 一定是 A 的代理对象
  • C. 取决于 getEarlyBeanReference 的执行结果,可能是代理对象

点击查看答案 **答案是 C。**当 B 需要 A 时,会触发三级缓存中 A 的工厂 → 调用 `getEarlyBeanReference`。如果 A 需要 AOP,`SmartInstantiationAwareBeanPostProcessor` 会在这里生成代理对象并返回。所以 B 拿到的是代理对象,而非原始对象。这也正是三级缓存的设计精髓:**按需决定返回什么**。

4.3 追问:为什么不直接在实例化时判断是否需要代理并生成?

如果在实例化时就生成代理对象,会出现两个问题:

1)有些 Bean 可能始终没有被其他 Bean 依赖,提前生成代理浪费性能;

2)若 Bean 最终不需要 AOP(例如代理条件不满足),提前生成反而会破坏原始对象。三级缓存做到了按需生成、且只生成一次

核心思想总结:"工厂可以决定返回什么,但对象不能决定自己是谁。" 三级缓存存的是"决定权",二级缓存存的是"结果",一级缓存存的是"成品"。

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

阅读目标:看完这一节,你能够从 JVM 底层解释为什么构造器注入是"死局"。

回到 JVM 对象创建的根本机制。Java 对象在堆内存中创建分为两个阶段:

  1. 内存分配阶段 :在堆中开辟内存空间,所有成员变量赋默认值(基本类型 0 / false,引用类型 null)。此时对象存在但不完整
  2. 初始化阶段:执行构造方法、成员变量显式赋值、代码块,将对象填充到完整状态。

关键区别:

  • Setter/字段注入:先完成内存分配(对象已存在),再设置属性。两个阶段可以拆分,给 Spring 预留了"先暴露对象引用、回头再填充"的操作空间。
  • 构造器注入 :内存分配和构造方法调用是原子操作、不可拆分 。构造方法一旦执行,必须传入所有依赖参数才能生成对象。循环依赖双方都需要对方作为构造参数,于是陷入先有鸡还是先有蛋的死锁。

Spring 之所以无法解决构造器循环依赖,从根本上取决于 JVM 的这个硬性规则,而非 Spring 自身设计缺陷。

工程实践建议:优先使用 Setter 或字段注入,构造器注入仅用于必需的核心依赖。

小测验 ③:以下哪种循环依赖场景 Spring 完全无法自动解决?

  • A. A 和 B 都是单例,通过 @Autowired 字段注入
  • B. A 和 B 都是单例,通过构造器注入且都加了 @Lazy
  • C. A 和 B 都是 prototype,通过 @Autowired 字段注入

点击查看答案 **答案是 C。**prototype 作用域的 Bean 每次获取都会创建新实例,Spring 不会为 prototype > Bean 维护任何缓存,因此循环依赖完全无法解决。B 选项加了 `@Lazy` 后,构造器注入的循环> > 依赖是可以解决的------`@Lazy` 会让 Spring 注入一个代理对象,延迟真正的实例化。

六、易错场景与最佳实践

6.1 开发避坑清单

易错场景 现象 解决方案
构造器注入的循环依赖 BeanCurrentlyInCreationException 改用 Setter/字段注入,或加 @Lazy
@Async / @Transactional 在循环依赖中失效 增强逻辑未生效 确保 Bean 在循环依赖中返回的是代理对象(三级缓存已处理此场景,但需注意代理类中调自身方法会绕过代理)
多例 Bean 循环依赖 同样抛异常 改为单例,或使用 @Lazy + ApplicationContext 手动获取
循环依赖中 Bean 后置处理器重复执行 代理对象被包装多次 依赖 getEarlyBeanReference 保证单次代理

6.2 正误代码逐行对比:构造器注入循环依赖

以下是一个典型错误和修复方案的逐行对比:

java 复制代码
// ❌ 错误写法:构造器注入循环依赖 → 启动直接抛异常
@Service
public class OrderService {
    private final UserService userService;

    public OrderService(UserService userService) {       // 构造器注入
        this.userService = userService;                   // 创建 OrderService 需要 UserService
    }
}

@Service
public class UserService {
    private final OrderService orderService;

    public UserService(OrderService orderService) {      // 构造器注入
        this.orderService = orderService;                 // 创建 UserService 需要 OrderService
    }                                                     // 双方都需要对方 → 死锁!
}
java 复制代码
// ✅ 修复方案一:改用字段注入(Spring 三级缓存可自动解决)
@Service
public class OrderService {
    @Autowired
    private UserService userService;                     // 字段注入:对象先创建,引用后填充
}

@Service
public class UserService {
    @Autowired
    private OrderService orderService;                   // 三级缓存正常工作 ✓
}
java 复制代码
// ✅ 修复方案二:保留构造器注入,加 @Lazy 打破循环
@Service
public class OrderService {
    private final UserService userService;

    public OrderService(@Lazy UserService userService) {  // @Lazy 让 Spring 注入代理对象
        this.userService = userService;                    // 代理对象不要求目标 Bean 已就绪
    }                                                      // 首次真正调用时才触发目标 Bean 创建
}

@Service
public class UserService {
    private final OrderService orderService;

    public UserService(@Lazy OrderService orderService) {  // 同理
        this.orderService = orderService;
    }
}

核心差异 :字段注入时,对象已通过无参构造器在堆中分配内存,Spring 可以提前暴露引用。构造器注入时,构造方法调用和内存分配是原子的,没有"提前暴露"的时间窗口。@Lazy 则通过注入代理对象绕过了这个限制------代理对象不需要目标 Bean 已存在。

6.3 变式场景判断

给你 3 种变体场景,试着判断 Spring 能否解决,参考答案在下方:

场景 你的判断(能/否)
场景 1:A → B → C → A(三个单例 Bean 形成环形依赖,全部字段注入)
场景 2:A 是单例,B 是 prototype,A 依赖 B,B 依赖 A(均字段注入)
场景 3:A 和 B 字段注入,A 需要 AOP(@Transactional),B 不需要 AOP

点击查看答案

  • 场景 1。三级缓存机制对参与方数量不敏感,3 个 Bean 和 2 个 Bean 的解决流程完全相同------无非是多一轮"发现依赖→转去创建→命中缓存→返回"的过程。
  • 场景 2不能 。prototype Bean 不在三级缓存的覆盖范围内,Spring 不会为 prototype 维护 singletonFactories。启动时会抛出 BeanCreationException
  • 场景 3 。这正是三级缓存(相比二级缓存)的核心优势场景。B 需要 A 时,三级缓存调用 getEarlyBeanReference 生成 A 的代理对象并注入 B;后续 A 的 initializeBean 阶段不会再重复生成代理(getEarlyBeanReference 有标记机制),所以 A 和 B 拿到的都是正确的代理对象。

6.4 理解层面

最好的学习方式是亲自启动一个循环依赖项目,将断点打在 doCreateBean 方法中,观察三个缓存的变化。Spring 的设计哲学在这里体现得淋漓尽致:用极小的代码量,通过分级缓存和一个巧妙的工厂模式,优雅地解决了生死攸关的循环依赖问题。

助记口诀一存成品二存半,三存工厂按需干。构造注入是死局,AOP 代理靠工厂。

拆解:

  • 一存成品 :一级缓存 singletonObjects 存已完全初始化的 Bean
  • 二存半 :二级缓存 earlySingletonObjects 存"半成品"(已实例化、未初始化)
  • 三存工厂按需干 :三级缓存 singletonFactoriesObjectFactory,只在被依赖方需要时才触发
  • 构造注入是死局:JVM 对象创建两个阶段不可拆分,无法提前暴露引用
  • AOP 代理靠工厂getEarlyBeanReference 在工厂调用时按需决定返回代理还是原始对象

全文视觉摘要(思维导图)

mindmap root((Spring 三级缓存<br>解决循环依赖)) 三级缓存定义 singletonObjects<br>一级:成品 Bean earlySingletonObjects<br>二级:早期半成品引用 singletonFactories<br>三级:ObjectFactory 工厂 核心流程 A ↔ B A 实例化 → 三级缓存存 A 工厂 A 填充属性 → 触发 B 创建 B 实例化 → 三级缓存存 B 工厂 B 填充属性 → getSingleton A 命中三级缓存 → 调 getEarlyBeanReference A 早期引用 → 存入二级缓存 → 删三级 B 注入 A 早期引用 → B 初始化 → B 入一级 A 从一级取完整 B → 注入 → A 初始化 → A 入一级 为什么是三级不是二级 核心矛盾:AOP 代理时机 属性注入时对象还是原始对象 初始化时才触发 AOP 生成代理 如果只有二级缓存 → 依赖方拿到的是原始对象 → 代理失效 三级缓存解法 三级:存工厂 → 按需决定返回原始/代理 二级:存结果 → 保证多次获取是同一对象 一级:存成品 → 对外正式暴露 构造器注入为什么不行 JVM 对象创建两阶段 内存分配 → 属性默认值 初始化 → 执行构造方法 字段注入:两个阶段可拆分 → 可提前暴露 构造器注入:两阶段原子绑定 → 无法提前暴露 AOP 场景处理 getEarlyBeanReference 在工厂触发时调用 SmartInstantiationAwareBeanPostProcessor 生成代理 → 存入二级缓存 → 只生成一次

延伸挑战:尝试回答以下问题------

  1. 如果不使用 Spring,你会如何手动"打破"A 和 B 之间的循环依赖?
  2. 如果 A 和 B 的循环依赖中,A 需要 AOP 代理但 B 不需要,二级缓存能解决吗?
  3. 阅读 DefaultSingletonBeanRegistryregisterSingleton 方法的源码,思考它如何与三级缓存协作。
相关推荐
MetaLite2 小时前
Spring-MVC微服务接口怎么分层-网关Controller与Service边界
spring·微服务·mvc
大模型码小白2 小时前
Spring AI 框架中集成 MCP 的完整指南:从服务端到客户端的全流程实践
大数据·运维·数据库·人工智能·python·sql·spring
小江的记录本2 小时前
【ORM 框架】MyBatis-Plus 核心特性、条件构造器、分页插件、乐观锁插件(附《思维导图》、《问题排查与实践清单》和《面试高频考点汇总》)
java·windows·spring boot·python·spring·面试·mybatis
小江的记录本3 小时前
【ORM框架】MyBatis核心原理、ORM思想、MyBatis vs JPA
java·数据库·后端·spring·spring cloud·oracle·mybatis
卓怡学长14 小时前
w176基于SpringBoot的医院管理系统
java·spring boot·spring·maven·intellij-idea
大模型码小白15 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
吴声子夜歌15 小时前
Java面试——Spring原理及应用(一)
java·spring·面试
weixin_3993806917 小时前
TongWeb7.0.4.9M11一键安装脚本(by sy)
java·spring
叫我Paul就好21 小时前
Spring 为何没有在 Java之外的地方存在?
java·后端·spring