【大白话说Java面试题 第155题】【06_Spring篇】第15题:Spring 如何解决循环依赖问题?

📌 PDF :大白话说Java面试题 --- 06_Spring篇

第15题:Spring 如何解决循环依赖问题?

📚 回答:

  • 核心考点 : Spring 循环依赖是面试中最经典、最深入的问题之一,大厂面试不会只问"三级缓存是什么",而是深入考察 三级缓存的源码级结构singletonObjects/earlySingletonObjects/singletonFactories 的字段定义与类型)、循环依赖的完整解决流程 (从 getBean()createBean()populateBean() 的源码链路)、为什么需要三级缓存而非二级 (AOP 代理对象的提前暴露问题)、构造器循环依赖为什么无法解决ConstructorResolver 的创建时机)、以及 @Lazy 延迟注入和 ObjectFactory 的替代方案。面试官真正想判断的是:你是否能从源码层面理解 Spring 解决循环依赖的设计精妙之处,以及能否在生产中识别和解决循环依赖引发的启动失败、代理失效等问题。

1. 什么是循环依赖?

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

复制代码
A → B → A(两两循环)
A → B → C → A(三角循环)

Spring 中的循环依赖类型

循环依赖类型 示例 Spring 是否支持 原因
构造器循环依赖 A(B b)B(A a) 不支持 构造器注入时 Bean 尚未实例化,无法提前暴露
Setter/字段循环依赖 @Autowired B b@Autowired A a 支持 实例化后可提前暴露半成品,再注入依赖
多例(prototype)循环依赖 @Scope("prototype") 不支持 原型 Bean 不缓存,每次创建都是新实例
@DependsOn 循环依赖 @DependsOn("b")@DependsOn("a") 不支持 @DependsOn 强制顺序,循环则报错

2. 三级缓存的源码级结构

Spring 的三级缓存在 DefaultSingletonBeanRegistry 中定义:

java 复制代码
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    // 一级缓存:成品单例池(完全初始化好的 Bean)
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    // 二级缓存:提前暴露的对象(半成品,用于解决循环依赖)
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

    // 三级缓存:单例工厂(用于创建 Bean 或代理对象)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

    // 正在创建中的 Bean 名称集合(用于检测循环依赖)
    private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}
缓存 字段名 类型 存放内容 作用
一级缓存 singletonObjects ConcurrentHashMap<String, Object> 完全初始化好的 Bean(成品) 对外提供 Bean 实例
二级缓存 earlySingletonObjects ConcurrentHashMap<String, Object> 已实例化但未初始化的 Bean(半成品) 解决循环依赖,避免重复创建代理
三级缓存 singletonFactories HashMap<String, ObjectFactory<?>> ObjectFactory(可生成 Bean 或代理) 延迟创建代理对象,支持 AOP

3. 三级缓存解决循环依赖的完整流程

以经典的 A → B → A 字段注入循环依赖为例,完整梳理源码链路:

Step 1:获取 A,开始创建

java 复制代码
// AbstractBeanFactory.doGetBean()
protected <T> T doGetBean(String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly) {
    // 1. 从一级缓存获取
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
        // 2. 标记 A 正在创建
        beforeSingletonCreation(beanName);  // 加入 singletonsCurrentlyInCreation
        // 3. 创建 Bean
        singletonObject = createBean(beanName, mbd, args);
    }
}

Step 2:实例化 A,放入三级缓存

java 复制代码
// AbstractAutowireCapableBeanFactory.doCreateBean()
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. 实例化 A(调用构造方法,此时 A 中 B 为 null)
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // 2. 【关键】如果是单例、允许循环依赖、正在创建中,放入三级缓存
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> {
            // 如果有 AOP 代理,提前创建代理;否则返回原始对象
            return getEarlyBeanReference(beanName, mbd, bean);
        });
    }

    // 3. 属性填充(populateBean)------ 发现需要注入 B
    populateBean(beanName, mbd, instanceWrapper);
    // 4. 初始化(initializeBean)
    exposedObject = initializeBean(beanName, exposedObject, mbd);
}

Step 3:创建 B,发现需要 A

java 复制代码
// 在 populateBean 中,发现 A 需要注入 B
// 调用 getBean("b") → doGetBean("b")
// B 的创建流程与 A 相同...
// B 实例化后,populateBean 发现需要注入 A
// 调用 getBean("a") → doGetBean("a")

Step 4:从三级缓存获取 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
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                // 4. 调用 getObject() 获取 Bean(或代理对象)
                singletonObject = singletonFactory.getObject();
                // 5. 提升到二级缓存
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
    return singletonObject;
}

Step 5:B 完成初始化,A 完成初始化

java 复制代码
// B 获取到 A 的半成品后,完成 populateBean 和 initializeBean
// B 放入一级缓存
addSingleton("b", b);

// 回到 A 的创建流程,B 已可用,A 完成 populateBean 和 initializeBean
// A 放入一级缓存
addSingleton("a", a);

// addSingleton 方法:
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);      // 放入一级缓存
        this.singletonFactories.remove(beanName);                   // 移除三级缓存
        this.earlySingletonObjects.remove(beanName);                // 移除二级缓存
    }
}

完整流程图

复制代码
getBean("a")
    ↓
doGetBean("a")
    ↓
getSingleton("a") → 一级缓存无,开始创建
    ↓
createBean("a") → doCreateBean("a")
    ↓
实例化 A(构造方法)→ A 对象创建,B=null
    ↓
addSingletonFactory("a", ObjectFactory) → 放入三级缓存
    ↓
populateBean("a") → 发现需要注入 B
    ↓
getBean("b")
        ↓
    doGetBean("b")
        ↓
    getSingleton("b") → 一级缓存无,开始创建
        ↓
    createBean("b") → doCreateBean("b")
        ↓
    实例化 B(构造方法)→ B 对象创建,A=null
        ↓
    addSingletonFactory("b", ObjectFactory) → 放入三级缓存
        ↓
    populateBean("b") → 发现需要注入 A
        ↓
    getBean("a")
            ↓
        getSingleton("a", true)
            ↓
        一级缓存无 → A 正在创建中
            ↓
        earlySingletonObjects 无
            ↓
        singletonFactories 获取 ObjectFactory
            ↓
        getObject() → 返回 A 的半成品(或代理)
            ↓
        放入二级缓存,移除三级缓存
            ↓
        返回 A 的半成品
        ↓
    B 完成 populateBean(A 已注入)
        ↓
    initializeBean("b") → B 完成初始化
        ↓
    addSingleton("b") → 放入一级缓存
        ↓
    返回 B
    ↓
A 完成 populateBean(B 已注入)
    ↓
initializeBean("a") → A 完成初始化
    ↓
addSingleton("a") → 放入一级缓存
    ↓
返回 A

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

这是面试中最核心的问题,需要从 AOP 代理 的角度理解:

  • 4.1 如果只有两级缓存的问题

    假设只有一级缓存(成品)和二级缓存(半成品):

    1. A 实例化后,如果需要 AOP 代理,必须立即创建代理对象放入二级缓存;
    2. 但 AOP 代理的创建时机是在 初始化后postProcessAfterInitialization),此时 A 尚未完成依赖注入和初始化;
    3. 如果在实例化后立即创建代理,后续的 @PostConstructInitializingBean 等初始化逻辑会在代理对象上执行,可能导致代理逻辑被覆盖或注入失败。
  • 4.2 三级缓存的精妙设计

    三级缓存存放的是 ObjectFactory(函数式接口),而非直接的 Bean 实例:

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

    延迟创建代理 :只有当循环依赖发生、需要从三级缓存获取 Bean 时,才调用 ObjectFactory.getObject()。此时:

    1. 如果 A 需要 AOP 代理,getEarlyBeanReference() 会提前创建代理对象;
    2. 如果 A 不需要代理,直接返回原始对象;
    3. 代理对象创建后提升到二级缓存,避免重复创建。

    为什么提升到二级缓存?

    如果循环依赖中有多个 Bean 依赖 A(如 B → AC → A),第一次从三级缓存获取 A 时创建代理并放入二级缓存,后续再获取 A 时直接从二级缓存拿,避免重复创建代理。

  • 4.3 三级 vs 二级缓存的本质区别

    维度 二级缓存方案 三级缓存方案
    存储内容 直接存放 Bean 实例 存放 ObjectFactory
    代理创建时机 实例化后立即创建 按需延迟创建(发生循环依赖时)
    初始化顺序 代理对象先创建,初始化逻辑后执行 原始对象先初始化,代理按需创建
    灵活性 低,所有 Bean 都提前创建代理 高,只有循环依赖的 Bean 才提前创建代理
    性能 差,所有 Bean 都提前创建代理 好,按需创建代理

5. 构造器循环依赖为什么无法解决?

构造器注入的循环依赖无法通过三级缓存解决,根本原因在于创建时机的差异

java 复制代码
@Service
public class A {
    private final B b;
    public A(B b) { this.b = b; }  // 构造器注入
}

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

问题分析

阶段 Setter/字段注入 构造器注入
实例化 无参构造创建对象 有参构造创建对象,参数必须已存在
提前暴露 实例化后即可暴露 构造器执行时就需要 B,B 尚未创建
循环依赖解决 ✅ 可以 ❌ 无法提前暴露

具体流程

复制代码
创建 A → 调用构造方法 A(B b) → 需要 B
    ↓
创建 B → 调用构造方法 B(A a) → 需要 A
    ↓
A 尚未实例化完成,无法从三级缓存获取 → 死循环

解决方案

方案 实现方式 说明
改用 Setter/字段注入 final 和构造器去掉,改用 @Autowired 最简单,但牺牲不可变性
@Lazy 延迟注入 @Lazy @Autowired private B b; 注入代理对象,延迟真正创建
ObjectFactory 延迟获取 @Autowired private ObjectFactory<B> bFactory; 使用时才获取
@Lookup 方法 @Lookup protected abstract B getB(); Spring 动态生成子类实现
重构代码 消除循环依赖 最佳方案,但成本最高

@Lazy 解决构造器循环依赖的原理

java 复制代码
@Service
public class A {
    private final B b;

    public A(@Lazy B b) {  // 注入的是 B 的代理对象
        this.b = b;
    }
}

Spring 为 B 创建 CGLIB 代理对象,A 的构造器接收的是代理而非真实 B。代理对象可以立即创建,不需要 B 的真实实例。当 A 初始化完成后,B 再创建并注入到 A 中。


6. 多例(prototype)循环依赖

原型 Bean 不缓存,每次 getBean() 都创建新实例,因此无法通过三级缓存解决循环依赖:

java 复制代码
@Scope("prototype")
@Component
public class A {
    @Autowired
    private B b;
}

@Scope("prototype")
@Component
public class B {
    @Autowired
    private A a;
}

报错BeanCurrentlyInCreationException

解决方案 :使用 ObjectFactory@Lookup 延迟获取。


7. 生产环境避坑指南
  • 7.1 启动时报 BeanCurrentlyInCreationException

    检查是否是构造器循环依赖或多例循环依赖。查看异常堆栈中的 Bean 名称,定位循环链。

  • 7.2 @Async 与循环依赖

    @Async 代理的创建可能导致循环依赖检测失败。确保 @EnableAsync@EnableAspectJAutoProxy 之后。

  • 7.3 AOP 代理与循环依赖的初始化顺序

    循环依赖中提前创建的代理对象,其 TargetSource 指向的是半成品 Bean。初始化完成后,代理对象的目标引用需要更新为完整的 Bean。

  • 7.4 使用 @DependsOn 导致循环

    @DependsOn("b") 强制在创建 A 之前先创建 B,如果 B 也 @DependsOn("a"),会直接报错,无法通过三级缓存解决。

  • 7.5 循环依赖是设计问题

    虽然 Spring 能解决 Setter 循环依赖,但循环依赖通常意味着职责划分不清。最佳实践是通过重构消除循环依赖。


8. 面试官追问与高分回答模板
  • 追问 1:"Spring 是如何解决循环依赖的?"

    低分回答:"通过三级缓存,在对象尚未完全初始化时提前暴露引用。"(没有触及源码和 AOP 代理)

    高分回答

    "Spring 通过三级缓存解决单例 Bean 的 Setter/字段注入循环依赖:

    缓存 字段 内容 作用
    一级缓存 singletonObjects 成品 Bean 对外提供
    二级缓存 earlySingletonObjects 半成品 Bean 解决循环依赖,避免重复创建代理
    三级缓存 singletonFactories ObjectFactory 延迟创建代理对象

    完整流程:

    1. A 实例化后(构造方法执行完,尚未注入依赖),将 ObjectFactory 放入三级缓存;
    2. A 属性填充时发现需要 B,开始创建 B;
    3. B 实例化后放入三级缓存,属性填充时发现需要 A;
    4. 从三级缓存获取 A 的 ObjectFactory,调用 getObject() 获取 A 的半成品(或代理对象),提升到二级缓存;
    5. B 完成初始化,放入一级缓存;
    6. A 继续初始化,放入一级缓存。

    关键是三级缓存存放 ObjectFactory 而非直接实例,实现了代理对象的延迟创建------只有发生循环依赖时才提前创建代理,避免所有 Bean 都提前代理的性能损耗。"

  • 追问 2:"为什么需要三级缓存?二级缓存不够吗?"

    高分回答

    "二级缓存不够,核心原因是 AOP 代理的创建时机问题

    如果只有两级缓存(成品 + 半成品),A 实例化后必须立即创建代理对象放入二级缓存。但 AOP 代理的正常创建时机是在初始化后postProcessAfterInitialization),此时 @PostConstructInitializingBean 等初始化逻辑尚未执行。如果提前创建代理:

    1. 初始化逻辑会在代理对象上执行,可能覆盖代理逻辑;
    2. 所有 Bean 都提前创建代理,性能损耗大。

    三级缓存存放 ObjectFactory,实现了按需延迟创建 :只有当发生循环依赖、需要从缓存获取 Bean 时,才调用 getObject() 创建代理。这样既保证了循环依赖时能获取到代理对象,又避免了不必要的提前代理。

    二级缓存(earlySingletonObjects)的作用是避免重复创建代理:如果多个 Bean 循环依赖 A,第一次从三级缓存创建代理后放入二级缓存,后续直接从二级缓存获取。"

  • 追问 3:"构造器循环依赖为什么无法解决?"

    高分回答

    "构造器循环依赖无法解决的根本原因是实例化和依赖注入的时机重叠

    Setter/字段注入的流程是:先无参构造实例化 → 提前暴露到三级缓存 → 再注入依赖。此时 Bean 已经存在,可以被其他 Bean 引用。

    构造器注入的流程是:调用构造方法时就需要传入依赖对象。创建 A 的构造方法 A(B b) 时,B 尚未创建;创建 B 的构造方法 B(A a) 时,A 尚未实例化完成。两者互相等待,形成死锁。

    解决方案:

    1. 改用 Setter/字段注入(最简单,但牺牲不可变性);
    2. @Lazy 延迟注入public A(@Lazy B b),注入 B 的代理对象,延迟真正创建;
    3. ObjectFactory 延迟获取 :使用时才 getObject()
    4. 重构代码,消除循环依赖(最佳方案)。"
  • 追问 4:"@Lazy 是怎么解决循环依赖的?"

    高分回答

    "@Lazy 解决循环依赖的原理是延迟注入代理对象

    以构造器循环依赖 A(B)B(A) 为例:

    java 复制代码
    public A(@Lazy B b) { this.b = b; }
    1. Spring 创建 A 时,发现构造器参数 B 标注了 @Lazy
    2. Spring 不为 B 创建真实实例,而是创建 B 的 CGLIB 代理对象
    3. A 的构造器接收代理对象,A 实例化完成并提前暴露到三级缓存;
    4. 创建 B 时,从三级缓存获取 A 的半成品注入;
    5. B 初始化完成;
    6. A 初始化完成,代理对象中的目标引用更新为完整的 B。

    关键点:代理对象可以立即创建,不需要依赖的真实实例。@Lazy 本质上是用"空间换时间",用代理对象打破循环等待。"

  • 追问 5:"Spring Boot 中怎么检测和避免循环依赖?"

    高分回答

    "Spring Boot 2.6+ 默认禁止循环依赖spring.main.allow-circular-references=false),启动时检测到循环依赖会直接报错。

    检测方法:

    1. 查看启动异常 BeanCurrentlyInCreationException,堆栈会显示循环链;
    2. 使用 IDE 插件(如 IntelliJ 的 Spring 依赖分析)可视化 Bean 依赖图;
    3. 代码审查:检查 @Autowired 和构造器注入的依赖关系。

    避免策略:

    1. 分层架构:Controller → Service → DAO,同层不依赖,避免循环;
    2. 事件驱动 :使用 ApplicationEventPublisher 解耦依赖;
    3. 抽象公共逻辑:将双向依赖的公共部分抽取到第三个 Bean;
    4. 接口隔离:通过接口降低耦合,但注意接口之间也可能循环。"
  • 追问 6:"如果 A 需要 AOP 代理,循环依赖中提前暴露的是代理对象还是原始对象?"

    高分回答

    "循环依赖中提前暴露的是代理对象(如果 A 需要 AOP 增强)。

    具体流程:

    1. A 实例化后,三级缓存中放入 ObjectFactory
    2. B 创建时需要 A,从三级缓存调用 ObjectFactory.getObject()
    3. getObject() 内部调用 getEarlyBeanReference(beanName, mbd, bean)
    4. getEarlyBeanReference() 遍历 SmartInstantiationAwareBeanPostProcessorAbstractAutoProxyCreator 判断 A 需要代理,提前创建代理对象;
    5. 代理对象返回给 B,B 注入的是 A 的代理;
    6. A 初始化完成后,代理对象的 TargetSource 指向完整的 A。

    这意味着:B 中注入的 A 从一开始就是代理对象,而非先注入原始对象再替换。这是三级缓存设计的关键------确保循环依赖中注入的是一致的代理对象。"


9. 方案选型速查表
循环依赖场景 是否支持 解决方案 推荐度
单例 + Setter/字段注入 三级缓存自动解决 ⭐⭐⭐⭐⭐
单例 + 构造器注入 改用 Setter、@Lazy、ObjectFactory、重构 ⭐⭐⭐⭐
单例 + @DependsOn 消除 @DependsOn 循环 ⭐⭐⭐⭐⭐
原型(prototype) ObjectFactory、@Lookup、重构 ⭐⭐⭐
多例 + 构造器 重构消除循环 ⭐⭐⭐⭐⭐
AOP 代理 + 循环依赖 三级缓存自动处理代理提前暴露 ⭐⭐⭐⭐⭐

💡 面试官想要的满分总结

Spring 解决循环依赖的核心设计是三级缓存 + 提前暴露 ,但这只适用于单例 + Setter/字段注入的场景。理解三级缓存必须抓住三个关键点:

  1. 三级缓存存放 ObjectFactory 而非实例 :实现了 AOP 代理的延迟创建 。只有发生循环依赖时才提前创建代理,避免所有 Bean 都提前代理的性能损耗。二级缓存(earlySingletonObjects)则避免重复创建代理。
  2. 构造器循环依赖无法解决 :因为构造器执行时 Bean 尚未实例化,无法提前暴露。解决方案包括 @Lazy(注入代理对象)、ObjectFactory(延迟获取)、或重构消除循环。
  3. 循环依赖是设计问题:虽然 Spring 能解决 Setter 循环依赖,但通常意味着职责划分不清。Spring Boot 2.6+ 默认禁止循环依赖,倒逼更好的设计。

生产中最容易踩的坑是构造器注入的循环依赖启动失败忘记 @Lazy 导致的问题。记住:三级缓存是 Spring 的"兜底"机制,不是鼓励循环依赖的"许可证"。优秀的架构应该通过分层设计和事件驱动消除循环依赖,而非依赖框架的容错能力。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
唐青枫2 小时前
Java Maven 实战指南:从 POM、依赖管理到多模块构建
java
硕风和炜2 小时前
【LeetCode: 1301. 最大得分的路径数目 + DP】
java·算法·leetcode·动态规划·dp·记忆化搜索
SamDeepThinking2 小时前
面向对象真正管理的,不是对象,而是依赖
java·后端·程序员
剑挑星河月3 小时前
94.二叉树的中序遍历
java·算法·leetcode
2301_801184753 小时前
docker 工作
java·docker·容器
老前端的功夫3 小时前
【Java从入门到入土】47:构建工具:Maven与Gradle的战争
java·python·maven
aaPIXa6223 小时前
C++模板元编程:在编译期完成计算,实现零运行时开销
java·开发语言·c++
夜郎king4 小时前
RuoYi-Vue3 企业级后台快速落地实战指南
java·开发语言
二宝哥4 小时前
创建Gradle多模块SpringBoot项目
java·spring boot·后端