当前位置: 首页 > article >正文

Spring 通过三级缓存解决循环依赖的深度剖析

Spring 通过三级缓存解决循环依赖的深度剖析

一、循环依赖问题的本质

(一)循环依赖的定义与常见场景

在 Spring 框架中,依赖注入是其核心特性之一,它允许对象之间的依赖关系在运行时动态注入。然而,当多个 Bean 之间的依赖关系形成一个闭环时,就会出现循环依赖问题。简单来说,循环依赖就是 Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,或者存在更复杂的多 Bean 循环依赖链。

常见的循环依赖场景包括构造器注入循环依赖和属性注入循环依赖。下面分别通过代码示例来展示这两种场景。

构造器注入循环依赖
@Component
public class ConstructorBeanA {
    private ConstructorBeanB beanB;

    @Autowired
    public ConstructorBeanA(ConstructorBeanB beanB) {
        this.beanB = beanB;
    }
}

@Component
public class ConstructorBeanB {
    private ConstructorBeanA beanA;

    @Autowired
    public ConstructorBeanB(ConstructorBeanA beanA) {
        this.beanA = beanA;
    }
}

在上述代码中,ConstructorBeanA 通过构造器依赖于 ConstructorBeanB,而 ConstructorBeanB 同样通过构造器依赖于 ConstructorBeanA。当 Spring 容器尝试创建这两个 Bean 时,会陷入无限循环,因为创建 ConstructorBeanA 需要先创建 ConstructorBeanB,而创建 ConstructorBeanB 又需要先创建 ConstructorBeanA

属性注入循环依赖
@Component
public class PropertyBeanA {
    @Autowired
    private PropertyBeanB beanB;
}

@Component
public class PropertyBeanB {
    @Autowired
    private PropertyBeanA beanA;
}

在属性注入循环依赖的场景中,PropertyBeanA 通过属性注入依赖于 PropertyBeanBPropertyBeanB 也通过属性注入依赖于 PropertyBeanA。虽然属性注入的循环依赖可以通过 Spring 的三级缓存机制解决,但构造器注入的循环依赖无法通过该机制解决,这是因为构造器注入要求在 Bean 实例化时就完成依赖注入,而此时 Bean 还未进入可以暴露的阶段。

(二)循环依赖带来的问题

循环依赖会导致 Spring 容器在创建 Bean 时陷入无限循环,最终抛出 BeanCurrentlyInCreationException 异常。这是因为在没有合适解决方案的情况下,Spring 会不断尝试创建相互依赖的 Bean,无法完成 Bean 的初始化过程。例如,当创建 ConstructorBeanA 时,需要调用其构造器并传入 ConstructorBeanB 的实例,而此时 ConstructorBeanB 还未创建,于是 Spring 开始创建 ConstructorBeanB。但在创建 ConstructorBeanB 时,又需要 ConstructorBeanA 的实例,这样就会陷入死循环,导致程序无法正常运行。

二、三级缓存的构成与作用

(一)一级缓存:完全初始化 Bean 的仓库

1. 一级缓存的定义与数据结构

一级缓存,也称为单例池,在 Spring 中通常用 ConcurrentHashMap<String, Object> 来实现。其中,键是 Bean 的名称,值是已经完全初始化好的 Bean 实例。ConcurrentHashMap 是线程安全的,这保证了在多线程环境下对一级缓存的访问和操作是安全的。

// 一级缓存的示例定义
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
2. 一级缓存的作用

一级缓存是 Spring 中最终存储可用 Bean 的地方。当一个 Bean 经历了所有的初始化步骤,包括实例化、属性注入、初始化方法调用等,最终成为一个可以直接使用的完整 Bean 时,它会被放入一级缓存中。后续其他 Bean 如果需要依赖这个 Bean,就可以直接从一级缓存中获取,避免了重复创建 Bean 的开销。例如,当一个服务层的 Bean 需要依赖一个数据访问层的 Bean 时,Spring 会首先从一级缓存中查找该数据访问层 Bean,如果存在则直接使用。

3. 一级缓存的使用时机

在 Bean 创建的最后阶段,当 Bean 完成了所有的初始化操作并且通过了各种后置处理器的处理后,Spring 会将该 Bean 放入一级缓存中。同时,会从二级缓存和三级缓存中移除该 Bean 的相关信息,以确保 Bean 只在一级缓存中存在。

(二)二级缓存:早期暴露 Bean 的临时存储

1. 二级缓存的定义与数据结构

二级缓存也是一个 Map 结构,通常使用 ConcurrentHashMap<String, Object> 实现。它用于存放早期暴露的 Bean 对象,这些 Bean 已经完成了实例化,但还没有进行属性填充等后续操作。

// 二级缓存的示例定义
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
2. 二级缓存的作用

二级缓存的主要作用是在解决循环依赖时,提供一个临时存储早期暴露 Bean 的地方。当一个 Bean 刚刚被实例化出来,但还没有完成所有的初始化步骤时,为了防止在循环依赖场景下出现问题,Spring 会将这个早期暴露的 Bean 先放入二级缓存中。这样,当其他 Bean 需要依赖它时,可以先从二级缓存中获取到这个尚未完全初始化的 Bean 引用,避免了不必要的递归创建。例如,在前面提到的 PropertyBeanAPropertyBeanB 的循环依赖场景中,当创建 PropertyBeanA 并将其早期暴露到二级缓存后,在创建 PropertyBeanB 时就可以从二级缓存中获取到 PropertyBeanA 的引用,从而打破循环。

3. 二级缓存的使用时机

在 Bean 实例化完成后,Spring 会将该 Bean 的早期引用放入二级缓存中。在后续的属性注入和初始化过程中,如果发现有其他 Bean 依赖于该 Bean,就会先从二级缓存中尝试获取该 Bean 的引用。当 Bean 最终完成所有初始化操作并放入一级缓存后,会从二级缓存中移除该 Bean 的引用。

(三)三级缓存:延迟创建 Bean 的工厂

1. 三级缓存的定义与数据结构

三级缓存是一个 Map<String, ObjectFactory<?>> 结构,其中键是 Bean 的名称,值是一个 ObjectFactory 对象。ObjectFactory 是一个函数式接口,它只有一个 getObject() 方法,用于创建 Bean 对象。

// 三级缓存的示例定义
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
2. 三级缓存的作用

三级缓存的主要作用是提供一种延迟加载和创建 Bean 的机制。在 Bean 创建过程中,尤其是在处理循环依赖时,它能够通过 ObjectFactory 来获取早期暴露的 Bean 对象。通过这种方式,Spring 可以在合适的时机创建 Bean,避免了因为直接创建而可能导致的循环依赖问题。例如,在某些情况下,可能需要对 Bean 进行一些额外的处理或增强,通过 ObjectFactory 可以在需要的时候才进行这些操作,而不是在 Bean 实例化时就进行。

3. 三级缓存的使用时机

在 Bean 实例化完成后,Spring 会创建一个 ObjectFactory 对象,并将其放入三级缓存中。这个 ObjectFactory 对象会在需要获取早期暴露的 Bean 时被调用。当从三级缓存中获取到 ObjectFactory 并调用其 getObject() 方法后,会将获取到的 Bean 放入二级缓存中,并从三级缓存中移除该 ObjectFactory

三、利用三级缓存解决循环依赖的详细过程

(一)创建 Bean A

1. 实例化 Bean A

当 Spring 容器开始创建 BeanA 时,首先会调用 BeanA 的构造函数来实例化它。在这个阶段,BeanA 只是一个刚被创建出来的对象,它的所有属性都还是默认值,还没有进行属性注入等操作。例如,如果 BeanA 有一个 name 属性,此时 name 的值为 null

// BeanA 的构造函数示例
public class BeanA {
    private BeanB beanB;

    public BeanA() {
        // 构造函数逻辑
    }
}
2. 将 Bean A 的早期引用放入三级缓存

BeanA 实例化完成后,Spring 会创建一个 ObjectFactory 对象,该对象封装了获取 BeanA 早期引用的逻辑。然后将这个 ObjectFactory 对象放入三级缓存中。

// 将 BeanA 的早期引用放入三级缓存的示例逻辑
singletonFactories.put("beanA", () -> getEarlyBeanReference(beanName, mbd, bean));

(二)属性注入时发现对 Bean B 的依赖

1. 开始属性注入

BeanA 实例化完成并将其早期引用放入三级缓存后,Spring 开始进行 BeanA 的属性注入。在这个过程中,会检查 BeanA 的所有依赖项。

2. 发现对 Bean B 的依赖

当检查到 BeanA 依赖于 BeanB 时,Spring 容器会尝试去获取 BeanB。由于此时容器中还没有完全初始化的 BeanB,所以 Spring 会进入创建 BeanB 的流程。

(三)创建 Bean B 及发现对 Bean A 的依赖

1. 实例化 Bean B

Spring 容器开始创建 BeanB,同样会调用 BeanB 的构造函数来实例化它。此时,BeanB 也只是一个刚被创建出来的对象,其属性还未进行注入。

// BeanB 的构造函数示例
public class BeanB {
    private BeanA beanA;

    public BeanB() {
        // 构造函数逻辑
    }
}
2. 将 Bean B 的早期引用放入三级缓存

BeanA 类似,在 BeanB 实例化完成后,Spring 会创建一个 ObjectFactory 对象,并将其放入三级缓存中。

3. 发现对 Bean A 的依赖

在进行 BeanB 的属性注入时,发现 BeanB 依赖于 BeanA。此时,Spring 会尝试从缓存中获取 BeanA

(四)从三级缓存获取 Bean A

1. 检查缓存

Spring 首先会检查一级缓存中是否存在 BeanA,由于 BeanA 还未完全初始化,所以一级缓存中不存在。然后会检查二级缓存,二级缓存中也没有。最后会检查三级缓存,发现三级缓存中有 BeanA 对应的 ObjectFactory

2. 获取早期暴露的 Bean A

Spring 会调用 BeanA 对应的 ObjectFactorygetObject() 方法,获取到早期暴露的 BeanA 的引用。然后将这个 BeanA 的引用从三级缓存中移除,并放入二级缓存中。

(五)完成 Bean B 的创建

1. 注入 Bean A 的引用

将从二级缓存中获取到的 BeanA 的引用注入到 BeanB 中。此时,BeanB 虽然获取到的是一个尚未完全初始化的 BeanA 引用,但这已经足够打破循环依赖。

2. 完成 Bean B 的初始化

BeanB 继续完成其他属性的注入和初始化操作。在这个过程中,由于已经有了 BeanA 的引用,所以不会再触发对 BeanA 的重新创建,从而避免了循环依赖。最终,BeanB 完成所有的初始化操作,被放入一级缓存中。

(六)完成 Bean A 的创建

1. 获取 Bean B 的引用

BeanA 获取到已经完全创建好的 BeanB,这个 BeanB 是从一级缓存中获取的。

2. 完成 Bean A 的初始化

BeanA 继续完成自身剩余的属性注入和其他初始化操作。最后,BeanA 也完成了所有的初始化步骤,被放入一级缓存中。此时,BeanABeanB 都已经成功创建,并且它们之间的循环依赖问题也得到了妥善解决。

通过这样精细的三级缓存机制,Spring 成功地打破了循环依赖的僵局,确保了 Bean 之间能够正确地进行依赖注入和初始化,为开发者提供了一个稳定、可靠的依赖管理环境。同时,我们也应该注意到,这种机制对于构造器注入的循环依赖是无法解决的,在开发过程中应尽量避免构造器注入的循环依赖情况的发生。

希望本文对 Spring 通过三级缓存解决循环依赖的详细讲解能够帮助读者更好地理解 Spring 框架的内部运行机制,在实际开发中能够更加熟练地运用 Spring 的各种特性。


http://www.kler.cn/a/527984.html

相关文章:

  • 【python】python基于机器学习与数据分析的手机特性关联与分类预测(源码+数据集)【独一无二】
  • 【AI】DeepSeek 概念/影响/使用/部署
  • 【gRPC-gateway】初探grpc网关,插件安装,默认实现,go案例
  • stm32控制直流电机程序
  • 【C++】List的模拟实现
  • 信号处理以及队列
  • MySQL 存储函数:数据库的自定义函数
  • 【Python蓝桥杯备赛宝典】
  • 落地 ORB角点检测与sift检测
  • Harbor 部署
  • 一文回顾讲解Java中的集合框架
  • 14-8C++STL的queue容器
  • OPENGLPG第九版学习 - OpenGL概述
  • 计算机网络一点事(20)
  • MySQL注入中load_file()函数的使用
  • 雨晨 27788.2025 企业版
  • Autogen_core源码:_agent_instantiation.py
  • 松灵机器人 scout ros2 驱动 安装
  • Elasticsearch Queries
  • UE学习日志#17 C++笔记#3 基础复习3
  • mysql 数据去重技术——全球数据备份—未来之窗跨平台操作
  • Java手写简单Merkle树
  • 【Java异步编程】基于任务类型创建不同的线程池
  • Python-基于mediapipe,pyautogui,cv2和numpy的电脑手势截屏工具(进阶版)
  • 【Rust】18.2. 可辩驳性:模式是否会无法匹配
  • Python 梯度下降法(五):Adam Optimize