1、版本说明
1. 说明
关于循环依赖,SpringBoot 在 2.6.x 版本有两个改动
循环依赖是否开启的控制变量位置变动
- 从原本的
AbstractAutowireCapableBeanFactory类移动到了SpringApplication中默认值从 true 改为无默认值,但是默认的赋值是 false
// 2.5.x 及以前
class AbstractAutowireCapableBeanFactory
private boolean allowCircularReferences = true;
// 2.6.x 及以后
class SpringApplication
private boolean allowCircularReferences;
虽然没有默认值,但是通过 debug,可以看到默认启动时,默认赋值为 false
2、开关配置
// 2.6.x 之后
spring.main.allow-circular-references = true
3、为何改动
2.6.x 之后 SpringBoot 默认禁止循环依赖(Spring 并没有),因为认为循环依赖的出现本身就意味着项目存在设计问题,不应该容忍循环依赖的存在
二、循环依赖Bean创建流程
步骤说明(图示说明)
// 例子
class A {
@Autowired
private B b;
}
class B {
@Autowired
private A a;
}
-
步骤 1:通过 A 的构造器构建原生对象 A,并将其放入三级缓存,之后进行属性注入
-
步骤 3:分别从一级、二级、三级缓存中尝试获取(当然没有啦),于是开始通过 B 的构造器构建原生对象 B,并将其放入三级缓存,之后进行属性注入
-
步骤 4 和 步骤 5:从三级缓存中可以找到原生对象 A,然后对原生对象 A 进行 AOP 处理,生成代理对象 ProxyA,将其放入二级缓存,注入给原生对象 B
- 将 ProxyA 放入二级缓存并将其从三级缓存中删除
-
步骤 6:原生对象 B 属性注入完成,开始进行 AOP 处理,生成代理对象 ProxyB,放入一级缓存并返回给原生对象 A 进行属性注入
-
步骤 8:原生对象 A 从二级缓存中取出自己的代理对象 ProxyA,放入一级缓存
一级、二级、三级缓存列表展示(A代表 People,B代表 User)
三、循环依赖有哪些
以下都是说单例,非单例循环依赖无法解决
普通 + 普通、普通 AOP + 普通 AOP:依靠 spring 三级缓存解决
普通 + 构造器、普通 AOP + 构造器 AOP:通过 @DependsOn 注解决定加载优先级解决
构造器 + 构造器:通过 @Lazy 解决
四、字段注入 + 构造器注入解决方案
解决方案
-
【不推荐】构造器注入的 Bean 名字在字典中靠前
- 例如 People 比 User 靠前
-
使用
@DependsOn(String[] beanName)让指定 bean 加载优先级高于当前 bean -
反向操作【也不推荐】:使用
@Lazy懒加载,可以理解为降低了优先级,让当前 Bean 低于其他 Bean 加载
说明
字段注入 + 构造器注入进行循环依赖是否会报错取决于两个 Bean 的加载顺序
当字段注入的 Bean 先行加载,则不会报错
- 字段注入的 Bean 先行加载,在属性注入前已经将自己的原生 Bean 放入三级缓存,构造器注入可以取到原生对象进行注入,所以可以正常注入
当构造器注入的 Bean 先行加载就会报错
- 构造器注入的 Bean 先行加载,未进行到三级缓存,导致字段注入的 Bean 加载时无法获取原生对象而报错
Bean 的加载顺序:取决于 Bean Name 和优先级
-
优先级高的先行加载,通过
@DependsOn(String[] beanName)注解提高优先级 -
Bean Name 通过字典排序,靠前的优先加载
- 例如 A Bean 优先于 B Bean 加载
示例
@Component
@AllArgsConstructor
@DependsOn({"user"})
public class People {
private User user;
}
@Component
public class User {
@Autowired
private People people;
}
// 如果上述 People 不加 @DependsOn,则会导致循环依赖异常
五、构造器注入 + 构造器注入解决方案
解决方案
让参与循环依赖的 Bean 的相关构造器上加入 @Lazy(其中一个构造器加上就可以) 可以解决构造器注入造成的循环依赖问题
说明
构造器加入 @Lazy 不会导致懒加载,只会让注入参数变为代理对象
@Lazy 底层原理
本质是为注解类生成代理类,当注入时,注入的是该类的代理类,所以可以正常通过构造器生成代理对象,从而依赖三级缓存完成注入
即引入一个第三方,来打破循环依赖
示例
@Component
public class User {
private People people;
@Lazy
public User(People people) {
this.people = people;
}
}
@Component
public class People {
private User user;
public People(User user) {
this.user = user;
}
}