Bean 的循环依赖问题 - Spring 循环依赖源码分析
Bean 的循环依赖问题 - Spring 循环依赖源码分析
老杜在课程中的 Spring 循环依赖源码分析部分,讲得有点虎头蛇尾,隔靴搔痒。
还是得自己追一遍源码呀。下面先给出相关的类和配置文件。
Husband
和 Wife
类
1 | public class Husband { |
1 | public class Wife { |
spring.xml
配置文件
1 |
|
单元测试 CircularDependencyTest
类
1 | public class CircularDependencyTest { |
从解析 spring.xml
开始
单例的 Bean 在 Spring 容器初始化时,就会被创建并进行属性的注入。下面这行代码执行完成后,huabandBean
和 wifeBean
都已经实例化并完成属性的赋值。
1 | ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); |
我们先从解析 spring.xml
开始,看看解析完成后,是在哪一步开始进行单例 Bean 的实例化的。
首先是 ClassPathXmlApplicationContext
,创建该对象时,构造函数将提供的配置文件 spring.xml
存到继承自父类的属性 configLocations
数组中,然后调用继承自父类的 refresh
方法来开始 Spring 容器的初始化。
我们重点关注 refresh
方法中以下两个部分:
1 | // refresh:553, AbstractApplicationContext |
先看 refresh
方法第一个部分。
1 | ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); |
该部分首先底层会创建一个DefaultListableBeanFactory
对象 beanFactory
,然后会找到之前存放在 configLocations
数组中的 spring.xml
,读取解析该文件中的 Bean 定义信息,并将这些信息保存到所创建的 Bean 工厂 beanFactory
对象中。
1 | // AbstractRefreshableApplicationContext |
beanFactory
对象包含两个属性 beanDefinitionMap
和 beanDefinitionNames
。在解析配置文件完成后,husbandBean
和 wifeBean
的信息就已经保存到这两个属性中。
beanDefinitionMap
:是一个 Map 集合,key 是 bean 的名字,如husbandBean
,value 是完整的 bean 定义信息,封装在类GenericBeanDefinition
中,其中包含全类名以及要注入的属性信息。beanDefinitionNames
:是一个 List 集合,存放的就是 bean 的名字。
再来看 refresh
方法的第二个部分。
1 | this.finishBeanFactoryInitialization(beanFactory); |
beanFactory
的 beanDefinitionNames
属性中已经包含了所有单例 Bean 的 id,接下来,Spring 就会根据其中的 id,尝试 getBean(beanName)
去获取相应的单例对象,如果获取不到,就会进行实例化和属性赋值。
1 | // AbstractApplicationContext |
注意,上面的 getBean(beanName)
其实只是为了触发后续的单例对象的创建和属性赋值,所以并没有对该方法的返回值进行接收。
husbandBean
和 wifeBean
是如何实例化并完成属性赋值的?
Spring 循环依赖的完整源码是比较复杂的,因此,这里只是针对最开始的问题场景,选取源码中的关键部分进行分析。
所有的分析将围绕下面的流程图展开,简单说明一下该流程图:
- 每个圆角矩阵中都包含一行关键的函数调用代码。
- 所有被调用的函数,按照其所属的类进行了垂直划分。
- 被圈在一个大方框中的多个圆角矩阵满足并列关系,这些圆角矩阵中的多个函数调用,都是另外某个函数的内容。
- 为节省空间,将
husbandBean
记为 “h”,wifeBean
记为 “w”。
DefaultSingletonBeanRegistry
中重要的 4 个属性
1 | /** Cache of singleton objects: bean name to bean instance. */ |
singletonObjects
是一级缓存,key 是 bean 的 id,value 是相应的完整单例 bean 对象。该缓存存放的是实例化并完成属性赋值的单例 Bean 对象。
earlySingletonObjects
是二级缓存,key 是 bean 的 id,value 是相应的早期单例 bean 对象。该缓存存放的是实例化但还未完成属性赋值的单例 Bean 对象。
singletonFactories
是三级缓存,key 是 bean 的 id,value 是工厂对象。该缓存存放的是能够获取早期的单例 Bean 对象的工厂对象。
singletonsCurrentlyInCreation
是一个 bean 的 id 的集合,每当准备实例化某个单例 bean 时,就会先将该单例 bean 的 id 放入该集合,表示正在创建该单例 bean。
我们后面追 Spring 源码的时候,会时刻关注这 4 个属性的值变化。
流程分析阶段一
getBean("h")
:准备获取 id 为 “h” 的单例 Bean 对象。
还记得前面的内容吗?在spring.xml
读取并解析完毕后,会有产生一个beanfactory
对象,其中的beanDefinitionNames
属性就包含了所有单例 bean id,接着 Spring 会对每个 bean id 调用getBean(beanName)
方法。在spring.xml
中,”h” 的 Bean 定义处在前面,因此会先执行getBean("h")
。doGetBean("h")
:获取 id 为 “h” 的单例 Bean 对象。
该函数中有两个关键部分,都是调用getSingleton
函数,只不过前后两次调用的是不同的重载版本。
getSingleton("h")
:尝试从缓存中获取 id 为 “h” 的单例 Bean 对象。
首先尝试从一级缓存singletonObjects
中获取 id 为 “h” 的单例 Bean 对象,但获取不到。
此时存在两种情况:要么 “h” 单例 Bean 处于创建中状态,二三级缓存存储了早期 “h” Bean 单例或者工厂对象;要么 “h” 单例 Bean 从未被创建过。
检查singletonsCurrentlyInCreation
,发现不包含 “h”,说明 “h” Bean 目前并非处于创建中。
综上可以断定,”h” 单例从未被创建过,二三级缓存肯定也没有相关对象,后续需要进行创建。getSingleton("h", () -> createBean("h"))
:尝试从一级缓存中获取 id 为 “h” 的单例 Bean 对象,获取到就直接返回,获取不到就启动 “h” Bean 单例的创建流程。这里显然是获取不到,要启动 “h” Bean 单例的创建流程。
该方法的第二个参数是要传入一个ObjectFactory
类型的工厂对象,ObjectFactory
是一个接口,其内部只有一个抽象方法getObject
。
回忆三级缓存,三级缓存的 value 就是ObjectFactory
类型。
这里相当于是,传入了创建 “h” 的单例 Bean 对象的工厂对象。那么该工厂对象,后续是否会被放到三级缓存singletonFactories
中呢?
结论是不会,后续会利用该工厂对象生产出早期单例 Bean 对象,然后创建另外一个工厂对象简单包装下该早期单例 Bean 对象,最后将这个新的工厂对象放到三级缓存中。beforeSingletonCreation("h")
:将 “h” 添加到singletonsCurrentlyInCreation
,表示要准备创建 “h” Bean 单例了。singletonFactory.getObject()
:singletonFactory
就是先前传入的工厂对象() -> createBean("h")
,这里调用getObject()
,其实就是调用createBean("h")
。createBean("h")
:准备创建 id 为 “h” 的单例 Bean 对象。doCreateBean("h")
:创建 id 为 “h” 的单例 Bean 对象。该方法包含创建单例 Bean 对象的完整过程。hBean = createBeanInstance("h")
:创建 “h” 单例 Bean 对象,底层调用无参数构造方法完成对象的创建,但此时hBean
的属性未进行填充。addSingletonFactory("h", () -> getEarlyBeanReference(hBean))
:将已经创建好的早期单例hBean
,封装到一个新的工厂对象() -> getEarlyBeanReference(hBean)
中,然后将该新工厂对象,放入三级缓存singletonFactories
中。
显然新的工厂对象底层并不执行创建工作,而是将早就创建好的hBean
返回。那么为什么不直接将hBean
放入二级缓存呢?这应该是为了兼容 AOP 所做的处理。getEarlyBeanReference(hBean)
中会检查hBean
是否进行了 AOP 代理,如果是的话,会基于hBean
返回其代理对象;否则直接返回hBean
。populateBean(hBean, mbd, instanceWrapper)
:对hBean
进行属性的注入/赋值/填充。
在spring.xml
中关于 “h” 的 Bean 定义信息,其实都包含在第二个参数mbd
中。
这里我不深入属性注入的细节,只需要知道两件事:- 底层是通过反射机制调用 setter 方法完成注入操作。
- 在注入
hBean
的第二个属性时,发现该属性的值是一个 ref 引用,因此会接着调用getBean("w")
来获取 id 为 “w” 的 Bean 进行注入。
显然,此时 id 为 “w” 的 Bean 是不存在的,类似的,
getBean("w")
接着会重新走一遍最开始getBean("h")
的流程,所以下面简单过一遍。getBean("w")
:准备获取 id 为 “w” 的单例 Bean 对象。doGetBean("w")
:获取 id 为 “w” 的单例 Bean 对象。getSingleton("w")
:尝试从缓存中获取 id 为 “w” 的单例 Bean 对象。获取不到,返回空。getSingleton("w", () -> createBean("w"))
: 启动 “w” Bean 的创建流程。传入创建 “w” 单例 Bean 的工厂对象。beforeSingletonCreation("w")
:将 “w” 添加到singletonsCurrentlyInCreation
,表示要准备创建 “w” Bean 单例了。此时singletonsCurrentlyInCreation
中既有 “h” 又有 “w”,两个 Bean 都在处于创建中。singletonFactory.getObject()
:通过先前传入的工厂对象() -> createBean("w")
,调用createBean("w")
。createBean("w")
:准备创建 id 为 “w” 的单例 Bean 对象。doCreateBean("w")
:创建 id 为 “w” 的单例 Bean 对象。该方法包含创建单例 Bean 对象的完整过程。wBean = createBeanInstance("w")
:调用无参构造方法,创建 “w” 单例 Bean 对象wBean
但属性未赋值。addSingletonFactory("w", () -> getEarlyBeanReference(wBean))
:将包装wBean
的新工厂对象,放入三级缓存singletonFactories
中。此时singletonFactories
中既有 “h” 的工厂对象,又有 “w” 的工厂对象。populateBean(wBean, mbd, instanceWrapper)
:对wBean
进行属性的注入。
在注入wBean
的第二个属性时,会调用getBean("h")
。getBean("h")
:wBean
注入需要获取 “h” Bean。doGetBean("h")
:wBean
注入需要获取 “h” Bean。getSingleton("h")
:尝试从缓存中获取 id 为 “h” 的单例 Bean 对象。
首先依然是从一级缓存singletonObjects
中获取,但是获取不到。
检查singletonsCurrentlyInCreation
发现 “h” 处于创建中,因此去二三级缓存中获取。
从二级缓存earlySingletonObjects
中获取,获取不到。
从三级缓存singletonFactories
中获取,能获取到之前存储的() -> getEarlyBeanReference(hBean)
工厂对象,工厂对象调用getObject()
即返回早就创建好的早期 Bean 对象hBean
。
将hBean
放入二级缓存earlySingletonObjects
中,并删除三级缓存singletonFactories
中的工厂对象。
最后,返回hBean
,注意目前其属性还未注入!
流程分析阶段二
在 25. getSingleton("h")
中拿到的 hBean
一路向上返回,一直回到 22. populateBean(wBean, mbd, instanceWrapper)
,将获取到的 hBean
注入到 wBean
的第二个属性 husband
中,自此,wBean
的两个属性都已经完成赋值,”w” 单例 Bean 对象 wBean
算是彻底完成创建了。
afterSingletonCreation("w")
:既然wBean
彻底完成创建了,就从singletonsCurrentlyInCreation
中移除掉 “w”。addSingleton("w", wBean)
:既然wBean
彻底完成创建了,是一个完整的 Bean 对象了,就需要将wBean
加入到一级缓存singletonObjects
中。wBean
加入一级缓存singletonObjects
后,会到二三级缓存中删除 “w” 相关的对象。
此时只有三级缓存singletonFactories
中含有 “w” 的工厂对象,将其从三级缓存中移除。
wBean
已经彻底完成创建了,不要忘记,我们最开始的目的是为了获取 wBean
,获取到之后就可以完成 hBean
中第二个属性 wife
的注入。w
一路向上返回,回到 11. populateBean(hBean, mbd, instanceWrapper)
,将获取到的 wBean
注入到 hBean
中。自此 hBean
的两个属性都已经完成赋值,”h” 单例 Bean 对象 hBean
算是彻底完成创建了。
afterSingletonCreation("h")
:既然hBean
彻底完成创建了,就从singletonsCurrentlyInCreation
中移除掉 “h”。
addSingleton("h", hBean)
:既然hBean
彻底完成创建了,是一个完整的 Bean 对象了,就需要将hBean
加入到一级缓存singletonObjects
中。hBean
加入一级缓存singletonObjects
后,会到二三级缓存中删除 “h” 相关的对象。
此时只有二级缓存earlySingletonObjects
中含有相关对象hBean
,将其从二级缓存中移除。这个hBean
和刚放入一级缓存中的hBean
其实是同一个。
hBean
已经彻底完成创建了,该 hBean
会一直向上返回,直到 1. getBean("h")
。还记得这个 getBean("h")
是哪里调用的吗?
1 | public void preInstantiateSingletons() throws BeansException { |
之前就说过,这里 getBean(beanName)
并没有使用变量接收返回值,因为目的就只是为了触发单例对象的创建和属性赋值。
beanDefinitionNames
中包含两个值 {"h", "w"}
。getBean("h")
的执行总共经历了上面这一长串流程,走完这写流程后,一级缓存 singletonObjects
已经存储了相应的单例对象 hBean
和 wBean
。因此,紧接着 getBean("w")
的执行就相对简单了:getBean("w")
到 doGetBean("w")
最后到 getSingleton("w")
从一级缓存中拿到 wBean
就结束了。
关于 populateBean
中属性注入的一个小细节
populateBean(hBean, mbd, instanceWrapper)
方法中,对 hBean
进行属性 name
和 wife
的注入时,是先将值 “张三” 注入到 name
中,然后再 getBean("w")
吗?
换句话说就是,在调用 getBean("w")
时,hBean
中的 name
属性是否已经赋上值 “张三” 了?
结论是并没有赋上值,Spring 的处理逻辑是,将所有属性的值,都获取到以后,再一次性地进行属性注入操作。
详情可以看,AbstractAutowireCapableBeanFactory
中的 applyPropertyValues
方法,Spring 将对每个属性,都先获取到其值,然后封装为一个 PropertyValue
对象存入到 List<PropertyValue> deepCopy
中。所有属性解析完成后,deepCopy
就保存了所有的属性和对应的属性值,此时再根据 deepCopy
进行属性注入。
写在最后
Spring 虽然在 singleton + set 注入模式下,能够解决循环依赖问题,但在平常开发中,应该尽可能避免循环依赖的产生。
Spring 的 Bean 管理,一直是整个体系中津津乐道的东西。尤其是 Bean 的循环依赖,更是很多面试官最喜欢考察的 2B 知识点之一。
但事实上,项目中存在 Bean 的循环依赖,是代码质量低下的表现。多数人寄希望于框架层来给擦屁股,造成了整个代码的设计越来越糟,最后用一些奇技淫巧来填补犯下的错误。
还好,SpringBoot 终于受不了这种滥用,默认把循环依赖给禁用了!
从 2.6 版本开始,如果你的项目里还存在循环依赖,SpringBoot 将拒绝启动!
评论区有人提到了平常开发中可能会遇到的循环依赖的场景,其他人给出了解决方案:
vishun:想请教下,如果我想在订单中查下此订单下的用户信息,在用户中查询下用户所下订单信息,如果不循环依赖的话,要怎么处理比较好呢?再新建个类?
KNORRIG:业务 service 注入用户 service 和订单 service 不就可以了吗。干嘛非要弄脏 UserService 和 OrderService 呢,除非你们架构对 UserService 和 OrderService 做了很清晰的职责划分,否则你的需求最好是第三方业务中的 Service 里面聚合好再返回
参考博客
- 高频面试题:Spring 如何解决循环依赖? - 皮皮Q的文章 - 知乎:非常适合初步了解,没有太深入的涉及源码。
学习完 AOP 相关的内容后,可以回过头来看看下面的博客。
- 面试必杀技:讲一讲Spring中的循环依赖:感觉很详细,没看但先 mark
- Spring 是如何解决循环依赖的? - Java3y的回答 - 知乎:感觉很详细,没看但先 mark
- 逐行解读Spring(五)- 没人比我更懂循环依赖!:没看完但先 mark
- Spring的循环依赖,学就完事了【附源码】:没看完但先 mark