Spring 是怎么解决循环依赖的

目录

一、什么是循环依赖​

二、Spring 解决循环依赖的原理 —— 三级缓存​

三、案例演示​

四、总结​


在 Java 开发中,Spring 框架以其强大的依赖注入功能深受开发者喜爱。然而,当项目中出现循环依赖时,就会引发一系列问题。本文将深入探讨 Spring 是如何解决循环依赖的,并结合实际案例进行详细说明。​

一、什么是循环依赖​

循环依赖,简单来说就是多个 Bean 之间相互依赖,形成一个闭环。例如,Bean A 依赖 Bean B,Bean B 又依赖 Bean C,而 Bean C 反过来又依赖 Bean A,这样就构成了循环依赖。在 Spring 容器创建 Bean 的过程中,如果遇到循环依赖,若不进行特殊处理,就会导致创建 Bean 的过程陷入死循环,无法成功初始化 Bean。​

根据依赖注入方式的不同,循环依赖主要分为三种类型:​

  1. 构造器循环依赖:通过构造函数进行依赖注入时产生的循环依赖。例如,class A { private final B b; public A(B b) { this.b = b; } } 与 class B { private final A a; public B(A a) { this.a = a; } },A 的构造函数需要 B 的实例,B 的构造函数又需要 A 的实例,形成循环依赖。​
  1. setter 循环依赖:通过 setter 方法进行依赖注入时产生的循环依赖。​
  1. 字段注入循环依赖:通过类的成员变量直接注入时产生的循环依赖 。​

二、Spring 解决循环依赖的原理 —— 三级缓存​

Spring 主要通过三级缓存机制来解决循环依赖问题,这三级缓存分别是:​

  1. 一级缓存(singletonObjects):用于存储已经创建完成且属性已填充好的单例 Bean。当 Spring 容器从一级缓存中获取到 Bean 时,这个 Bean 是完全可用的状态。​
  2. 二级缓存(earlySingletonObjects):存储早期暴露的单例 Bean 对象引用,这些 Bean 已经实例化但还未完成属性填充和初始化等完整过程。在解决循环依赖时,早期暴露的 Bean 会被放入二级缓存,供其他依赖它的 Bean 获取引用。​
  3. 三级缓存(singletonFactories):存储 Bean 的创建工厂,用于生成 Bean 的早期引用。每个单例 Bean 对应一个 ObjectFactory,通过这个工厂可以获取到 Bean 的早期引用。​

当 Spring 容器创建 Bean 时,解决循环依赖的大致流程如下:​

  1. 首先从一级缓存singletonObjects中尝试获取 Bean,如果能获取到,说明 Bean 已经创建完成,直接返回。​
  2. 如果一级缓存中没有,就从二级缓存earlySingletonObjects中获取,如果能获取到,返回早期暴露的 Bean 引用。​
  3. 如果二级缓存也没有,就检查三级缓存singletonFactories中是否存在对应的 ObjectFactory。如果存在,通过 ObjectFactory 获取 Bean 的早期引用,并将这个早期引用从三级缓存移动到二级缓存中,同时从三级缓存中移除对应的 ObjectFactory(因为早期引用已经放入二级缓存,不需要再通过工厂获取了)。​
  4. 如果三级缓存中也没有,就开始创建 Bean。首先通过反射实例化 Bean(此时 Bean 只是一个空壳,属性还未填充),然后将创建 Bean 的 ObjectFactory 放入三级缓存singletonFactories中,这一步就是早期暴露。接着进行属性填充,在填充属性过程中,如果遇到依赖的其他 Bean,就会递归地去创建这些依赖的 Bean。如果在创建依赖 Bean 的过程中,依赖的 Bean 又依赖当前正在创建的 Bean,就会从二级缓存或三级缓存中获取当前 Bean 的早期引用,从而打破循环依赖。​
  5. 当 Bean 的属性填充完成后,将 Bean 从二级缓存(如果之前在二级缓存的话)移动到一级缓存singletonObjects中,并将二级缓存中的对应引用移除,此时 Bean 就处于完全可用状态。​

三、案例演示​

接下来通过一个简单的案例,演示 Spring 是如何解决循环依赖的。​

1. 创建 Bean 类​

public class A {​

private B b;​

​

public void setB(B b) {​

this.b = b;​

}​

​

public void say() {​

System.out.println("A say: I have B, and B says: " + b.say());​

}​

​

public String say() {​

return "A say something";​

}​

}​

public class B {​

private A a;​

​

public void setA(A a) {​

this.a = a;​

}​

​

public String say() {​

return "B say: I have A, and A says: " + a.say();​

}​

}​

在上述代码中,A类依赖B类,B类又依赖A类,形成了 setter 循环依赖。​

2. 配置 Spring 容器​

<beans xmlns="http://www.springframework.org/schema/beans"​

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"​

xsi:schemaLocation="http://www.springframework.org/schema/beans​

http://www.springframework.org/schema/beans/spring-beans.xsd">​

<bean id="a" class="com.example.A">​

<property name="b" ref="b"/>​

</bean>​

<bean id="b" class="com.example.B">​

<property name="a" ref="a"/>​

</bean>​

</beans>​

3. 测试代码​

import org.springframework.context.ApplicationContext;​

import org.springframework.context.support.ClassPathXmlApplicationContext;​

​

public class Main {​

public static void main(String[] args) {​

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");​

A a = context.getBean("a", A.class);​

a.say();​

}​

}​

​

运行上述测试代码,Spring 容器能够成功创建A和B的实例,并且不会因为循环依赖而报错。这是因为在创建A实例时,会先实例化A并将其早期引用放入三级缓存,然后在填充A的b属性时,去创建B实例。在创建B实例并填充其a属性时,由于A已经在三级缓存中暴露了早期引用,所以可以获取到A的早期引用并注入到B中,从而打破了循环依赖。​

四、总结​

Spring 通过巧妙的三级缓存机制,有效地解决了循环依赖问题,确保了容器中 Bean 的正常创建和初始化。在实际开发中,虽然 Spring 能够处理大部分的循环依赖情况,但我们还是应该尽量避免过度复杂的循环依赖,保持代码的清晰和简洁,这样更有利于项目的维护和扩展。同时,理解 Spring 解决循环依赖的原理,也有助于我们在遇到类似问题时,能够快速定位和解决问题。​

希望本文对你理解 Spring 解决循环依赖的机制有所帮助。如果你在实际开发中还有其他关于 Spring 的问题,欢迎在评论区留言讨论。​

上述内容结合原理与案例讲解了 Spring 解决循环依赖的方法。若你对案例代码细节、原理拓展还有需求,或想了解其他 Spring 相关知识,欢迎随时说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值