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