⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 jayliu.blog.csdn.net/article/details/129972669 「熊猫Jay」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

什么是循环依赖?

循环依赖是指在Spring Boot 应用程序中,两个或多个类之间存在彼此依赖的情况,形成一个循环依赖链。

在这种情况下,当一个类在初始化时需要另一个类的实例,而另一个类又需要第一个类的实例时,就会出现循环依赖问题。这会导致应用程序无法正确地初始化和运行,因为Spring Boot 无法处理这种循环依赖关系。

问题及症状

在2.6.0之前,Spring Boot会自动处理循环依赖的问题。2.6.0及之后的版本会默认检查循环依赖,存在该问题则会报错。

ComponentA类注入ComponentB类,ComponentB类注入ComponentA类,就会发生循环依赖的问题。

ComponentA

import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class ComponentA {

@Resource
private ComponentB componentB;

}

ComponentB

import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class ComponentB {

@Resource
private ComponentA componentA;

}

错误

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

***************************
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注解指定依赖项的加载顺序,以避免出现循环依赖问题

构造器注入的案例

假设有以下两个类:

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;
}
}

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

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 实例传递给 A 的构造函数即可,不需要再通过 setter 方法将 B 实例注入到 A 中。同理,在创建 B 实例时,只需要将 A 实例传递给 B 的构造函数即可,不需要再通过 setter 方法将 A 实例注入到 B 中。这样可以避免循环依赖。

延迟注入的案例

假设有如下情景:

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

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

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

示例代码如下:

@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,它们之间存在循环依赖:

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类中需要使用的方法:

public interface Service {
void doSomething();
}

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

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实现类:

@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,避免了循环依赖的问题。

文章目录
  1. 1. 什么是循环依赖?
  2. 2. 问题及症状
    1. 2.1. ComponentA
    2. 2.2. ComponentB
    3. 2.3. 错误
  3. 3. 解决方法
    1. 3.1. 构造器注入的案例
    2. 3.2. 延迟注入的案例
    3. 3.3. 接口隔离的案例