Spring 之循环依赖底层源码剖析
版本说明
本文使用的 Spring 版本为 5.2.8.RELEASE,在阅读 Spring 底层源码时,不同版本之间可能会有细微的差别。
循环依赖的概述
Spring 官方文档
官方解释说明

翻译:如果主要使用构造方法注入,则可能会创建一个无法解决的循环依赖场景。例如:类 A 通过构造方法注入需要类 B 的实例,类 B 通过构造方法注入要求类 A 的实例。如果为类 A 和类 B 配置 Bean 以相互注入,那么 Spring IOC 容器会在运行时检测到这个循环引用,并抛出
BeanCurrentlyInCreationException异常。一种可能的解决方案是编辑一些类的源代码,由 Setter 方法而不是构造方法进行注入。或者,避免构造方法注入,只使用 Setter 方法注入。换句话说,尽管不建议这样做,但您可以使用 Setter 方法注入来配置循环依赖关系。与典型的情况(没有循环依赖关系)不同,Bean A 和 Bean B 之间的循环依赖关系迫使其中一个 Bean 在完全初始化之前注入另一个 Bean(典型的先有鸡后有蛋的场景)。
结论一:在 Spring 中,如果 Bean 不是单例或者使用了构造方法注入,一旦发生了循环依赖,Spring 无法自行解决循环依赖问题。
结论二:对于 A、B、C 循环依赖的问题,只要 Bean A 的注入方式是 Setter 方法注入或者属性注入,且 Bean 是单例,那么就不会发生循环依赖问题。
什么是循环依赖
Spring 中的循环依赖指的是当两个或多个 Bean 之间存在相互依赖关系时,可能导致的循环引用问题。具体来说,当 Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean C,而 Bean C 又依赖于 Bean A,这样就会发生循环依赖。这种情况下,Spring 容器在初始化 Bean 的过程中可能会陷入死循环,导致应用程序无法启动或者抛出异常。这是因为 Spring 在创建 Bean 的过程中是通过构造方法注入或者 Setter 方法注入依赖的,如果存在循环依赖,那么容器就无法决定先创建哪个 Bean,从而无法满足依赖关系。

面试技巧
通常来说,如果面试问到 Spring 容器的内部是如何解决循环依赖的,那么一定是指在默认的单例 Bean 中,属性互相引用的场景。
循环依赖验证一
循环依赖的演示代码
这里将演示,在 Spring 中,使用构造方法注入会很容易产生循环依赖的问题。
- 定义 Bean A
1 |
|
- 定义 Bean B
1 |
|
- 定义测试类
1 | /** |
- 代码执行输出的结果,会抛出
BeanCurrentlyInCreationException异常
1 | Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation: Is there an unresolvable circular reference? |
- 构造方法注入很容易产生循环依赖的问题,想让构造方法注入支持循环依赖,那是不可能的。如果构造方法注入支持循环依赖,那么程序员就可以无限套娃,代码如下
1 | public class ClientConstructor { |
- 简而言之,多个类在各自实例化时都需要对方实例,这就类似于线程死锁,如果不采取一种办法解决,那么它们将永远互相等待下去
循环依赖的解决方案
在 Spring 中,可以使用 Setter 方法注入且单例(Singleton)来解决循环依赖的问题。
- 定义 Bean A
1 |
|
- 定义 Bean B
1 |
|
循环依赖验证二
这里将演示,在 Spring 中,使用 Setter 方法注入且单例(Singleton)的场景是支持循环依赖的,而使用 Setter 方法注入且原型(Prototype)的场景是不支持循环依赖的。
循环依赖的演示代码
- 定义 A 类
1 | public class A { |
- 定义 B 类
1 | public class B { |
- 定义测试类
1 | public class SpringContainerTest { |
- 定义 Spring 的 XML 配置文件,
scope的默认值就是singleton,可以省略不写,表示每次从 IOC 容器中获取的 Bean 实例都是单例
1 |
|
- 代码执行输出的结果如下,使用 Setter 方法注入且单例(Singleton)的场景是支持循环依赖的,程序运行不会报错
1 | A created success |
- 将上述 Spring 的 XML 配置文件中的
scope修改为prototype,表示每次从 IOC 容器中获取 Bean 实例时,都会产生一个新的 Bean 实例
1 | <bean id="a" class="com.java.interview.spring.dependency2.A" scope="prototype"> |
- 代码执行就会报下面的错误,根本原因是使用 Setter 方法注入且原型(Prototype)的场景是不支持循环依赖的
1 | Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'a' defined in class path resource [application-context.xml]: Cannot resolve reference to bean 'b' while setting bean property 'b'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [application-context.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference? |
循环依赖的解决方案
Spring 内部是通过三级缓存来解决循环依赖,底层源码如下:

第一级缓存
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);- 可称之为成品单例池,存放已经经历了完整生命周期的 Bean 对象,常说的 Spring 容器就是指它,平时获取单例 Bean 就是在这里面获取的
第二级缓存
Map<String, Object> earlySingletonObjects = new HashMap<>(16);- 存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完整,可以认为是半成品的 Bean 对象)
第三级缓存
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);- 存放可以生成 Bean 的工厂,用于处理 AOP 代理或 FactoryBean 生成的代理对象
特别注意
在 Spring 中,只有单例的 Bean 会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的 Bean,每次从容器中获取都是一个全新的 Bean,即都会重新创建 Bean,所以非单例的 Bean 是没有缓存的,Spring 不会将其放到三级缓存中,这也导致了使用 Setter 方法注入且原型(Prototype)的场景不支持循环依赖。
循环依赖的底层源码剖析
源码 Debug 的前置知识
实例化与初始化
- 实例化:在堆内存中申请一块内存空间,用于创建(存放)对象

- 初始化:完成对象属性的填充

三级缓存与四大方法

三级缓存
Spring 内部是通过三级缓存来解决循环依赖,底层源码如下:

第一级缓存
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);- 存放的是已经初始化好了的 Bean 实例,Bean 名称与 Bean 实例相对应,即所谓的单例池。
- 表示 Bean 已经经历了完整的生命周期。
第二级缓存
Map<String, Object> earlySingletonObjects = new HashMap<>(16);- 存放的是实例化了,但是未初始化的 Bean 实例,Bean 名称与 Bean 实例相对应。
- 表示 Bean 的生命周期还没走完(属性还未填充完整,可以认为是半成品的 Bean 对象)就将这个 Bean 放入该缓存中,也就是将已实例化但未初始化的 Bean 放入该缓存中。
第三级缓存
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);- 存放的是生成 Bean 的 Bean 工厂实例,Bean 名称与 Bean 工厂实例对应,用于处理 AOP 代理或 FactoryBean 生成的代理对象。
- 假如 A 类实现了 FactoryBean 接口,那么依赖注入的不是 A 类,而是 A 类生成的 Bean 实例。
四大方法
getSingleton():从容器里面获得单例的 Bean,如果不存在,则会创建 Bean。doCreateBean():执行创建 Bean 的操作(在 Spring 中以do开头的方法都是干实事的方法)。populateBean():创建完 Bean 之后,对 Bean 的属性进行填充。addSingleton():Bean 初始化完成之后,将其添加到单例池中,下次执行getSingleton()方法时就能获取到单例的 Bean。
对象在三级缓存中的迁移
1 | class A { |
(1) 首先实例化 A,然后对 A 进行初始化。
(2) A 在初始化的过程中需要 B,于是 A 将自己放到第三级缓存里面,然后去实例化 B。
(3) B 实例化的时候发现需要 A,于是 B 先查第一级缓存,发现缓存里没有;再查第二级缓存,缓存里还是没有;再查第三级缓存,终于找到了 A;然后将第三级缓存里面的 A 放到第二级缓存里面,并删除第三级缓存里面的 A。
(4) B 顺利完成初始化后,将自己放到第一级缓存里面(此时 B 里面的 A 依然是处于初始化中状态),然后回来接着初始化 A。
(5) 由于 B 已经初始化完成,A 可以直接从第一级缓存里面获取到 B,然后完成 A 的初始化,并将 A 放到第一级缓存里面,并删除第二级缓存里面的 A。
源码 Debug 的详细分析
Debug 的代码
这里将基于上面 循环依赖验证二 的源码进行 Debug 分析。
Debug 的步骤
如何阅读框架的源码?
阅读框架源码的方法是:打断点 + 看输出的日志。
第一阶段
在
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml")代码处打上断点,逐步执行 Step Over,发现执行new ClassPathXmlApplicationContext()操作时,Bean A 和 Bean B 都已经被创建好了,因此需要进入该操作的内部。

执行 Step Into,进入
new ClassPathXmlApplicationContext()操作的内部,首先进入了静态代码块,这里没有要看的代码,执行 Step Out 跳出该代码块。

再次执行 Step Into,进入 ClassPathXmlApplicationContext 类的构造方法,该构造方法使用
this调用了另一个重载构造方法。

继续执行 Step Into,进入 ClassPathXmlApplicationContext 的重载构造方法后,单步执行 Step Over,发现执行完
refresh()方法后会输出日志,于是将断点打在refresh()那一行代码。

继续执行 Step Into,进入
refresh()方法,发现执行完finishBeanFactoryInitialization()方法后会输出日志,于是将断点打在finishBeanFactoryInitialization()那一行代码,从注释也可以看出该方法完成了非懒加载单例 Bean 的实例化。

继续执行 Step Into,进入
finishBeanFactoryInitialization()方法,发现执行完beanFactory.preInstantiateSingletons()方法后会输出日志,于是将断点打在beanFactory.preInstantiateSingletons()那一行代码,,从注释也可以看出该方法完成了非懒加载单例 Bean 的实例化。

第二阶段
执行 Step Into,进入
beanFactory.preInstantiateSingletons()方法,发现执行完getBean()方法后会输出日志,于是将断点打在getBean()那一行代码。

执行 Step Into,进入
getBean()方法,发现调用了doGetBean()方法,也就是前面说过的:在 Spring 里面,以do开头的方法都是干实事的方法。

执行 Step Into,进入
doGetBean()方法,这里的transformedBeanName()方法会将用户定义的别名转换为 Bean 的真实名称。

执行 Step Over,继续执行后面的
getSingleton()方法

执行 Step Into,进入
getSingleton()方法,发现调用了其重载的方法。

执行 Step Into,进入重载的
getSingleton()方法,传入的allowEarlyReference参数,表示是否可以从第二级缓存中获取 Bean。发现该方法会尝试从第一级缓存singletonObjects中获取 Bean A,由于 Bean A 现在还没有开始创建,因此从第一级缓存中获取不到,而且isSingletonCurrentlyInCreation()方法会返回false,导致最后返回的 Bean A 为null。

返回到
doGetBean()方法中,执行完getSingleton()方法会返回null。

执行 Step Over,继续执行后面的代码,可以看到 RootBeanDefinition 实例。在 XML 配置文件中定义的 Bean,对于 Spring 来说就是一个个的 RootBeanDefinition 实例。

执行 Step Over,继续执行后面的代码,可以看到有一个
dependsOn变量,它对应于 Bean 的depends-on属性,因为在 XML 配置文件中没有配置过该属性,因此为null。

执行 Step Over,继续执行后面的代码,终于看到开始准备创建 Bean A 了。

第三阶段
执行 Step Into,进入
getSingleton()方法中,在 IDEA 2020 里面需要使用鼠标左键点击getSingleton()方法进入。

在
getSingleton()方法中,首先从第一级缓存singletonObjects获取 Bean A,缓存里面没有,那么就需要创建 Bean A,此时日志会输出Creating shared instance of singleton bean 'a'。

当执行完
singletonObject = singletonFactory.getObject()时,日常会输出A created success,这说明执行singletonFactory.getObject()方法时将会实例化 Bean A,并且根据变量名可得知是单例工厂创建的,这个单例工厂就是传入的 Lambda 表达式。

执行 Step Into,进入
createBean()方法,mbdToUse将用于创建 Bean A。

执行 Step Over,继续执行后面的代码,终于要执行
doCreateBean()方法实例化 Bean A 了。

执行 Step Into,进入
doCreateBean()方法,在factoryBeanInstanceCache中并不存在 Bean A 对应的 Wrapper 缓存,因此要先创建 Bean A 对应的instanceWrapper。形象的理解:Bean A 的实例化需要经过instanceWrapper之手,Bean A 实例被instanceWrapper包裹在其中。

执行 Step Into,进入
createBeanInstance()方法,里面的核心逻辑是使用反射创建 Bean A。

执行 Step Over,继续执行后面的代码,有一个
resolved变量,写着注释:Shortcut when re-creating the same bean…,如果值为true,表示使用快捷方式重新创建同一个 Bean。

执行 Step Over,继续执行后面的
instantiateBean()方法。

执行 Step Into,进入
instantiateBean()方法,会执行getInstantiationStrategy().instantiate()方法完成 Bean A 的实例化。

执行 Step Into,进入
getInstantiationStrategy().instantiate()方法,首先通过执行bd.resolvedConstructorOrFactoryMethod来获取已经解析好的构造器 ,由于是第一次创建,所以获取不到构造器,因此constructorToUse == null;然后获取 Bean A 的类型,如果发现是接口,则直接抛出异常;最后通过执行clazz.getDeclaredConstructor()方法来获取 Bean A 的构造器,并赋值给constructorToUse变量。

执行 Step Over,继续执行后面的代码,可以看到上面获取构造器
constructorToUse的目的是为了实例化 Bean A。

执行 Step Into,进入
BeanUtils.instantiateClass()方法,日志会输出A created success

执行 Step Over,会返回到
getInstantiationStrategy().instantiate()方法中,开始创建 Bean A 实例,不过还没有进行初始化,可以看到属性b = null。

执行 Step Over,会返回到
instantiateBean()方法中,得到刚刚创建的 Bean A 实例,但其属性b并未被初始化。

执行 Step Over,将已实例化的 Bean A 封装进 BeanWrapper 中,并返回 BeanWrapper 实例。

执行 Step Over,会返回到
createBeanInstance()方法中,得到刚刚创建的 BeanWrapper 实例,该 BeanWrapper 封装了创建好的 Bean A 实例。

执行 Step Over,会返回到
doCreateBean()方法中,获得 BeanWrapper 实例,并通过 BeanWrapper 实例获取创建好的 Bean A 实例。

执行 Step Over,继续执行后面的代码,获取 Bean A 的全类名。

执行 Step Over,继续执行后面的代码,执行 BeanPostProcessor。

执行 Step Over,继续执行后面的代码,如果该 Bean 是单例,并且允许循环依赖,且当前 Bean 正在创建过程中,那么就允许提前暴露该 Bean,也就是将该 Bean 放到第三级缓存
singletonFactories中。

执行 Step Into,进入
addSingletonFactory()方法。首先去第一级缓存singletonObjects中找一下有没有 Bean A,如果找不到,则将 Bean A 添加到第三级缓存singletonFactories中,并将 Bean A 从第二级缓存earlySingletonObjects中移除。最后将beanName添加至registeredSingletons中,表示该单例 Bean 已经被注册过。

第四阶段
执行 Steop Over,会返回到
doCreateBean()方法中,需要执行populateBean()方法对 Bean A 中的属性进行填充。

执行 Step Into,进入
populateBean()方法,首先会获取 Bean A 的属性列表。

执行 Steop Over,执行后面的
applyPropertyValues()方法,完成 Bean A 属性的填充。

执行 Step Into,进入
applyPropertyValues()方法,获取到 Bean A 的属性列表,发现里面有个属性为b。

执行 Step Over,继续执行后面的代码,遍历每一个属性,并对每一个属性进行注入,
valueResolver.resolveValueIfNecessary()方法的作用是:给定一个 PropertyValue,返回一个值,如有必要,解析工厂中对其他 Bean 的任何引用。

执行 Step Into,进入
valueResolver.resolveValueIfNecessary()方法,会执行resolveReference()方法来解决依赖注入的问题。

执行 Step Into,进入
resolveReference()方法,首先获得属性b的名称,再通过this.beanFactory.getBean()方法获取 Bean B 的实例。

执行 Step Into,进入
this.beanFactory.getBean()方法,这里会调用熟悉的doGetBean()方法。

执行 Step Into,进入
doGetBean()方法,由于 Bean B 还没有实例化,因此getSingleton()方法返回null。

执行 Step Over,继续执行后面的代码,又回到这个熟悉的地方,尝试获取 Bean B 实例,获取不到就创建。

执行 Step Into,进入
getSingleton()方法,首先尝试从一级缓存singletonObjects中获取 Bean B 实例,缓存里面没有,因此获取不到。

执行 Step Over,继续执行后面的
singletonFactory.getObject()方法创建 Bean B 实例。

执行 Step Over,继续执行
createBean()方法创建 Bean B 实例。

执行 Step Into,进入
createBean()方法,首先会获取 Bean B 的类型。

执行 Step Over,继续执行后面的
doCreateBean()方法。

执行 Step Into,进入
doCreateBean()方法,会通过createBeanInstance()方法创建 Bean B 对应的 BeanWrapper 实例。

执行 Step Into,进入
createBeanInstance()方法,会执行instantiateBean()方法创建 Bean B 对应的 BeanWrapper 实例。

执行 Step Into,进入
instantiateBean()方法,会执行getInstantiationStrategy().instantiate()方法创建 Bean B 实例。

执行 Step Into,进入
getInstantiationStrategy().instantiate()方法,会先获取 Bean B 的构造器,然后再执行BeanUtils.instantiateClass()方法创建 Bean B 实例。

执行 Step Into,进入
BeanUtils.instantiateClass(constructorToUse)方法,通过调用 B 类的构造器创建 Bean B 实例,此时日志会输出:B created success。

执行 Step Over,会返回到
instantiateBean()方法中,将创建好的 Bean B 实例封装进 BeanWrapper 实例。

执行 Step Over,会返回到
doCreateBean()方法中,createBeanInstance()方法返回了封装着 Bean B 实例的 BeanWrapper。

执行 Step Over,继续执行后面的代码,执行 BeanPostProcessor 的处理过程。

执行 Step Over,继续执行后面的代码,由于 Bean B 满足单例并且正在被创建,因此 Bean B 可以被提前暴露出去(在属性还未初始化的时候可以提前暴露出去),于是执行
addSingletonFactory()方法将其添加到第三级缓存singletonFactory中。

执行 Step Into,进入
addSingletonFactory()方法,可以看到会将 Bean B 实例添加到第三级缓存singletonFactory中,并从二级缓存earlySingletonObjects中移除 Bean B,同时注册其beanName。

执行 Step Over,返回到
doCreateBean()方法中,执行populateBean()方法填充 Bean B 的属性。

第五阶段
循环依赖问题解决总结
在 Spring 中,可以通过使用 Setter 方法注入、属性注入、@Lazy 注解或者重构代码来解决循环依赖问题。
解决方案一
尽量不要使用构造方法注入,而是使用 Setter 方法注入或者属性注入,这样 Spring 就可以使用三级缓存机制来解决循环依赖问题。因为在 Spring 中,如果使用构造方法注入,一旦发生了循环依赖,Spring 无法自行解决这个问题。
1 | public class A { |
或者
1 | public class A { |
解决方案二
使用 @Lazy 注解解决循环依赖问题。@Lazy 注解是 Spring Framework 提供的一个注解,用于延迟 Bean 的初始化,可以用在类或方法上。通常情况下,Spring 容器会在启动时创建并初始化所有单例 Bean,但使用 @Lazy 注解可以改变这一行为,使 Bean 在第一次被访问时才进行初始化。对于某些特定的循环依赖场景,@Lazy 可以推迟依赖的注入,从而打破循环依赖链。
1 | public class A { |
解决方案三
重构代码,避免相互依赖。通常情况下,可以通过引入第三个类或接口来拆分依赖链,从而避免了循环依赖。比如在下面的例子中,A 和 B 都依赖于 CommonService,而不直接依赖对方,从而避免了循环依赖。
1 | public class A { |
