如何解决spring中的循环依赖
如何解决spring中的循环依赖
小吴顶呱呱如何解决spring循环依赖:
通俗来讲,循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套使用)
举个例子:
1 | public class AService { |
上述例子中 AService
依赖了 BService
,BService
也依赖了 AService
,这就是两个对象之间的相互依赖。当然循环依赖还包括 自身依赖、多个实例之间相互依赖。
1.1 Bean的创建步骤
为了能更好的展示出现循环依赖问题的环节,所以这里的 Bean 创建步骤做了简化:
- 在创建 Bean 之前,Spring 会通过扫描获取 BeanDefinition。
- BeanDefinition就绪后会读取 BeanDefinition 中所对应的 class 来加载类。
- 实例化阶段:根据构造函数来完成实例化 (未属性注入以及初始化的对象 这里简称为 原始对象)
- 属性注入阶段:对 Bean 的属性进行依赖注入 (这里就是发生循环依赖问题的环节)
- 如果 Bean 的某个方法有AOP操作,则需要根据原始对象生成代理对象。
- 最后把代理对象放入单例池(一级缓存
singletonObjects
)中。
1.2 为什么 SpringBean 会产生循环依赖
循环依赖是指两个或多个组件(通常是类或模块)之间相互引用,形成一个环路的情况。在编程中,循环依赖通常被视为一种不良的实践,因为它可能导致代码的可维护性下降,以及运行时的问题。
例如,在Spring框架中,循环依赖可能发生在以下几种情况下:
- 构造函数循环依赖:两个或更多的组件在它们的构造函数中互相依赖。
- 属性循环依赖:两个或更多的组件在它们的属性中互相依赖。
1.3 spring是如何解决循环依赖
Spring 中的循环依赖是指两个或更多个Bean之间相互依赖,形成一个闭环。这可能会导致应用程序启动失败或不稳定。Spring 通过三级缓存来解决这个问题。
1.3.1 spring的三级缓存
-
一级缓存:保存所有已经创建的bean
-
二级缓存:保存正常创建中的bean(完成了实例化,但未完成属性注入)
-
三级缓存:保存的是ObjectFactory类型的对象的工厂,通过工厂的方法可以获取到目标对象
在源码DefaultSingletonBeanRegistry类里定义了这几个缓存
1
2
3
4
5
6
7
8//一级缓存,保存所有已经创建完成的bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
//三级缓存,一个创建对象的工厂(其实就是根据这个工厂可以生产或者拿到一个对象)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
//二级缓存,保存的是未完成创建的bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
1.3.2 spring利用三级缓存解决循环依赖
-
三级缓存:Spring 使用三级缓存来提前曝光Bean对象,从而解决循环依赖。这三级缓存位于DefaultListableBeanFactory的父类DefaultSingletonBeanRegistry中,包括三个Map。
-
解决方案
当两个Bean相互引用时,Spring的解决过程如下:
1
2
3
4
5
6
7
8
9
10
public class ObjectA {
private ObjectB b;
}
public class ObjectB {
private ObjectA a;
}- 第一步:尝试从缓存中获取A对象,在每次创建对象之前,Spring首先会尝试从缓存中获取对象,但显然还没有创建过,所有从缓存获取不到,所以会执行下面的对象实例化过程,尝试创建A对象
- 第二步:实例化A对象 这一步会进行A对象的实例化流程,首先会在标记缓存标记A正在创建中,然后会调用构造方法进行实例化,然后把自己放到一个ObjectFactory工厂里在保存到三级缓存
- 第三步:A对象进行属性注入,我们发现A需要依赖于B对象,所以完成A要完成创建首先需要获得B,所以这时候会尝试从一级缓存获取B对象,但是此时一级缓存没有;然后我们会看B对象是否正处于创建中,显然现在B也还未开始创建,所以这个时候容器会先去创建完B对象,等拿到B对象的之后,然后再回来完成A的属性注入。
- 第四步:实例化对象 ,尝试创建对象B之前,容器还是会尝试先从缓存里面查找,然而没找到;这才真正决定进行B对象的实例化,首先会标记B正在创建中,然后调用构造方法进行实例化,再把自己放到一个ObjectFactory工厂对象里保存到三级缓存里。
- 第五步:B对象进行属性注入,进行属性注入的时候,我们发现B的属性需要依赖于A对象,到这里spring也还是会尝试从缓存获取,先查看一级缓存有没有A对象,但是此时一级缓存没有A对象;然后再看A对象是否正处于创建中(这时知道了A正处于创建中),所以就继续从二级缓存中去获取B(二级缓存也没有),最后去三级缓存里面找(此时A对象存在于三级缓存),从三级缓存里获得一个ObjectFactory,然后调用ObjectFactory.getObject()方法得到了A对象。拿到A对象后 这里会把A对象从三级缓存移出,然后把A保存到二级缓存。
- 第六步:B对象创建完成 ,拿到A对象后,spring把A对象的引用赋值给B对象的属性,然后B就完成了创建,最后会把B对象从三级缓存移出,保存到一级缓存里去,同时也会移出创建中的标记。
- 第七步:完成A对象的属性注入,这个时候,我们的代码流程会返回到第三步,容器已经拿到了B对象了,所以可以继续完成A对象的属性注入工作了。拿到B对象后,然后把B对象引用赋值给A的属性,最后同样也会把A对象从二级缓存移出,保存到一级缓存里去,同时也会移出创建中的标记。
1.4 springboot 中循环依赖是如何解决的
在2.6.0之前,Spring Boot会自动处理循环依赖的问题。2.6.0及之后的版本会默认检查循环依赖,存在该问题则会报错。
ComponentA
类注入ComponentB
类,ComponentB
类注入ComponentA
类,就会发生循环依赖的问题。
ComponentA
1 | import org.springframework.stereotype.Service; |
ComponentB
1 | import org.springframework.stereotype.Service; |
错误
现在,2.6.0 这个版本已经默认禁止 Bean 之间的循环引用, 则基于上面的代码,会报错:
1 | *************************** |
解决方法
循环依赖是指两个或更多的组件之间存在着互相依赖的关系。在Spring Boot应用程序中,循环依赖通常是由以下几种情况引起的:
- 构造函数循环依赖: 两个或更多的组件在它们的构造函数中互相依赖。
- 属性循环依赖: 两个或更多的组件在它们的属性中互相依赖。
- 方法循环依赖: 两个或更多的组件在它们的方法中互相依赖。
Spring Boot提供了一些解决循环依赖的方法:
- 构造函数注入: 在构造函数中注入依赖项,而不是在属性中注入。
- Setter注入: 使用setter方法注入依赖项,而不是在构造函数中注入。
- 延迟注入: 使用
@Lazy
注解延迟加载依赖项。 - @Autowired注解的required属性: 将
required
属性设置为false,以避免出现循环依赖问题。 - @DependsOn注解: 使用
@DependsOn
注解指定依赖项的加载顺序,以避免出现循环依赖问题
构造器注入的案例
假设有如下俩个类:
1 | public class A { |
通过构造函数注入可以避免循环依赖,改造后的代码如下:
1 | public class A { |
延迟注入的案例
假设有如下情景:
类A依赖于类B,同时类B也依赖于类A。这样就形成了循环依赖。
为了解决这个问题,可以使用@Lazy
注解,将类A或类B中的其中一个延迟加载。
例如,我们可以在类A中使用@Lazy
注解,将类A延迟加载,这样在启动应用程序时,Spring容器不会立即加载类A,而是在需要使用类A的时候才会进行加载。这样就避免了循环依赖的问题。
示例代码如下:
1 |
|
在类A中,我们使用了@Lazy
注解,将类B延迟加载。这样在启动应用程序时,Spring容器不会立即加载类B,而是在需要使用类B的时候才会进行加载。
这样就避免了类A和类B之间的循环依赖问题。
接口隔离的案例
假设有两个类A和B,它们之间存在循环依赖:
1 | public class A { |
这时候,如果直接在Spring Boot中注入A和B,就会出现循环依赖的问题。为了解决这个问题,可以使用接口隔离。
首先,定义一个接口,包含A和B类中需要使用的方法:
1 | public interface Service { |
然后,在A和B类中分别注入Service接口:
1 | public class A { |
最后,在Spring Boot中注入Service实现类:
1 |
|
通过这种方式,A和B类不再直接依赖于彼此,而是依赖于同一个接口。同时,Spring Boot也能够正确地注入A、B和ServiceImpl
,避免了循环依赖的问题。