Spring 三级缓存和循环依赖介绍

Spring May 6, 2025

Spring 中的 三级缓存机制 是为了解决 构造函数注入(不推荐)和 setter 注入(常见)方式下的单例 Bean 的循环依赖问题


💡 一、什么是循环依赖

循环依赖(Circular Dependency):两个或多个 Bean 相互依赖。

举例:

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

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

Spring 在创建 A 时需要先创建 B,但 B 又依赖 A,陷入循环。


🧠 二、Spring 如何解决循环依赖?

Spring 为了解决「构造函数注入之外的循环依赖」,引入了 三级缓存机制 来提前暴露对象引用:


🧱 三、三级缓存结构(DefaultSingletonBeanRegistry)

Spring 用以下三个 Map 来管理单例 Bean 的生命周期:

缓存名称 变量名 描述
一级缓存(成品缓存) singletonObjects 存放已完全初始化的单例 Bean
二级缓存(半成品缓存) earlySingletonObjects 存放早期曝光的半初始化 Bean(尚未完成依赖注入)
三级缓存(工厂缓存) singletonFactories 存放创建早期 Bean 引用的 ObjectFactory(避免循环引用)

🔁 四、创建 Bean 时的生命周期流程(含三级缓存)

A → B → A 的循环依赖为例,流程如下:

1. 创建 Bean A(A 依赖 B):

  • singletonObjects 查不到 A

  • 执行 createBean("a")

    • 实例化对象 new A(),但还没依赖注入
    • AObjectFactory 放入 singletonFactories(三级缓存)
    • 下一步准备注入依赖,发现需要 B

2. 创建 Bean B(B 依赖 A):

  • singletonObjects 查不到 B

  • 执行 createBean("b")

    • 实例化对象 new B(),把 B 的 ObjectFactory 放入三级缓存
    • 注入依赖时发现需要 A

3. B 注入 A:

  • singletonObjects 查不到 A

  • 查找 earlySingletonObjects,仍没有

  • 查找 singletonFactories(三级缓存):

    • 拿到 A 的工厂,创建早期引用并放入 earlySingletonObjects(二级缓存)
  • 注入 A 成功,继续完成 B 的初始化

  • B 初始化完成后,放入 singletonObjects,清除缓存中旧的

4. 回到 A:

  • B 已注入完成,A 的依赖完成
  • 初始化 A,放入 singletonObjects

❌ 五、为什么构造函数注入会失败?

构造函数注入会在实例化时就需要完整依赖,无法提前暴露引用,因此无法利用三级缓存机制。

@Component
public class A {
    private final B b;

    @Autowired
    public A(B b) { this.b = b; }  // 在实例化时就需要 b
}

在构造函数注入场景中,Spring 无法将 A 暴露给 B,因而循环依赖无法解决。


🧩 六、三级缓存简化图示

singletonObjects          ← 一级缓存:完全初始化的Bean
     ↑
earlySingletonObjects     ← 二级缓存:半初始化Bean引用
     ↑
singletonFactories        ← 三级缓存:创建Bean引用的工厂(lambda)

✅ 七、小结

项目 是否解决循环依赖
setter注入 ✅ 可以解决(通过三级缓存)
构造器注入 ❌ 不支持
@Scope("prototype") 原型模式 ❌ 不支持(因为原型不走单例容器)

📚 八、特殊情况

如果一个是构造器注入,一个是setter注入,会怎么样?

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

@Component
public class B {
    private A a;
    @Autowired
    public void setA(A a) {
        this.a = a;
    }
}

答案是:会报错

┌─────┐
|  a (field public com.example.demo.B com.example.demo.A.b)
↑     ↓
|  b defined in file class
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

无论是属性循环依赖,还是构造器循环依赖,Spring都会进行检测,并且在检测到循环依赖时,会抛出异常UnsatisfiedDependencyException异常。并给出提示信息。设置spring.main.allow-circular-references=true可以允许循环依赖。

允许循环依赖

我们让A是构造器注入,B是setter注入,然后设置spring.main.allow-circular-references=true,再运行

@Component
public class A {
    private final B b;  // 构造器注入
    @Autowired
    public A(B b) {
        this.b = b;
    }  // 在实例化时就需要 b    
}

@Component
public class B {
    private A a;
    @Autowired
    public void setA(A a) {
        this.a = a;
    }
} 

结果,还是报错。提示我们无法打破循环依赖。

```Java 
┌─────┐
|  a defined in file
↑     ↓
|  b (field public com.example.demo.A com.example.demo.B.a)
└─────┘


Action:

Despite circular references being allowed, the dependency cycle between beans could not be broken. Update your application to remove the dependency cycle.

🔍 为什么构造器注入+setter注入仍然失败?

  • A 使用 构造器注入 B
  • B 使用 setter注入 A
  • 开启了 allow-circular-references=true

关键点是:

Spring 的三级缓存机制只能在 “Bean 已经创建(new 了对象),但还没完成依赖注入” 这个阶段,提前暴露引用给别人用。

但是——

❌ 构造器注入的对象还没办法放入三级缓存

Spring 不能提前暴露 A 的引用(因为还没创建出来),而创建 A 需要 B,此时 Spring 会去创建 B,但 B 又依赖 A,而 A 还没法暴露出来,于是死循环发生了。


🔁 再还原一遍整个流程

  1. Spring 创建 A,它构造器需要 B
  2. 去创建 B
  3. B 是 setter 注入 A
  4. 于是先去获取 A 的引用
  5. 发现 A 还没有被创建(构造器注入阶段,不能提前暴露)
  6. ❌ 报错:依赖无法解析,循环引用未被打破

反过来

如果A是setter注入,B是构造器注入,会怎么样?
不会报错,可以解决循环依赖问题。

  1. Spring 开始创建 AA 是无参构造 + setter 注入 B
  2. A 实例通过无参构造器创建出来了 ✅
  3. Spring 立即将 A 的 ObjectFactory 放入 三级缓存 singletonFactories
  4. 准备注入依赖:A 需要注入 B → 进入创建 B
  5. 创建 B 时,发现其 构造器参数需要 A
  6. Spring 获取 A 的引用 → 进入 getSingleton("a")
  7. 一级缓存没有 → 二级缓存没有 → 从三级缓存中获取 A 的 ObjectFactory,并调用之
  8. 得到早期 A 的引用,放入二级缓存 earlySingletonObjects
  9. 使用这个 A 实例成功构造 B
  10. B 初始化完成,放入一级缓存
  11. 回到 A 的注入流程,将完整的 B 注入到 A 中
  12. A 也完成初始化,放入一级缓存 ✅

🧾 总结

Spring 为了解决 单例 Bean 的循环依赖问题,设计了 三级缓存机制,分别是:

  • singletonObjects:一级缓存,已完全初始化的 Bean;
  • earlySingletonObjects:二级缓存,正在初始化过程中的早期引用;
  • singletonFactories:三级缓存,用于生成早期引用的工厂(lambda 形式)。

只有当 Bean 是通过无参构造器创建(即不是构造器注入),并且注入方式为 setter 或字段注入 时,Spring 才能在创建阶段将其 ObjectFactory 放入三级缓存,从而打破依赖环路

✅ 可解决的循环依赖:

  • 两边都是 setter / 字段注入 ✅
  • 一方构造器注入,另一方 setter 注入,并且「构造器依赖方是后被创建的一方」✅

❌ 无法解决的循环依赖:

  • 构造器注入 → 构造器注入 ❌
  • 构造器注入在前,setter 注入在后 ❌

Spring 能解决一定范围内的循环依赖问题,但并非鼓励使用循环依赖。构造器注入无法通过缓存机制绕过依赖链,是一种明确的设计信号:“该依赖关系设计可能存在耦合过深的问题”

因此,在日常开发中,应尽量避免循环依赖,或通过以下方式解耦:

  • 拆分 Bean,提取中间服务;
  • 使用懒加载 @Lazy 延迟注入;

zzx

a programmer. github: https://github.com/zzxT