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 { |