Spring循环依赖:三级缓存解析

什么是循环依赖?

循环依赖指的是两个或多个 Bean 之间相互依赖的情况。例如:

复制代码
// A依赖B
@Component
public class A {
    @Autowired
    private B b;
}

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

在这个例子中,A 的创建需要 B,而 B 的创建又需要 A,形成了一个闭环。如果没有特殊处理,这将导致无限循环或初始化失败。

循环依赖的类型

Spring 中存在三种主要的循环依赖场景:

  1. 构造器注入循环依赖:通过构造方法相互依赖
  2. setter 注入循环依赖:通过 setter 方法相互依赖
  3. 字段注入循环依赖:通过字段直接注入相互依赖

其中,只有构造器注入的循环依赖是 Spring 无法解决的,而后两种可以通过 Spring 的缓存机制完美解决。

Spring 解决循环依赖的核心:三级缓存

Spring 通过三级缓存机制解决了单例 Bean 的循环依赖问题,这三个缓存实际上是三个 Map:

  1. 一级缓存(singletonObjects):存储完全初始化完成的单例 Bean
  2. 二级缓存(earlySingletonObjects):存储提前暴露的、尚未完全初始化的单例 Bean 实例
  3. 三级缓存(singletonFactories):存储 Bean 的工厂对象(ObjectFactory),用于创建 Bean 的早期引用

这三级缓存的定义可以在DefaultSingletonBeanRegistry类中找到:

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

// 二级缓存:存放早期暴露的Bean实例(未完全初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

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

解决循环依赖的详细流程

让我们以 A 依赖 B,B 依赖 A 的场景为例,详细解析 Spring 如何处理循环依赖:

  1. 初始化 A

    • 检查三级缓存,发现 A 不存在
    • 实例化 A(调用构造方法,此时 A 的属性尚未设置)
    • 将 A 的工厂对象添加到三级缓存:singletonFactories.put("a", () -> getEarlyBeanReference("a", mbd, a))
    • 开始为 A 填充属性,发现依赖 B
  2. 初始化 B

    • 检查三级缓存,发现 B 不存在
    • 实例化 B
    • 将 B 的工厂对象添加到三级缓存
    • 开始为 B 填充属性,发现依赖 A
  3. 处理 B 对 A 的依赖

    • 检查一级缓存,A 不存在(未完全初始化)
    • 检查二级缓存,A 不存在
    • 检查三级缓存,找到 A 的工厂对象
    • 通过工厂对象获取 A 的早期引用
    • 将 A 的早期引用从三级缓存移到二级缓存
    • 将 A 的早期引用注入到 B 中
    • B 完成初始化,放入一级缓存
  4. 完成 A 的初始化

    • 将 B 从一级缓存中取出,注入到 A 中
    • A 完成初始化,从二级缓存移到一级缓存

通过这个流程,Spring 成功打破了 "A 依赖 B,B 依赖 A" 的循环,两个 Bean 都能被正确初始化。

三级缓存的精妙之处

为什么需要三级缓存而不是两级?这与 Spring 的 AOP 特性密切相关。

三级缓存中存储的是ObjectFactory,它的作用是在必要时生成 Bean 的代理对象。当一个 Bean 需要被 AOP 增强时,这个工厂会返回代理对象而不是原始对象。

如果没有三级缓存,直接在二级缓存中存储原始对象,那么当需要代理时,注入的就会是原始对象而不是代理对象,这显然不符合 AOP 的预期。

通过三级缓存,Spring 实现了延迟创建代理对象的功能,只有在发生循环依赖时才会提前创建代理,否则会在 Bean 初始化完成后再创建代理,这符合 Spring 的设计理念。

无法解决的循环依赖情况

Spring 的循环依赖解决方案并非万能,以下情况无法解决:

  1. 构造器注入的循环依赖

    @Component
    public class A {
    private B b;

    复制代码
     @Autowired
     public A(B b) {
         this.b = b;
     }

    }

    @Component
    public class B {
    private A a;

    复制代码
     @Autowired
     public B(A a) {
         this.a = a;
     }

    }

这种情况下,Spring 会抛出BeanCurrentlyInCreationException异常,因为构造器注入需要在实例化阶段就获取依赖,而此时依赖对象还未创建。

原型 Bean 的循环依赖

原型 Bean(@Scope("prototype"))每次请求都会创建新实例,Spring 不会缓存原型 Bean,因此无法解决其循环依赖。

非单例 Bean 的循环依赖

除了单例 Bean 外,其他作用域的 Bean(如 request、session)的循环依赖也无法被 Spring 解决。

如何避免循环依赖?

虽然 Spring 能解决单例 Bean 的循环依赖,但在设计上应尽量避免循环依赖,这通常意味着代码结构可以优化:

  1. 重构代码:将相互依赖的部分提取到第三个类中
  2. 使用 @Lazy 注解:延迟初始化其中一个 Bean
  3. 使用 setter 注入代替构造器注入:构造器注入更适合强制依赖,但可能导致循环依赖
相关推荐
程序员爱钓鱼1 小时前
Python编程实战 · 基础入门篇 | Python的缩进与代码块
后端·python
earthzhang20214 小时前
第3讲:Go垃圾回收机制与性能优化
开发语言·jvm·数据结构·后端·性能优化·golang
apocelipes4 小时前
golang unique包和字符串内部化
java·python·性能优化·golang
Full Stack Developme5 小时前
java.text 包详解
java·开发语言·python
刘梦凡呀6 小时前
C#获取钉钉平台考勤记录
java·c#·钉钉
thinktik6 小时前
AWS EKS 集成Load Balancer Controller 对外暴露互联网可访问API [AWS 中国宁夏区]
后端·kubernetes·aws
best_virtuoso6 小时前
PostgreSQL 常见数组操作函数语法、功能
java·数据结构·postgresql
yudiandian20146 小时前
02 Oracle JDK 下载及配置(解压缩版)
java·开发语言
追逐时光者7 小时前
将 EasySQLite 解决方案文件格式从 .sln 升级为更简洁的 .slnx
后端·.net
驰羽7 小时前
[GO]GORM 常用 Tag 速查手册
开发语言·后端·golang