渐进分析Bean的循环依赖问题:依赖注入方式,三层缓存,lazy注解
什么是循环依赖问题
- 就是有两个或两个以上的Bean互相引用(依赖)对方,造成了
循环闭环
。 - 出现循环依赖问题时尽量想办法消除循环依赖,消除不了再想办法进行规避处理。
规避处理
- 有两种:
Spring自动处理
,三层缓存,需要开启配置@Lazy注解
,需要添加在属性上或是构造函数上,而不是类上。
Spring 自动处理
开启循环依赖的支持
//在Spring boot配置文件设置
spring.main.allow-circular-references: true
注:Spring Boot 2.6.0之后,默认为false
,即不支持循环依赖,这种情况下,出现循环依赖就会报错如下错误:
不可以解决的场景
多例注入
依赖双方都为构造器注入
- 依赖注入分类:
- 单例和多例
- 属性注入(Field注入), Setter注入,构造器注入
单例与多例
-
单例:
- 实例化一次,所有对该bean的调用都会使用这唯一实例。
- Spring容器管理该实例,容器在该实例就在。
-
多例:
- 对该bean的每次调用都会创建一个新的bean实例。
- Spring容器负责实例的创建,调用方调用后,容器就不管了,由调用方进行该实例生命周期的维护。
-
Bean的单例和多例是通过
scope
属性或Scope注解设置的,默认为单例。- XML文件创建Bean时使用属性设置多例
<bean id="UserService" class="com.service.HelloSerevice" scope="prototype" />
- 注解方式创建bean时使用Scope注解设置多例
@Component
@Scope("prototype")
public class UserService{
}
依赖注入方式
属性注入/Field注入
public class UserController{
@Autowired
//@Resource @Inject
private UserService userService;
}
优点:简单,所以最常用。
补充:
1. @Autowired
是Spring提供的注解,默认是按照byType的方式注入,要使用byName方式,需要添加注解@Qualifiler
2. @Resource
是JDK提供的,默认按照byName的方式注入,其提供两个属性,name和type供选择。
3. @Inject
是JDK提供的,默认按照ByType方式注入。
Setter注入
public class SimpleMovieLister {
private MovieFinder movieFinder;
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
- 优点:
- 重新注入:即在运行时修改,重新注入参数。
构造器注入
public class SimpleMovieLister {
private final MovieFinder movieFinder;
SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
-
这是Spring官方推荐的构造器,其有以下优点:
- 注入的组件不可变。
- 确保依赖项不为空。
- 构造器注入的组件是
完全初始化
的状态。
-
官网上对于setter和构造器注入方式的对比。
- 更推荐构造器注入。
- 构造器适用于强制依赖注入。setter使用于可选依赖注入(当添加@Autowired注解后就变成了必需的依赖注入。不需要一次性提供所有依赖。
- setter注入需要进行非空检查。
- setter注入支持对象的动态重新配置。
实现方式-三级缓存
Spring Bean的创建流程
- doCreateBean()方法 主要分为3步
createBeanInstance()
实例化,调用对象的构造方法实例化对象。populateBean()
填充属性,如类中的使用@Autowired注解的bean对象。循环依赖发生在这一步。initializeBean()
初始化。
使用缓存解决循环依赖问题。
假设A,B类互相依赖注入。
- 创建A的时候,
createBeanInstance(A)
->populateBean(A)
这个时候需要获取Bean B了,BcreateBeanInstance(B)
->populateBean(B)
也需要获取BeanA, 就造成循环了。 - 那么如果我们在创建A第一步的时候就将半成品A(
createBeanInstance(A)
结果)放在缓存中,创建B的时候调用这个半成品A,就没有循环了。
为什么多例和构造器注入不能被解决。
构造器注入
:- 在非构造方法中,半成品bean的生成即实例化,是通过反射调用无参构造方法的,现在自己定义了构造方法就不行了。
- 这种情况下,在生成半成品之前就会创建依赖的bean,那么依赖的bean就会找不到缓存,造成循环。
- 也就是说一个是构造器注入,另一个是非构造器注入是可以的,系统可以先创建非构造器注入的bean,生成缓存,再创建构造器注入的bean。
多例
情况下B不会从缓存中获取A,而是会创建一个全新的A,所以会进入死循环。
什么是三层缓存
- 通过上述分析,我们知道了使用缓存保存bean的早期对象(没有进行属性填充)就可以解决循环依赖问题了。
- 但实际上,Spring采用的是3级循环:
- 第一级缓存:
singletonObjects
:用于存放完全初始化好的bean。必不可少。 - 第二级缓存:
earlySingletonObjects
:用于存放原始的bean对象,即半成品bean或半成品bean的代理对象(没有填充属性) - 第三级缓存:
singletonFactories
: 存放bean工厂对象,存放半成品bean。
- 第一级缓存:
循环依赖时3层缓存的使用流程。
- 普通循环依赖:
- a实例化完成之后(a成为半成品),将a放入三级缓存
- 给a填充属性b,又去创建b
- b实例化完成之后(b成为半成品),将b放入三级缓存
- 给b填充属性a,又去从容器中获取a
- 此时可以从三级缓存中查到a。将半成品a放入二级缓存,并从三级缓存中移除a。最终返回半成品a。开始回溯。
- b的创建过程回溯完之后,b成为正品,将b从三级缓存中移除,将b放入一级缓存。而a还是半成品。(b中的属性a的属性b还是null,即:b.a.b=null)
- 将b返回给a,然后a进行第三步初始化。,a也成为正品,将a放入一级缓存,并从二级缓存中移除。循环依赖已解决,a和b均创建成功。
注:半成品bean会先放在三级缓存中,再被使用时会放在二级缓存,变成成品时会放入一级缓存
。
- 增加AOP之后:
5. 此时从从三级缓存中查到a,然后生成a的代理对象,将a的代理对象放在第二级循环中。
6. b填充后属性之后,初始化时创建b的代理对象,将b从3层缓存中删除,然后将b的代理对象放入一级缓存。
7. b创建完后返回给a,然后a进行初始化,将a的代理对象从二级缓存中删去,然后放在一级缓存中。
注:这时候A的代理对象是在a还是半成品的时候进行的,B的代理对象是在b变成成品的时候创建的
非要3级缓存吗
-
没有AOP,那么只需要两级就够了
- 第一级必不可少,存放完全初始化好的bean。
- 第二级存放半成品bean就可以了。不需要3层
-
有AOP功能,那么需要3级
- 第一级依旧必不可少
- 第二级存放代理对象。
- 第三级存放半成品bean。
-
有AOP,也可以通过设计不需要第三级缓存,那就是将b的代理对象也提前,在其是半成品时进行创建。 但Spring没有这样做。因为其原则是代理对象尽量推迟创建。
@lazy解决循环依赖
- lazy可以解决循环依赖,但其不能放在类名上。而应该放在依赖注入的地方。如:
@Lazy
A(C b){
this.b = b;
}
@Lazy
@Autowired
private A a;
- lazy可以解决依赖双方都为构造器注入的场景
- lazy可以解决多例情况
- 原理: 就是加了Lazy注解后,创建bean A的时候就不会创建依赖的bean B,而是在使用的时候再创建,这个时候A已经创建好了,所以就不会出现循环依赖。
- lazy是通过动态代理完成的,创建bean A的时候就不会创建依赖的bean B指的是创建A的时候只会创建B的代理对象,不会触发注入对象的加载。
参考文献
[Spring中Bean的单例和多例简单总结]https://blog.csdn.net/u014644574/article/details/103308742
https://cloud.tencent.com/developer/article/1497692
https://www.cnblogs.com/lvdeyinBlog/p/15178226.html
https://juejin.cn/post/7301242196887027721
https://blog.csdn.net/qq_43290318/article/details/114679612
Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗 - 青石路 - 博客园 (cnblogs.com)
【Spring源码三千问】@Lazy原理分析——它为什么可以解决特殊的循环依赖问题?_@lazy为什么能解决循环依赖-CSDN博客