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 天前
Spring Boot 拦截器(Interceptor)与过滤器(Filter)有什么区别?
java·spring boot·后端
来一杯龙舌兰1 天前
【Sharding-JDBC】Spring/Spring Boot 集成 Sharding-JDBC,分表策略与 API、YAML 配置实践
java·spring boot·spring
叫我阿柒啊1 天前
Java全栈开发工程师面试实战:从基础到微服务的完整技术演进
java·spring boot·微服务·前端框架·vue3·全栈开发·面试技巧
华农第一蒟蒻1 天前
Elasticsearch赋能3D打印机任务统计分析
java·大数据·spring boot·后端·elasticsearch·adb·maven
bobz9651 天前
mtu 协商与配置
后端
AAA修煤气灶刘哥1 天前
后端哭晕:超时订单取消踩过的坑,延迟消息这么玩才对!
后端·spring cloud·rabbitmq
金銀銅鐵1 天前
[Java] 验证 HashMap 的扩容时机
java·后端
间彧1 天前
Java高级语法糖有哪些
java
食亨技术团队1 天前
聚合配送系统对非阻塞并发的实践
后端
零念1 天前
ragflow-疑难杂症-OSError: [Errno 24] Too many open files
后端