Bean 的循环依赖问题 - Spring 循环依赖源码分析

老杜在课程中的 Spring 循环依赖源码分析部分,讲得有点虎头蛇尾,隔靴搔痒。

还是得自己追一遍源码呀。下面先给出相关的类和配置文件。

HusbandWife

1
2
3
4
5
6
7
8
9
10
public class Husband {
private String name;
private Wife wife;
public void setName(String name) {
this.name = name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Wife {
private String name;
private Husband husband;
public void setName(String name) {
this.name = name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}

}

spring.xml 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>

<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>

</beans>

单元测试 CircularDependencyTest

1
2
3
4
5
6
7
8
public class CircularDependencyTest {
@Test
public void testCD() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
}
}

从解析 spring.xml 开始

单例的 Bean 在 Spring 容器初始化时,就会被创建并进行属性的注入。下面这行代码执行完成后,huabandBeanwifeBean 都已经实例化并完成属性的赋值。

1
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

我们先从解析 spring.xml 开始,看看解析完成后,是在哪一步开始进行单例 Bean 的实例化的。

首先是 ClassPathXmlApplicationContext,创建该对象时,构造函数将提供的配置文件 spring.xml 存到继承自父类的属性 configLocations 数组中,然后调用继承自父类的 refresh 方法来开始 Spring 容器的初始化。

我们重点关注 refresh 方法中以下两个部分:

1
2
3
4
5
6
7
// refresh:553, AbstractApplicationContext
public void refresh() throws BeansException, IllegalStateException {
// 底层完成了 spring.xml 文件的读取
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
// 底层完成了单例 bean 的创建
this.finishBeanFactoryInitialization(beanFactory);
}

先看 refresh 方法第一个部分。

1
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();

该部分首先底层会创建一个DefaultListableBeanFactory 对象 beanFactory,然后会找到之前存放在 configLocations 数组中的 spring.xml,读取解析该文件中的 Bean 定义信息,并将这些信息保存到所创建的 Bean 工厂 beanFactory 对象中。

1
2
3
4
5
6
// AbstractRefreshableApplicationContext
protected final void refreshBeanFactory() throws BeansException {
DefaultListableBeanFactory beanFactory = this.createBeanFactory(); // 创建 BeanFactory
this.loadBeanDefinitions(beanFactory); // 解析配置文件
this.beanFactory = beanFactory;
}

beanFactory 对象包含两个属性 beanDefinitionMapbeanDefinitionNames。在解析配置文件完成后,husbandBeanwifeBean 的信息就已经保存到这两个属性中。

  • beanDefinitionMap:是一个 Map 集合,key 是 bean 的名字,如 husbandBean,value 是完整的 bean 定义信息,封装在类 GenericBeanDefinition 中,其中包含全类名以及要注入的属性信息。
  • beanDefinitionNames:是一个 List 集合,存放的就是 bean 的名字。

再来看 refresh 方法的第二个部分。

1
this.finishBeanFactoryInitialization(beanFactory);

beanFactorybeanDefinitionNames 属性中已经包含了所有单例 Bean 的 id,接下来,Spring 就会根据其中的 id,尝试 getBean(beanName) 去获取相应的单例对象,如果获取不到,就会进行实例化和属性赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AbstractApplicationContext
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
beanFactory.preInstantiateSingletons();
}

// DefaultListableBeanFactory
public void preInstantiateSingletons() throws BeansException {
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

for (String beanName : beanNames) {
if (!isFactoryBean(beanName)) {
getBean(beanName);
}
}
}

注意,上面的 getBean(beanName) 其实只是为了触发后续的单例对象的创建和属性赋值,所以并没有对该方法的返回值进行接收。

husbandBeanwifeBean 是如何实例化并完成属性赋值的?

Spring 循环依赖的完整源码是比较复杂的,因此,这里只是针对最开始的问题场景,选取源码中的关键部分进行分析。

所有的分析将围绕下面的流程图展开,简单说明一下该流程图:

  • 每个圆角矩阵中都包含一行关键的函数调用代码。
  • 所有被调用的函数,按照其所属的类进行了垂直划分。
  • 被圈在一个大方框中的多个圆角矩阵满足并列关系,这些圆角矩阵中的多个函数调用,都是另外某个函数的内容。
  • 为节省空间,将 husbandBean 记为 “h”,wifeBean 记为 “w”。

DefaultSingletonBeanRegistry 中重要的 4 个属性

1
2
3
4
5
6
7
8
9
10
11
/** Cache of singleton objects: bean name to bean instance. */
Map<String, Object> singletonObjects

/** Cache of early singleton objects: bean name to bean instance. */
Map<String, Object> earlySingletonObjects

/** Cache of singleton factories: bean name to ObjectFactory. */
Map<String, ObjectFactory<?>> singletonFactories

/** Names of beans that are currently in creation. */
Set<String> singletonsCurrentlyInCreation

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 个属性的值变化。

流程分析阶段一

  1. getBean("h"):准备获取 id 为 “h” 的单例 Bean 对象。
    还记得前面的内容吗?在 spring.xml 读取并解析完毕后,会有产生一个 beanfactory 对象,其中的 beanDefinitionNames 属性就包含了所有单例 bean id,接着 Spring 会对每个 bean id 调用 getBean(beanName) 方法。在 spring.xml 中,”h” 的 Bean 定义处在前面,因此会先执行 getBean("h")

  2. doGetBean("h"):获取 id 为 “h” 的单例 Bean 对象。
    该函数中有两个关键部分,都是调用 getSingleton 函数,只不过前后两次调用的是不同的重载版本。

  1. getSingleton("h"):尝试从缓存中获取 id 为 “h” 的单例 Bean 对象。
    首先尝试从一级缓存 singletonObjects 中获取 id 为 “h” 的单例 Bean 对象,但获取不到。
    此时存在两种情况:要么 “h” 单例 Bean 处于创建中状态,二三级缓存存储了早期 “h” Bean 单例或者工厂对象;要么 “h” 单例 Bean 从未被创建过。
    检查 singletonsCurrentlyInCreation,发现不包含 “h”,说明 “h” Bean 目前并非处于创建中。
    综上可以断定,”h” 单例从未被创建过,二三级缓存肯定也没有相关对象,后续需要进行创建。

  2. getSingleton("h", () -> createBean("h")):尝试从一级缓存中获取 id 为 “h” 的单例 Bean 对象,获取到就直接返回,获取不到就启动 “h” Bean 单例的创建流程。这里显然是获取不到,要启动 “h” Bean 单例的创建流程。
    该方法的第二个参数是要传入一个 ObjectFactory 类型的工厂对象,ObjectFactory 是一个接口,其内部只有一个抽象方法 getObject
    回忆三级缓存,三级缓存的 value 就是 ObjectFactory 类型。
    这里相当于是,传入了创建 “h” 的单例 Bean 对象的工厂对象。那么该工厂对象,后续是否会被放到三级缓存 singletonFactories 中呢?
    结论是不会,后续会利用该工厂对象生产出早期单例 Bean 对象,然后创建另外一个工厂对象简单包装下该早期单例 Bean 对象,最后将这个新的工厂对象放到三级缓存中。

  3. beforeSingletonCreation("h"):将 “h” 添加到 singletonsCurrentlyInCreation,表示要准备创建 “h” Bean 单例了。

  4. singletonFactory.getObject()singletonFactory 就是先前传入的工厂对象 () -> createBean("h"),这里调用 getObject(),其实就是调用 createBean("h")

  5. createBean("h"):准备创建 id 为 “h” 的单例 Bean 对象。

  6. doCreateBean("h"):创建 id 为 “h” 的单例 Bean 对象。该方法包含创建单例 Bean 对象的完整过程。

  7. hBean = createBeanInstance("h"):创建 “h” 单例 Bean 对象,底层调用无参数构造方法完成对象的创建,但此时 hBean 的属性未进行填充。

  8. addSingletonFactory("h", () -> getEarlyBeanReference(hBean)):将已经创建好的早期单例 hBean,封装到一个新的工厂对象 () -> getEarlyBeanReference(hBean) 中,然后将该新工厂对象,放入三级缓存 singletonFactories 中。
    显然新的工厂对象底层并不执行创建工作,而是将早就创建好的 hBean 返回。那么为什么不直接将 hBean 放入二级缓存呢?这应该是为了兼容 AOP 所做的处理。
    getEarlyBeanReference(hBean) 中会检查 hBean 是否进行了 AOP 代理,如果是的话,会基于 hBean 返回其代理对象;否则直接返回 hBean

  9. 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") 的流程,所以下面简单过一遍。

  10. getBean("w"):准备获取 id 为 “w” 的单例 Bean 对象。

  11. doGetBean("w"):获取 id 为 “w” 的单例 Bean 对象。
  12. getSingleton("w"):尝试从缓存中获取 id 为 “w” 的单例 Bean 对象。获取不到,返回空。
  13. getSingleton("w", () -> createBean("w")): 启动 “w” Bean 的创建流程。传入创建 “w” 单例 Bean 的工厂对象。
  14. beforeSingletonCreation("w"):将 “w” 添加到 singletonsCurrentlyInCreation,表示要准备创建 “w” Bean 单例了。此时 singletonsCurrentlyInCreation 中既有 “h” 又有 “w”,两个 Bean 都在处于创建中。
  15. singletonFactory.getObject():通过先前传入的工厂对象 () -> createBean("w"),调用 createBean("w")
  16. createBean("w"):准备创建 id 为 “w” 的单例 Bean 对象。
  17. doCreateBean("w"):创建 id 为 “w” 的单例 Bean 对象。该方法包含创建单例 Bean 对象的完整过程。
  18. wBean = createBeanInstance("w"):调用无参构造方法,创建 “w” 单例 Bean 对象 wBean 但属性未赋值。
  19. addSingletonFactory("w", () -> getEarlyBeanReference(wBean)):将包装 wBean 的新工厂对象,放入三级缓存 singletonFactories 中。此时 singletonFactories 中既有 “h” 的工厂对象,又有 “w” 的工厂对象。
  20. populateBean(wBean, mbd, instanceWrapper):对 wBean 进行属性的注入。
    在注入 wBean 的第二个属性时,会调用 getBean("h")

  21. getBean("h")wBean 注入需要获取 “h” Bean。

  22. doGetBean("h")wBean 注入需要获取 “h” Bean。
  23. 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 算是彻底完成创建了。

  1. afterSingletonCreation("w"):既然 wBean 彻底完成创建了,就从 singletonsCurrentlyInCreation 中移除掉 “w”。

  2. 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 算是彻底完成创建了。

  1. afterSingletonCreation("h"):既然 hBean 彻底完成创建了,就从 singletonsCurrentlyInCreation 中移除掉 “h”。
  1. addSingleton("h", hBean):既然 hBean 彻底完成创建了,是一个完整的 Bean 对象了,就需要将 hBean 加入到一级缓存 singletonObjects 中。
    hBean 加入一级缓存 singletonObjects 后,会到二三级缓存中删除 “h” 相关的对象。
    此时只有二级缓存 earlySingletonObjects 中含有相关对象 hBean,将其从二级缓存中移除。这个 hBean 和刚放入一级缓存中的 hBean 其实是同一个。

hBean 已经彻底完成创建了,该 hBean 会一直向上返回,直到 1. getBean("h")。还记得这个 getBean("h") 是哪里调用的吗?

1
2
3
4
5
6
7
8
9
public void preInstantiateSingletons() throws BeansException {
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

for (String beanName : beanNames) {
if (!isFactoryBean(beanName)) {
getBean(beanName);
}
}
}

之前就说过,这里 getBean(beanName) 并没有使用变量接收返回值,因为目的就只是为了触发单例对象的创建和属性赋值。

beanDefinitionNames 中包含两个值 {"h", "w"}getBean("h") 的执行总共经历了上面这一长串流程,走完这写流程后,一级缓存 singletonObjects 已经存储了相应的单例对象 hBeanwBean。因此,紧接着 getBean("w") 的执行就相对简单了:getBean("w")doGetBean("w") 最后到 getSingleton("w") 从一级缓存中拿到 wBean 就结束了。

关于 populateBean 中属性注入的一个小细节

populateBean(hBean, mbd, instanceWrapper) 方法中,对 hBean 进行属性 namewife 的注入时,是先将值 “张三” 注入到 name 中,然后再 getBean("w") 吗?

换句话说就是,在调用 getBean("w") 时,hBean 中的 name 属性是否已经赋上值 “张三” 了?

结论是并没有赋上值,Spring 的处理逻辑是,将所有属性的值,都获取到以后,再一次性地进行属性注入操作。

详情可以看,AbstractAutowireCapableBeanFactory 中的 applyPropertyValues 方法,Spring 将对每个属性,都先获取到其值,然后封装为一个 PropertyValue 对象存入到 List<PropertyValue> deepCopy 中。所有属性解析完成后,deepCopy 就保存了所有的属性和对应的属性值,此时再根据 deepCopy 进行属性注入。

写在最后

Spring 虽然在 singleton + set 注入模式下,能够解决循环依赖问题,但在平常开发中,应该尽可能避免循环依赖的产生。

痛快!SpringBoot终于禁掉了循环依赖! 中提到:

Spring 的 Bean 管理,一直是整个体系中津津乐道的东西。尤其是 Bean 的循环依赖,更是很多面试官最喜欢考察的 2B 知识点之一。

但事实上,项目中存在 Bean 的循环依赖,是代码质量低下的表现。多数人寄希望于框架层来给擦屁股,造成了整个代码的设计越来越糟,最后用一些奇技淫巧来填补犯下的错误。

还好,SpringBoot 终于受不了这种滥用,默认把循环依赖给禁用了!

从 2.6 版本开始,如果你的项目里还存在循环依赖,SpringBoot 将拒绝启动!

评论区有人提到了平常开发中可能会遇到的循环依赖的场景,其他人给出了解决方案:

vishun:想请教下,如果我想在订单中查下此订单下的用户信息,在用户中查询下用户所下订单信息,如果不循环依赖的话,要怎么处理比较好呢?再新建个类?

KNORRIG:业务 service 注入用户 service 和订单 service 不就可以了吗。干嘛非要弄脏 UserService 和 OrderService 呢,除非你们架构对 UserService 和 OrderService 做了很清晰的职责划分,否则你的需求最好是第三方业务中的 Service 里面聚合好再返回

参考博客


学习完 AOP 相关的内容后,可以回过头来看看下面的博客。