Spring中的循环依赖解决详解
前言
说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚。本文就试着尽自己所能,对此做出一个较详细的解读。另,需注意一点,下文中会出现类的实例化跟类的初始化两个短语,为怕园友迷惑,事先声明一下,本文的实例化是指刚执行完构造器将一个对象new出来,但还未填充属性值的状态,而初始化是指完成了属性的依赖注入。
一、先说说Spring解决的循环依赖是什么
Java中的循环依赖分两种,一种是构造器的循环依赖,另一种是属性的循环依赖。
构造器的循环依赖就是在构造器中有属性循环依赖,如下所示的两个类就属于构造器循环依赖:
@Service
public class Student {
@Autowired
private Teacher teacher; public Student (Teacher teacher) {
System.out.println("Student init1:" + teacher);
} public void learn () {
System.out.println("Student learn");
}
}
@Service
public class Teacher {
@Autowired
private Student student; public Teacher (Student student) {
System.out.println("Teacher init1:" + student); } public void teach () {
System.out.println("teach:");
student.learn();
}
}
这种循环依赖没有什么解决办法,因为JVM虚拟机在对类进行实例化的时候,需先实例化构造器的参数,而由于循环引用这个参数无法提前实例化,故只能抛出错误。
Spring解决的循环依赖就是指属性的循环依赖,如下所示:
@Service
public class Teacher {
@Autowired
private Student student; public Teacher () {
System.out.println("Teacher init1:" + student); } public void teach () {
System.out.println("teach:");
student.learn();
} }
@Service
public class Student {
@Autowired
private Teacher teacher; public Student () {
System.out.println("Student init:" + teacher);
} public void learn () {
System.out.println("Student learn");
}
}
测试扫描类:
@ComponentScan(value = "myPackage")
public class ScanConfig { }
测试启动类:
public class SpringTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);
applicationContext.getBean(Teacher.class).teach();
}
}
测试类执行结果:
Student init:null
Teacher init:null
teach:
Student learn
可以看到,在构造器执行的时候未完成属性的注入,而在调用方法的时候已经完成了注入。下面就一起看看Spring内部是在何时完成的属性注入,又是如何解决的循环依赖。
二、循环依赖与属性注入
1、对于非懒加载的类,是在refresh方法中的 finishBeanFactoryInitialization(beanFactory) 方法完成的包扫描以及bean的初始化,下面就一起追踪下去。
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 其他代码
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
}
可以看到调用了beanFactory的一个方法,此处的beanFactory就是指我们最常见的那个DefaultListableBeanFactory,下面看它里面的这个方法。
2、DefaultListableBeanFactory的preInstantiateSingletons方法
public void preInstantiateSingletons() throws BeansException {
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
// Trigger initialization of all non-lazy singleton beans...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { // 判断为非抽象类、是单例、非懒加载 才给初始化
if (isFactoryBean(beanName)) {
// 无关代码(针对FactoryBean的处理)
}
else {
// 重要!!!普通bean就是在这里初始化的
getBean(beanName);
}
}
}
// 其他无关代码
}
可以看到,就是在此方法中循环Spring容器中所有的bean,依次对其进行初始化,初始化的入口就是getBean方法
3、AbstractBeanFactory的getBean跟doGetBean方法
追踪getBean方法:
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
可见引用了重载的doGetBean方法,继续追踪之:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name);
Object bean; // 方法1)从三个map中获取单例类
Object sharedInstance = getSingleton(beanName);
// 省略无关代码
}
else {
// 如果是多例的循环引用,则直接报错
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// 省略若干无关代码
try {
// Create bean instance.
if (mbd.isSingleton()) {
// 方法2) 获取单例对象
sharedInstance = getSingleton(beanName, () -> {
try { //方法3) 创建ObjectFactory中getObject方法的返回值
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
// 省略若干无关代码
return (T) bean;
}
该方法比较长,对于解决循环引用来说,上面标出来的3个方法起到了至关重要的作用,下面我们挨个攻克。
3.1) getSingleton(beanName)方法: 注意该方法跟方法2)是重载方法,名字一样内部逻辑却大相径庭。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);// 步骤A
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);// 步骤B
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);// 步骤C
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
通过上面的步骤可以看出这三个map的优先级。其中singletonObjects里面存放的是初始化之后的单例对象;earlySingletonObjects中存放的是一个已完成实例化未完成初始化的早期单例对象;而singletonFactories中存放的是ObjectFactory对象,此对象的getObject方法返回值即刚完成实例化还未开始初始化的单例对象。所以先后顺序是,单例对象先存在于singletonFactories中,后存在于earlySingletonObjects中,最后初始化完成后放入singletonObjects中。
当debug到此处时,以上述Teacher和Student两个循环引用的类为例,如果第一个走到这一步的是Teacher,则从此处这三个map中get到的值都是空,因为还未添加进去。这个方法主要是给循环依赖中后来过来的对象用。
3.2)getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法:
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 省略无关代码
beforeSingletonCreation(beanName); // 步骤A
boolean newSingleton = false;
// 省略无关代码
try {
singletonObject = singletonFactory.getObject();// 步骤B
newSingleton = true;
}
// 省略无关代码
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
afterSingletonCreation(beanName);// 步骤C
}
if (newSingleton) {
addSingleton(beanName, singletonObject);// 步骤D
}
}
return singletonObject;
}
}
获取单例对象的主要逻辑就是此方法实现的,主要分为上面四个步骤,继续挨个看:
步骤A:
protected void beforeSingletonCreation(String beanName) {
// 判断,并首次将beanName即teacher放入singletonsCurrentlyInCreation中
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
步骤C:
protected void afterSingletonCreation(String beanName) {
// 得到单例对象后,再讲beanName从singletonsCurrentlyInCreation中移除
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
}
}
步骤D:
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);//添加单例对象到map中
this.singletonFactories.remove(beanName);//从早期暴露的工厂中移除,此map在解决循环依赖中发挥了关键的作用
this.earlySingletonObjects.remove(beanName);//从早期暴露的对象map中移除
this.registeredSingletons.add(beanName);//添加到已注册的单例名字集合中
}
}
步骤B:
此处调用了ObjectFactory的getObject方法,此方法是在哪里实现的呢?返回的又是什么?且往回翻,找到3中的方法3,对java8函数式编程有过了解的园友应该能看出来,方法3 【createBean(beanName, mbd, args)】的返回值就是getObject方法的返回值,即方法3返回的就是我们需要的单例对象,下面且追踪方法3而去。
3.3)AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[]) 方法
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException { // 省略无关代码
try {
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
return beanInstance;
}
// 省略无关代码
}
去掉无关代码之后,关键方法只有doCreateBean方法,追踪之:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException { BeanWrapper instanceWrapper = null;
// 省略代码
if (instanceWrapper == null) {
// 实例化bean
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 重点!!!将实例化的对象添加到singletonFactories中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 初始化bean
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);//也很重要
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
// 省略无关代码
return exposedObject;
}
上面注释中标出的重点是此方法的关键。在addSingletonFactory方法中,将第二个参数ObjectFactory存入了singletonFactories供其他对象依赖时调用。然后下面的populateBean方法对刚实例化的bean进行属性注入(该方法关联较多,本文暂时不展开追踪了,有兴趣的园友自行查看即可),如果遇到Spring中的对象属性,则再通过getBean方法获取该对象。至此,循环依赖在Spring中的处理过程已经追溯完毕,下面我们总结一下。
小结
属性注入主要是在populateBean方法中进行的。对于循环依赖,以我们上文中的Teacher中注入了Student、Student中注入了Teacher为例来说明,假定Spring的加载顺序为先加载Teacher,再加载Student。
getBean方法触发Teacher的初始化后:
a. 首先走到3中的方法1),此时map中都为空,获取不到实例;
b. 然后走到方法2)中,步骤A、步骤C、步骤D为控制map中数据的方法,实现简单,可暂不关注。其中步骤B的getObject方法触发对方法3)的调用;
c. 在方法3)中,先通过createBeanInstance实例化Teacher对象,又将该实例化的对象通过addSingletonFactory方法放入singletonFactories中,完成Teacher对象早期的暴露;
d. 然后在方法3)中通过populateBean方法对Teacher对象进行属性的注入,发现它有一个Student属性,则触发getBean方法对Student进行初始化
e. 重复a、b、c步骤,只是此时要初始化的是Student对象
f. 走到d的时候,调用populateBean对Student对象进行属性注入,发现它有一个Teacher属性,则触发getBean方法对Teacher进行初始化;
g. 对Teacher进行初始化,又来到a,但此时map已经不为空了,因为之前在c步骤中已经将Teacher实例放入了singletonFactories中,a中得到Teacher实例后返回;
h.完成f中对Student的初始化,继而依次往上回溯完成Teacher的初始化;
完成Teacher的初始化后,Student的初始化就简单了,因为map中已经存了这个单例。
至此,Spring循环依赖的总结分析结束,一句话来概括一下:Spring通过将实例化后的对象提前暴露给Spring容器中的singletonFactories,解决了循环依赖的问题。
Spring中的循环依赖解决详解的更多相关文章
- 【Spring】Spring中的循环依赖及解决
什么是循环依赖? 就是A对象依赖了B对象,B对象依赖了A对象. 比如: // A依赖了B class A{ public B b; } // B依赖了A class B{ public A a; } ...
- 面试必杀技,讲一讲Spring中的循环依赖
本系列文章: 听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configu ...
- 面试阿里,腾讯,字节跳动90%都会被问到的Spring中的循环依赖
前言 Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃 ...
- 从一部电影史上的趣事了解 Spring 中的循环依赖问题
title: 从一部电影史上的趣事了解 Spring 中的循环依赖问题 date: 2021-03-10 updated: 2021-03-10 categories: Spring tags: Sp ...
- 一起来踩踩 Spring 中这个循环依赖的坑
1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什么要依赖注入 6. Spring的依赖注入模型 7. 非典型问题 参考资料 1. 前言 这两天工作遇到了一个挺有意思的Sp ...
- Spring中的循环依赖
循环依赖 在使用Spring时,如果主要采用基于构造器的依赖注入方式,则可能会遇到循环依赖的情况,简而言之就是Bean A的构造器依赖于Bean B,Bean B的构造器又依赖于Bean A.在这种情 ...
- Spring中@Component注解,@Controller注解详解
在使用Spring的过程中,为了避免大量使用Bean注入的Xml配置文件,我们会采用Spring提供的自动扫描注入的方式,只需要添加几行自动注入的的配置,便可以完成 Service层,Controll ...
- Spring中@Value标签的使用详解
1.@Value标签 由于Spring对通过IOC的方式对对象进行统一管理,所以对任何对象而言,其生成方法均由Spring管理.传统的方法是通过XML配置每一个Bean,并对这个Bean的所有Fiel ...
- springboot中的parent依赖作用详解
最近在阅读springboot+vue全栈开发实战,松哥编写的,虽然比较简单,各种技术没有深入讲解,但是还是可以看看的,特别是我这个前端菜鸟哈哈,以后可是要学习全栈的,把书中看到的不会的地方会记录下笔 ...
随机推荐
- 常用的re模块的正则匹配的表达式
07.01自我总结 常用的re模块的正则匹配的表达式 一.校验数字的表达式 1.数字 ^[0-9]\*$ 2.n位的数字 ^\d{n}$ 3.至少n位的数字 ^\d{n,}$ 4.m-n位的数字 ^\ ...
- CF1195C Basketball Exercise (dp + 贪心)
题解出处:https://www.luogu.org/problemnew/solution/CF1195C 很水的一道C题……目测难度在黄~绿左右.请各位切题者合理评分. 注意到可以选择的球员编号是 ...
- python基础-python解释器多版本共存-变量-常量
一.编程语言的发展史 机器语言-->汇编语言-->高级语言,学习难度及执行效率由高到低,开发效率由低到高 机器语言:二进制编程,0101 汇编语言:用英文字符来代替0101编程 高级语言: ...
- NetworkStream.Read
Reads data from the NetworkStream. 参数 buffer 类型:System.Byte[]类型 Byte 的数组,它是内存中用于存储从 NetworkStream 读取 ...
- 15款好用超赞的chrome插件, 开发者们的必备~
今天推荐一波Chrome插件干货.这些插件带给我开发效率上的提升.所以在这里整理一下,分享给朋友们. 作为一名程序开发者,推荐一波常用的chrome插件,用了就舍不得丢,包括免费FQ工具,github ...
- git远程服务器回滚
1.git log查找commit hash 2.git reset --hard hash 回滚本地git库 3.git push -f origin(git仓库的url) branch名 强制提交
- EF Core的Code First 基础
一.创建实体类与映射类 通过NuGet引用Microsoft.EntityFrameworkCore 1.创建实体类 Code First可以通过为实体类字段添加相应特性,来创建对应的字段类型等,举例 ...
- 使用阿里云对docker拉取镜像加速
使用docker的时候,总是需要去search镜像,使用国外的源下载太慢,还有诸多的限制,无意中发现可以使用阿里云进行加速,实测有用,废话少说,操作如下: 1.打开阿里云控制台,没有的可以用淘宝账号或 ...
- c#小灶——初识c#
提到c#,就不得不说.net,.net是微软开发的一个平台,简单来说,在这个平台上,可以编写.运行程序.可能很多人觉得这个平台离我们很遥远,其实不然,这个平台就一直在我们的windows操作系统里,默 ...
- H3C模拟器单臂路由配置实例
单臂路由:用802.1Q和子接口实现VLAN间路由 单臂路由利用trunk链路允许多个VLAN的数据帧通过而实现 网络拓扑图: RTA配置: SW1配置: PC1/2配置如图: 但是值得注意的是,在配 ...