如何解决spring中的循环依赖

如何解决spring循环依赖:

通俗来讲,循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套使用)

举个例子:

1
2
3
4
5
6
7
public class AService {
private BService bService;
}

public class BService {
private AService aService;
}

上述例子中 AService 依赖了 BServiceBService 也依赖了 AService,这就是两个对象之间的相互依赖。当然循环依赖还包括 自身依赖、多个实例之间相互依赖

1.1 Bean的创建步骤

为了能更好的展示出现循环依赖问题的环节,所以这里的 Bean 创建步骤做了简化:

  1. 在创建 Bean 之前,Spring 会通过扫描获取 BeanDefinition。
  2. BeanDefinition就绪后会读取 BeanDefinition 中所对应的 class 来加载类。
  3. 实例化阶段:根据构造函数来完成实例化 (未属性注入以及初始化的对象 这里简称为 原始对象
  4. 属性注入阶段:对 Bean 的属性进行依赖注入 (这里就是发生循环依赖问题的环节
  5. 如果 Bean 的某个方法有AOP操作,则需要根据原始对象生成代理对象
  6. 最后把代理对象放入单例池(一级缓存singletonObjects)中。

1.2 为什么 SpringBean 会产生循环依赖

循环依赖是指两个或多个组件(通常是类或模块)之间相互引用,形成一个环路的情况。在编程中,循环依赖通常被视为一种不良的实践,因为它可能导致代码的可维护性下降,以及运行时的问题。

例如,在Spring框架中,循环依赖可能发生在以下几种情况下:

  1. 构造函数循环依赖:两个或更多的组件在它们的构造函数中互相依赖。
  2. 属性循环依赖:两个或更多的组件在它们的属性中互相依赖。

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利用三级缓存解决循环依赖

  1. 三级缓存Spring 使用三级缓存来提前曝光Bean对象,从而解决循环依赖。这三级缓存位于DefaultListableBeanFactory的父类DefaultSingletonBeanRegistry中,包括三个Map

  2. 解决方案

    当两个Bean相互引用时,Spring的解决过程如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class ObjectA {
    @Autowired
    private ObjectB b;
    }
    @Component
    public class ObjectB {
    @Autowired
    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
2
3
4
5
6
7
8
9
10
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class ComponentA {

@Resource
private ComponentB componentB;

}

ComponentB

1
2
3
4
5
6
7
8
9
10
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class ComponentB {

@Resource
private ComponentA componentA;

}

错误

现在,2.6.0 这个版本已经默认禁止 Bean 之间的循环引用, 则基于上面的代码,会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
| componentA
↑ ↓
| componentB
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

解决方法

循环依赖是指两个或更多的组件之间存在着互相依赖的关系。在Spring Boot应用程序中,循环依赖通常是由以下几种情况引起的:

  • 构造函数循环依赖: 两个或更多的组件在它们的构造函数中互相依赖。
  • 属性循环依赖: 两个或更多的组件在它们的属性中互相依赖。
  • 方法循环依赖: 两个或更多的组件在它们的方法中互相依赖。

Spring Boot提供了一些解决循环依赖的方法:

  • 构造函数注入: 在构造函数中注入依赖项,而不是在属性中注入。
  • Setter注入: 使用setter方法注入依赖项,而不是在构造函数中注入。
  • 延迟注入: 使用@Lazy注解延迟加载依赖项。
  • @Autowired注解的required属性:required属性设置为false,以避免出现循环依赖问题。
  • @DependsOn注解: 使用@DependsOn注解指定依赖项的加载顺序,以避免出现循环依赖问题

构造器注入的案例

假设有如下俩个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class A {
private B b;

public A() {
// ...
}

public void setB(B b) {
this.b = b;
}
}

public class B {
private A a;

public B() {
// ...
}

public void setA(A a) {
this.a = a;
}
}

通过构造函数注入可以避免循环依赖,改造后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class A {
private B b;

public A(B b) {
this.b = b;
}
}

public class B {
private A a;

public B(A a) {
this.a = a;
}
}

延迟注入的案例

假设有如下情景:

类A依赖于类B,同时类B也依赖于类A。这样就形成了循环依赖。

为了解决这个问题,可以使用@Lazy注解,将类A或类B中的其中一个延迟加载。

例如,我们可以在类A中使用@Lazy注解,将类A延迟加载,这样在启动应用程序时,Spring容器不会立即加载类A,而是在需要使用类A的时候才会进行加载。这样就避免了循环依赖的问题。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class A {

private final B b;

public A(@Lazy B b) {
this.b = b;
}

//...
}

@Component
public class B {

private final A a;

public B(A a) {
this.a = a;
}

//...
}

在类A中,我们使用了@Lazy注解,将类B延迟加载。这样在启动应用程序时,Spring容器不会立即加载类B,而是在需要使用类B的时候才会进行加载。

这样就避免了类A和类B之间的循环依赖问题。

接口隔离的案例

假设有两个类A和B,它们之间存在循环依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
private final B b;
public A(B b) {
this.b = b;
}
}

public class B {
private final A a;
public B(A a) {
this.a = a;
}
}

这时候,如果直接在Spring Boot中注入A和B,就会出现循环依赖的问题。为了解决这个问题,可以使用接口隔离。

首先,定义一个接口,包含A和B类中需要使用的方法:

1
2
3
public interface Service {
void doSomething();
}

然后,在A和B类中分别注入Service接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
private final Service service;
public A(Service service) {
this.service = service;
}
}

public class B {
private final Service service;
public B(Service service) {
this.service = service;
}
}

最后,在Spring Boot中注入Service实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class ServiceImpl implements Service {
private final A a;
private final B b;
public ServiceImpl(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void doSomething() {
// ...
}
}

通过这种方式,A和B类不再直接依赖于彼此,而是依赖于同一个接口。同时,Spring Boot也能够正确地注入A、B和ServiceImpl,避免了循环依赖的问题。