Spring循环依赖解决方案

注意
本文最后更新于 2025-05-07,文中内容可能已过时。

Spring 循环依赖的产生原因、底层解决逻辑(三级缓存)

核心前提:Spring 仅解决「单例 Bean + Setter 注入」的循环依赖,其他场景均无法解决,这是基础前提,必须先记住。

一、循环依赖的产生场景

定义:两个或多个 Bean 互相依赖对方,形成闭环(如 A 依赖 B,B 依赖 A;或 A→B→C→A),Spring 容器启动时无法正常创建 Bean,需通过特定机制解决。

1. Setter 注入(可解决)

最常见场景,通过 Setter 方法注入依赖,允许“先实例化 Bean,后注入依赖”,这是 Spring 能解决循环依赖的核心前提。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 示例:Setter 注入循环依赖(Spring 可解决)
@Component
public class A {
    @Autowired
    private B b; // A 依赖 B
}

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

2. 构造器注入(无法解决)

通过构造器注入依赖,要求“实例化 Bean 时必须注入所有构造器依赖”,而循环依赖的 Bean 互相等待对方实例化,形成死锁,Spring 无法解决。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 示例:构造器注入循环依赖(Spring 无法解决)
@Component
public class A {
    // 构造器注入 B,实例化 A 必须先有 B
    public A(B b) {
        this.b = b;
    }
    private B b;
}

@Component
public class B {
    // 构造器注入 A,实例化 B 必须先有 A
    public B(A a) {
        this.a = a;
    }
    private A a;
}

关键点:“构造器注入无法解决循环依赖,Setter 注入(单例)可解决”,后续结合三级缓存说明原因。

二、三级缓存解决循环依赖的原理(源码级流程)

核心逻辑:Spring 通过「三级缓存」提前暴露“未完成初始化的 Bean 实例”,让依赖方先获取到这个“半成品”Bean,避免互相等待,待双方都实例化、注入完成后,再完善 Bean 初始化。

核心前提:仅适用于「单例 Bean」(原型 Bean 每次获取都新建,无法缓存,故无法解决)、「Setter 注入」(允许先实例化后注入)。

1. 三级缓存的定义(源码核心变量)

Spring 容器中,三级缓存是三个 Map(定义在 DefaultSingletonBeanRegistry 中),作用各不相同,缺一不可:

缓存级别 缓存名称(源码变量) 存储内容 核心作用
一级缓存(最终缓存) singletonObjects 完全初始化完成的单例 Bean(可直接使用) 供外部获取已就绪的 Bean,避免重复创建,是最终的缓存容器
二级缓存(临时缓存) earlySingletonObjects 未完成初始化(未注入依赖、未执行初始化方法)但已实例化的单例 Bean(半成品) 避免重复执行三级缓存中的工厂方法,提升性能,临时存储“提前暴露”的半成品 Bean
三级缓存(工厂缓存) singletonFactories 单例 Bean 的工厂对象(ObjectFactory),用于生成“提前暴露的半成品 Bean” 核心!解决 AOP 代理场景下的循环依赖,通过工厂方法动态生成代理对象(而非直接存储 Bean)

2. 源码级解决流程(以 A→B、B→A 为例)

核心步骤:实例化 → 暴露工厂 → 注入依赖 → 初始化 → 放入一级缓存,循环依赖的关键是“提前暴露工厂”,打破互相等待。

  1. Spring 容器启动,开始创建 Bean A(单例):

    • 1.1 检查一级缓存(singletonObjects):无 A,继续;

    • 1.2 检查二级缓存(earlySingletonObjects):无 A,继续;

    • 1.3 检查三级缓存(singletonFactories):无 A,继续;

    • 1.4 实例化 A(仅执行构造器,未注入依赖、未初始化,是半成品);

    • 1.5 创建 A 的工厂对象(ObjectFactory),放入三级缓存(singletonFactories),提前暴露 A 的半成品

    • 1.6 开始为 A 注入依赖 B(此时 A 还未初始化)。

  2. Spring 开始创建 Bean B(单例):

    • 2.1 检查一级、二级、三级缓存:无 B,继续;

    • 2.2 实例化 B(半成品,未注入依赖、未初始化);

    • 2.3 创建 B 的工厂对象,放入三级缓存;

    • 2.4 开始为 B 注入依赖 A(此时 B 还未初始化)。

  3. B 注入依赖 A 时,触发 A 的获取流程:

    • 3.1 检查一级缓存:无 A(A 未初始化完成);

    • 3.2 检查二级缓存:无 A;

    • 3.3 检查三级缓存:有 A 的工厂对象;

    • 3.4 通过 A 的工厂对象,获取 A 的半成品(未初始化),放入二级缓存(earlySingletonObjects),并删除三级缓存中的 A 工厂;

    • 3.5 将 A 的半成品注入到 B 中(此时 B 已注入依赖)。

  4. B 完成注入后,执行初始化方法,初始化完成后,将 B 放入一级缓存(singletonObjects),删除二级、三级缓存中的 B;

  5. B 创建完成,回到 A 的注入流程,将 B(已就绪)注入到 A 中;

  6. A 完成注入后,执行初始化方法,初始化完成后,将 A 放入一级缓存,删除二级缓存中的 A 半成品;

  7. 循环依赖解决,A 和 B 均为就绪状态,可正常使用。

面试技巧:不用背源码细节,重点说清“提前暴露工厂→获取半成品→注入→初始化→放入一级缓存”的流程,强调三级缓存的协作逻辑。

三、关键问题:为什么需要三级缓存?二级缓存行不行? 🟠

核心结论:不行!必须用三级缓存,二级缓存无法解决 AOP 代理场景下的循环依赖,这是 Spring 设计三级缓存的核心原因。

1. 二级缓存的局限

如果只保留一级+二级缓存,流程会变成:Bean 实例化后,直接将半成品放入二级缓存,供依赖方获取。

但如果 Bean 需要被 AOP 代理(如 @Transactional、@Aspect 注解),问题就出现了:

  • Bean 实例化时,生成的是“原始对象”,而最终需要的是“代理对象”;

  • 如果直接将原始对象放入二级缓存,依赖方注入的是原始对象,而非代理对象,后续 Bean 初始化时生成代理对象,会导致依赖注入的对象与最终的 Bean 对象不一致,出现异常。

2. 三级缓存的核心作用(解决 AOP 代理问题)

三级缓存存储的是“工厂对象(ObjectFactory)”,而非直接存储 Bean 实例,工厂对象的核心作用是:在依赖方获取 Bean 时,动态生成代理对象(如果需要)

具体逻辑:

  • Bean 实例化后,不直接暴露半成品,而是暴露一个工厂;

  • 当依赖方需要获取该 Bean 时,通过工厂方法 getObject() 生成对象:

    • 如果 Bean 需要 AOP 代理:工厂方法生成代理对象,放入二级缓存;

    • 如果不需要代理:工厂方法直接返回原始半成品对象,放入二级缓存;

  • 这样,依赖方注入的就是“最终需要的对象”(代理/原始),保证了对象一致性。

3. 总结

二级缓存只能存储“固定的半成品 Bean”,无法处理 AOP 代理场景(无法动态生成代理对象);三级缓存通过工厂对象,延迟生成代理对象,既解决了循环依赖,又保证了 AOP 代理的正确性,所以必须用三级缓存。

四、无法解决循环依赖的场景 🔴

Spring 仅解决「单例 Bean + Setter 注入」的循环依赖,以下场景均无法解决,会直接抛出 BeanCurrentlyInCreationException 异常。

1. 构造器注入(最常见)

原因:构造器注入要求“实例化 Bean 时必须注入所有依赖”,循环依赖的 Bean 互相等待对方实例化,形成死锁,Spring 无法打破。

解决方案:将构造器注入改为 Setter 注入,或使用 @Lazy 注解(懒加载,延迟依赖注入)。

2. 原型 Bean(prototype 作用域)

原因:原型 Bean 每次 getBean 都会新建一个实例,Spring 不缓存原型 Bean,无法通过三级缓存提前暴露半成品,每次创建都会陷入循环。

解决方案:尽量避免原型 Bean 之间的循环依赖,或改为单例 Bean。

3. 其他场景

  • 单例 Bean + 构造器注入(同场景1);

  • Bean 被 @Scope("request/session") 修饰(非单例,无法缓存);

  • 手动调用 getBean() 触发的循环依赖(未走 Spring 容器的缓存流程)。

五、伪代码

核心:三级缓存的协作流程,重点体现“实例化→暴露工厂→注入→初始化”的逻辑,以 A→B、B→A 为例。

1. 三级缓存定义(伪代码)

1
2
3
4
5
6
// 一级缓存:存储完全初始化的单例 Bean
private Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
// 二级缓存:存储未初始化的半成品 Bean
private Map<String, Object> earlySingletonObjects = new HashMap<>();
// 三级缓存:存储 Bean 工厂,用于生成半成品/代理对象
private Map<String, ObjectFactory> singletonFactories = new HashMap<>();

2. 循环依赖解决流程(伪代码+文字描述,默写重点)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 1. 创建 Bean A
if (!singletonObjects.containsKey("a")) {
    // 实例化 A(半成品,未注入、未初始化)
    A a = new A();
    // 暴露 A 的工厂到三级缓存
    singletonFactories.put("a", () -> getEarlyBeanReference(a));
    // 为 A 注入依赖 B
    B b = (B) getBean("b"); // 触发 B 的创建
    a.setB(b); // 注入 B
    // A 初始化完成,放入一级缓存
    singletonObjects.put("a", a);
    // 删除二、三级缓存中的 A
    earlySingletonObjects.remove("a");
    singletonFactories.remove("a");
}

// 2. 创建 Bean B(被 A 注入时触发)
if (!singletonObjects.containsKey("b")) {
    // 实例化 B(半成品)
    B b = new B();
    // 暴露 B 的工厂到三级缓存
    singletonFactories.put("b", () -> getEarlyBeanReference(b));
    // 为 B 注入依赖 A
    A a = (A) getBean("a"); // 从三级缓存获取 A 的工厂,生成半成品 A
    b.setA(a); // 注入 A(半成品)
    // B 初始化完成,放入一级缓存
    singletonObjects.put("b", b);
    // 删除二、三级缓存中的 B
    earlySingletonObjects.remove("b");
    singletonFactories.remove("b");
}

// 3. 工厂方法(核心:动态生成代理对象)
private Object getEarlyBeanReference(Object bean) {
    // 如果需要 AOP 代理,返回代理对象;否则返回原始 bean
    return wrapIfNecessary(bean); // 模拟 AOP 代理逻辑
}

A 实例化 → 放工厂(三级)→ 注入 B → B 实例化 → 放工厂(三级)→ 注入 A → 从三级拿 A 工厂 → 生成 A 半成品(放二级)→ B 注入 A 后初始化 → 放一级 → A 注入 B 后初始化 → 放一级 → 循环解决。

总结 🔴

  1. Spring 仅解决「单例 Bean + Setter 注入」的循环依赖,构造器注入、原型 Bean 无法解决;

  2. 三级缓存:singletonObjects(成品)、earlySingletonObjects(半成品)、singletonFactories(工厂);

  3. 核心原理:提前暴露 Bean 工厂,动态生成半成品/代理对象,打破互相等待;

  4. 为什么需要三级缓存?二级缓存无法处理 AOP 代理,三级缓存通过工厂延迟生成代理对象,保证对象一致性;

  5. 异常场景:构造器注入、原型 Bean 循环依赖,会抛出 BeanCurrentlyInCreationException。