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 通过属性注入依赖于 PropertyBeanB,PropertyBeanB 也通过属性注入依赖于 PropertyBeanA。虽然属性注入的循环依赖可以通过 Spring 的三级缓存机制解决,但构造器注入的循环依赖无法通过该机制解决,这是因为构造器注入要求在 Bean 实例化时就完成依赖注入,而此时 Bean 还未进入可以暴露的阶段。
循环依赖带来的问题
循环依赖会导致 Spring 容器在创建 Bean 时陷入无限循环,最终抛出 BeanCurrentlyInCreationException 异常。这是因为在没有合适解决方案的情况下,Spring 会不断尝试创建相互依赖的 Bean,无法完成 Bean 的初始化过程。例如,当创建 ConstructorBeanA 时,需要调用其构造器并传入 ConstructorBeanB 的实例,而此时 ConstructorBeanB 还未创建,于是 Spring 开始创建 ConstructorBeanB。但在创建 ConstructorBeanB 时,又需要 ConstructorBeanA 的实例,这样就会陷入死循环,导致程序无法正常运行。
三级缓存的构成与作用
一级缓存-完全初始化Bean的仓库
一级缓存的定义与数据结构
一级缓存,也称为单例池,在 Spring 中通常用 ConcurrentHashMap<String, Object> 来实现。其中,键是 Bean 的名称,值是已经完全初始化好的 Bean 实例。ConcurrentHashMap 是线程安全的,这保证了在多线程环境下对一级缓存的访问和操作是安全的。
// 一级缓存的示例定义 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
一级缓存的作用
一级缓存是 Spring 中最终存储可用 Bean 的地方。当一个 Bean 经历了所有的初始化步骤,包括实例化、属性注入、初始化方法调用等,最终成为一个可以直接使用的完整 Bean 时,它会被放入一级缓存中。后续其他 Bean 如果需要依赖这个 Bean,就可以直接从一级缓存中获取,避免了重复创建 Bean 的开销。例如,当一个服务层的 Bean 需要依赖一个数据访问层的 Bean 时,Spring 会首先从一级缓存中查找该数据访问层 Bean,如果存在则直接使用。
一级缓存的使用时机
在 Bean 创建的最后阶段,当 Bean 完成了所有的初始化操作并且通过了各种后置处理器的处理后,Spring 会将该 Bean 放入一级缓存中。同时,会从二级缓存和三级缓存中移除该 Bean 的相关信息,以确保 Bean 只在一级缓存中存在。
二级缓存-早期暴露的Bean临时存储
二级缓存的定义与数据结构
二级缓存也是一个 Map 结构,通常使用 ConcurrentHashMap<String, Object> 实现。它用于存放早期暴露的 Bean 对象,这些 Bean 已经完成了实例化,但还没有进行属性填充等后续操作。
// 二级缓存的示例定义 private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
二级缓存的作用
二级缓存的主要作用是在解决循环依赖时,提供一个临时存储早期暴露 Bean 的地方。当一个 Bean 刚刚被实例化出来,但还没有完成所有的初始化步骤时,为了防止在循环依赖场景下出现问题,Spring 会将这个早期暴露的 Bean 先放入二级缓存中。这样,当其他 Bean 需要依赖它时,可以先从二级缓存中获取到这个尚未完全初始化的 Bean 引用,避免了不必要的递归创建。例如,在前面提到的 PropertyBeanA 和 PropertyBeanB 的循环依赖场景中,当创建 PropertyBeanA 并将其早期暴露到二级缓存后,在创建 PropertyBeanB 时就可以从二级缓存中获取到 PropertyBeanA 的引用,从而打破循环。
二级缓存的使用时机
在 Bean 实例化完成后,Spring 会将该 Bean 的早期引用放入二级缓存中。在后续的属性注入和初始化过程中,如果发现有其他 Bean 依赖于该 Bean,就会先从二级缓存中尝试获取该 Bean 的引用。当 Bean 最终完成所有初始化操作并放入一级缓存后,会从二级缓存中移除该 Bean 的引用。
三级缓存-延迟创建Bean的工厂
三级缓存的定义与数据结构
三级缓存是一个 Map<String, ObjectFactory<?>> 结构,其中键是 Bean 的名称,值是一个 ObjectFactory 对象。ObjectFactory 是一个函数式接口,它只有一个 getObject() 方法,用于创建 Bean 对象。
// 三级缓存的示例定义 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
三级缓存的作用
三级缓存的主要作用是提供一种延迟加载和创建 Bean 的机制。在 Bean 创建过程中,尤其是在处理循环依赖时,它能够通过 ObjectFactory 来获取早期暴露的 Bean 对象。通过这种方式,Spring 可以在合适的时机创建 Bean,避免了因为直接创建而可能导致的循环依赖问题。例如,在某些情况下,可能需要对 Bean 进行一些额外的处理或增强,通过 ObjectFactory 可以在需要的时候才进行这些操作,而不是在 Bean 实例化时就进行。
三级缓存的使用时机
在 Bean 实例化完成后,Spring 会创建一个 ObjectFactory 对象,并将其放入三级缓存中。这个 ObjectFactory 对象会在需要获取早期暴露的 Bean 时被调用。当从三级缓存中获取到 ObjectFactory 并调用其 getObject() 方法后,会将获取到的 Bean 放入二级缓存中,并从三级缓存中移除该 ObjectFactory。
利用三级缓存解决循环依赖的详细过程
创建Bean A
实例化Bean A
当 Spring 容器开始创建 BeanA 时,首先会调用 BeanA 的构造函数来实例化它。在这个阶段,BeanA 只是一个刚被创建出来的对象,它的所有属性都还是默认值,还没有进行属性注入等操作。例如,如果 BeanA 有一个 name 属性,此时 name 的值为 null。
// BeanA 的构造函数示例 public class BeanA { private BeanB beanB; public BeanA() { // 构造函数逻辑 } }
将Bean A的早期引用放入三级缓存
在 BeanA 实例化完成后,Spring 会创建一个 ObjectFactory 对象,该对象封装了获取 BeanA 早期引用的逻辑。然后将这个 ObjectFactory 对象放入三级缓存中。
// 将 BeanA 的早期引用放入三级缓存的示例逻辑 singletonFactories.put("beanA", () -> getEarlyBeanReference(beanName, mbd, bean));
属性注入时发现对Bean B的依赖
开始属性注入
在 BeanA 实例化完成并将其早期引用放入三级缓存后,Spring 开始进行 BeanA 的属性注入。在这个过程中,会检查 BeanA 的所有依赖项。
发现对Bean B的依赖
当检查到 BeanA 依赖于 BeanB 时,Spring 容器会尝试去获取 BeanB。由于此时容器中还没有完全初始化的 BeanB,所以 Spring 会进入创建 BeanB 的流程。
创建Bean B及发现对Bean A的依赖
实例化Bean B
Spring 容器开始创建 BeanB,同样会调用 BeanB 的构造函数来实例化它。此时,BeanB 也只是一个刚被创建出来的对象,其属性还未进行注入。
// BeanB 的构造函数示例 public class BeanB { private BeanA beanA; public BeanB() { // 构造函数逻辑 } }
将Bean B的早期引用放入三级缓存
与 BeanA 类似,在 BeanB 实例化完成后,Spring 会创建一个 ObjectFactory 对象,并将其放入三级缓存中。
发现对Bean A的依赖
在进行 BeanB 的属性注入时,发现 BeanB 依赖于 BeanA。此时,Spring 会尝试从缓存中获取 BeanA。
从三级缓存获取Bean A
检查缓存
Spring 首先会检查一级缓存中是否存在 BeanA,由于 BeanA 还未完全初始化,所以一级缓存中不存在。然后会检查二级缓存,二级缓存中也没有。最后会检查三级缓存,发现三级缓存中有 BeanA 对应的 ObjectFactory。
获取早期暴露的Bean A
Spring 会调用 BeanA 对应的 ObjectFactory 的 getObject() 方法,获取到早期暴露的 BeanA 的引用。然后将这个 BeanA 的引用从三级缓存中移除,并放入二级缓存中。
完成Bean B的创建
注入Bean A的引用
将从二级缓存中获取到的 BeanA 的引用注入到 BeanB 中。此时,BeanB 虽然获取到的是一个尚未完全初始化的 BeanA 引用,但这已经足够打破循环依赖。
完成Bean B的初始化
BeanB 继续完成其他属性的注入和初始化操作。在这个过程中,由于已经有了 BeanA 的引用,所以不会再触发对 BeanA 的重新创建,从而避免了循环依赖。最终,BeanB 完成所有的初始化操作,被放入一级缓存中。
完成Bean A的创建
获取Bean B的引用
BeanA 获取到已经完全创建好的 BeanB,这个 BeanB 是从一级缓存中获取的。
完成Bean A的初始化
BeanA 继续完成自身剩余的属性注入和其他初始化操作。最后,BeanA 也完成了所有的初始化步骤,被放入一级缓存中。此时,BeanA 和 BeanB 都已经成功创建,并且它们之间的循环依赖问题也得到了妥善解决。
通过这样精细的三级缓存机制,Spring 成功地打破了循环依赖的僵局,确保了 Bean 之间能够正确地进行依赖注入和初始化,为开发者提供了一个稳定、可靠的依赖管理环境。同时,我们也应该注意到,这种机制对于构造器注入的循环依赖是无法解决的,在开发过程中应尽量避免构造器注入的循环依赖情况的发生。
以上就是Spring用三级缓存处理循环依赖的方法详解的详细内容,更多关于Spring三级缓存处理循环依赖的资料请关注脚本之家其它相关文章!
相关文章
SpringBoot中使用Session共享实现分布式部署的示例代码
这篇文章主要介绍了SpringBoot中使用Session共享实现分布式部署的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-07-07
最新评论