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 注入代替构造器注入:构造器注入更适合强制依赖,但可能导致循环依赖
相关推荐
aloha_7892 分钟前
新国都面试真题
jvm·spring boot·spring·面试·职场和发展
你的人类朋友6 分钟前
🍃说说Base64
前端·后端·安全
zzywxc7879 分钟前
AI 行业应用:AI 在金融、医疗、教育、制造业等领域的落地案例
人工智能·spring·金融·prompt·语音识别·xcode
凯哥197019 分钟前
Vue 3 + Supabase 认证与授权时序最佳实践指南
前端·后端
唐叔在学习19 分钟前
pip安装太慢?一键切换国内镜像源,速度飞起!
后端·python
BingoGo19 分钟前
PHP 8.2 vs PHP 8.3 对比:新功能、性能提升和迁移技巧
后端·php
我不是混子19 分钟前
如何保证接口幂等性?
java·后端
Gz、21 分钟前
Spring Boot 常用注解详解
spring boot·后端·python
用户40993225021224 分钟前
PostgreSQL数据类型怎么选才高效不踩坑?
后端·ai编程·trae