解决SpringBoot循环依赖

2023-03-27
预计阅读时间:3分钟

概念引入

循环依赖:是指两个或多个模块之间相互依赖,形成一个循环的依赖关系。当这种情况发生时,编译器无法确定哪个模块应该先被编译,导致编译错误或者程序运行出现异常。循环依赖通常会在软件设计或者系统架构中出现,需要采取相应的解决方案来避免或者处理它们。

本文部分内容基于Chat-GPT完成

发生原理

当Spring的context开始加载所有beans的时候,它尝试按照某种顺序去创建这些beans,从而使得他们能完全工作。例如,如果我们没有循环依赖的话,可能会有如下的案例:

Bean A -> Bean B -> Bean C

这样Spring会先创建bean C,然后Spring再创建Bean B(同时把C注入到B中),然后再创建Bean A(同时把bean B注入到A中)。

但是,如果发生循环依赖,他们彼此依赖,导致Spring无法决定哪一个bean最先被创建。在这样的情况下,Spring将在加载context时产生一个BeanCurrentlyInCreationException

循环依赖只会在Spring使用 构造器注入(constructor injection)的时候才会产生;如果你使用其他类型的注入方式,这些bean只会在被调用的时候才加载到context中,这样你就不会遇到循环依赖的问题。

场景复现

在SpringBoot项目中复现循环依赖的场景:

ServiceA.java

@Service
public class ServiceA {
 
    private ServiceB serviceB;
 
    @Autowired
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

ServiceB.java

@Service
public class ServiceB {
 
    private ServiceA serviceA;
 
    @Autowired
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

此时,在启动类启动过程中控制台会输出下列信息提示:

***************************
APPLICATION FAILED TO START
***************************

Description:

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

┌─────┐
|  serviceA defined in file [/Users/defchan/Documents/quiz/target/classes/com/devon/quiz/service/ServiceA.class]
↑     ↓
|  serviceB defined in file [/Users/defchan/Documents/quiz/target/classes/com/devon/quiz/service/ServiceB.class]
└─────┘


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.


Process finished with exit code 1

解决方法

  • 方式1:配置文件添加配置项(使用属性注入情况下可生效)

application.yml中添加以下配置项:

spring:
  main:
    allow-circular-references: true

添加上述配置项后,Spring不再抛出循环依赖的错误

  • 方式2:使用@Lazy注解

这里有一个简单的方法来打破这种循环,就是告诉Spring 在加载context之后再初始化Beans,而不是完全初始化这个bean,使用@Lazy时,Spring将创建一个代理bean注入到其他bean中,这种注入只有在bean第一次调用时才会被完全生效。

为了测试@Lazy注解,需要将上述代码修改为以下的形式:

ServiceA.java:

@Service
public class ServiceA {
 
    private ServiceB serviceB;
 
    @Autowired
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

ServiceB.java:

@Service
public class ServiceB {
 
    private ServiceA serviceA;
 
    @Autowired
    public ServiceB(@Lazy ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

进过这次修改,可以发现能够正常启动了

  • 方式3:使用Setter/Field注入

使用Setter注入是Spring官方文档推荐的,也是最受欢迎的。

你只需要把原来使用构造器注入(@Resource @AutoWired)的方式. 替换成setter注入(或field注入),就会解决问题,这样bean只会在被调用时才会被注入。

如下是使用Setter注入的例子:

ServiceA.java:

@Service
public class ServiceA {
 
    private ServiceB serviceB;
 
    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
 
    public ServiceB getServiceB() {
        return serviceB;
    }
}

ServiceB.java:

@Service
public class ServiceB {
 
    private ServiceA serviceA;
 
    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
 
    public ServiceA getServiceA() {
        return serviceA;
    }
}