循环依赖或多或少地都会充斥在我们开发的进程中,或许我们借助了框架的力量化解掉其中可能发生的问题,但到底是如何解决的可能大家会觉得太复杂而避而不谈。实际上循环依赖处理起来是非常简单的,下面笔者将会把这些都摆上台面逐帧进行分析。
概述
在软件工程中,循环依赖指模块间的属性存在相互依赖的关系。如果存在循环依赖可能会导致以下的不良结果:
- 导致相互依赖的模块紧耦合,从而降低各自模块的复用性。
- 导致模块间的多米诺效应,一个很小的改动引发其他模块甚至全局的不可用,例如导致无限循环或其他不可预期的异常发生。
- 导致内存泄露,阻止垃圾收集器回收无用对象(使用引用计数法的垃圾收集器)。
因此,我们应该遵循循环依赖原则(ADP
),即在包或者组件的依赖图谱中不应该存在循环。
循环依赖是一种反模式(
anti-pattern
)。
如何避免
传统模式下,如果我们通过内建的方式创建依赖属性,则很容易导致循环依赖的发生,从而在创建对象时发生死循环。例如,
java
public class Component1 {
private Component2 component2;
public Component1() {
this.component2 = new Component2();
}
}
public class Component2 {
private Component1 component1;
public Component2() {
this.component1 = new Component1();
}
}
public class Application {
public static void main(String[] args) {
// 无限循环导致栈溢出
Component1 component1 = new Component1();
// 无限循环导致栈溢出
Component2 component2 = new Component2();
}
}
上述代码中,对ComponentOne
或者ComponentSecond
的创建都会导致无限循环的发生。
一般,我们可以通过依赖反转(DIP
)和依赖注入(DI
)很大程度地避免循环依赖地发生。例如,
java
public interface IComponent1 {
}
public interface IComponent2 {
}
public class Component1 implements IComponent1 {
private IComponent2 component2;
public Component1(IComponent2 component2) {
this.component2 = component2;
}
}
public class Component2 implements IComponent2 {
private IComponent1 component1;
public Component2(IComponent1 component1) {
this.component1 = component1;
}
}
public class Application {
public static void main(String[] args) {
// compiler error,无法通过构造方法创建出Component1
IComponent1 component1 = new Component1(...);
// compiler error,无法通过构造方法创建出Component2
IComponent2 component2 = new Component2(...);
}
}
这样,我们在构造对象时就能及时发现循环依赖的存在,而不是运行时错误。
如何解决
对于已存在循环依赖的情况,我们应该如何正确地构建对象呢?如果要在程序中正确的构建具有循环依赖的对象,则需要提前将尚未初始化属性的对象提前暴露出去。例如,
java
public class Component1 {
private Component2 component2;
public Component1(){
}
public void setComponent2(Component2 component2) {
this.component2 = component2;
}
}
public class Component2 {
private Component1 component1;
public Component2(){
}
public void setComponent1(Component1 component1) {
this.component1 = component1;
}
}
public class Application {
public static void main(String[] args) {
// 提前将尚未初始化属性的Component1对象提前暴露出去
Component1 component1 = new Component1();
// 提前将尚未初始化属性的Component2对象提前暴露出去
Component2 component2 = new Component2();
// 初始化属性
component1.setComponent2(component2);
component2.setComponent1(component1);
}
}
然而,对于体量较大的项目使用这种方式效率却是有点低,所以一些应用层框架(例如Spring
)常会将控制反转(IoC
)结合依赖注入(DI
)进行使用。在控制反转(IoC
)的作用下,框架首先会构造一个类似于控制器的对象,并将控制权转交给控制器。然后,控制器会先实例化应用对象中的依赖,并通过依赖注入(DI
)传递到应用对象中。
以Spring
为例,我们先来看看它是如何使用控制反转(IoC
)和依赖注入(DI
)。
通过将Spring
的@Autowired
注解声明于构造方法上,我们即可轻松实现对其的依赖注入(DI
),例如:
java
@Component
public class Component1 {
private Component2 component2;
@Autowired
public Component1(Component2 component2){
this.component2 = component2;
}
}
@Component
public class Component2 {
private Component1 component1;
@Autowired
public Component2(Component1 component1) {
this.component1 = component1;
}
}
显然地,通过构造方法注入是可以产生出循环依赖的。而如果想用正确的姿势使用含循环依赖的类,可通过setter
的方式进行注入,例如:
java
@Component
public class Component1 {
private Component2 component2;
@Autowired
public void setComponent2(Component2 component2){
this.component2 = component2;
}
}
@Component
public class Component2 {
private Component1 component1;
@Autowired
public void setComponent1(Component1 component1){
this.component1 = component1;
}
}
或者,使用更加简洁的注入方式,即直接在属性字段上声明@Autowired
,例如:
java
@Component
public class Component1 {
@Autowired
private Component2 component2;
}
@Component
public class Component2 {
@Autowired
private Component1 component1;
}
这样,我们就能顺利的实例化具有循环依赖的类了。
当然,循环依赖被定性为一种反模式,不推荐使用。在发生循环依赖时,我们应该更多地思考我们的模块划分是否合理,如何去避免它的发生,而不是如何去解决它。
抛开循环依赖的争议,我们看一下在Spring
中是如何解决循环依赖的(仅考虑单例模式下的bean
)。
首先,我们可以看到用于实例化bean
的doGetBean
方法,在doGetBean
方法中Spring
会完成bean
的创建、属性填充和初始化,具体代码如下所示:
java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws ... {
String beanName = ...;
Object beanInstance;
// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName, true);
if (sharedInstance != null && args == null) {
// ...
beanInstance = getObjectForBeanInstance(sharedInstance, ...);
}
else {
// ...
RootBeanDefinition mbd = ...;
// ...
// Create bean instance.
sharedInstance = getSingleton(beanName, () -> {
// 假设没有异常
return createBean(beanName, mbd, args);
});
beanInstance = getObjectForBeanInstance(sharedInstance, ...);
}
return adaptBeanInstance(name, beanInstance, requiredType);
}
在上述代码中,我们可以看到在真正执行bean
的创建前会先通过getSingleton(String, boolean)
方法在缓存中查找出对应的bean
实例,在查找失败的情况下再使用getSingleton(String, ObjectFactory)
方法创建对应的bean
实例。为了便于后续的分析,下面我们先来看看getSingleton(String, boolean)
方法的执行逻辑(为了便于理解,笔者对getSingleton(String, boolean)
方法进行了大量的精简)。
java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
}
return singletonObject;
}
具体执行流程如下所示:
- 在
singletonObjects
缓存中查询是否存在创建完成的bean
实例,若存在立即返回,否则执行第2
步。 - 在
earlySingletonObjects
缓存中查询是否存在早期创建的bean
实例,若存在立即返回,否则返回null
。
显然地,在首次通过doGetBean
方法实例化bean
是不会在getSingleton(String, boolean)
方法中查询到缓存实例的,那么我们就进入到getSingleton(String, ObjectFactory)
方法进行bean
实例的创建了(为了便于理解,笔者对getSingleton(String, ObjectFactory)
方法进行了大量的精简)。
java
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = singletonFactory.getObject();
addSingleton(beanName, singletonObject);
}
return singletonObject;
}
protected void addSingleton(String beanName, Object singletonObject) {
this.singletonObjects.put(beanName, singletonObject);
this.earlySingletonObjects.remove(beanName);
}
具体执行流程如下所示:
- 在
singletonObjects
缓存中查询是否存在创建完成的bean
实例,若存在立即返回,否则执行第2
步。 - 执行
ObjectFactory#getObject
创建bean
实例,即执行createBean
方法创建bean
实例。 - 将创建完成的
bean
实例加入到singletonObjects
缓存中,同时将它从earlySingletonObjects
缓存中移除。
同样地,在首次创建时singletonObjects
缓存必然不会存在对应的缓存实例,那么我们再进入到createBean
方法中(为了便于理解,笔者对createBean
方法和doCreateBean
方法进行了大量的精简)。
java
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
RootBeanDefinition mbdToUse = mbd;
//...
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
return beanInstance;
}
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
// 1. 执行`raw bean`实例的创建
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// ...
// 2. 将`raw bean`实例提前添加到earlySingletonObjects缓存中
addEarlySingletonObjects(beanName, bean);
Object exposedObject = bean;
// 3. 执行`raw bean`实例的属性填充
populateBean(beanName, mbd, instanceWrapper);
// 4. 执行`raw bean`实例的初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
// ...
return exposedObject;
}
具体执行流程如下所示:
- 执行
raw bean
实例的创建 - 将
raw bean
实例添加到earlySingletonObjects
缓存中 - 执行
raw bean
实例的属性填充 - 执行
raw bean
实例的初始化
需要注意,在第
2
步中"将raw bean
实例添加到earlySingletonObjects
缓存中"是笔者在某种场景下简化原有逻辑转化而来的,实际上Spring
在此处会先构建出创建earlySingletonObject
实例的工厂实例并将其添加到工厂实例的缓存中,然后在循环依赖发生时再次执行getSingleton(String, boolean)
方法触发earlySingletonObject
实例的创建并将其添加到earlySingletonObjects
缓存中。这可能与当前笔者给出的代码和执行流程有所出入,但在大部分场景下它们最终所生成的bean
实例是相同的。
在上述流程中我们可以看到Spring
会在第2
步中将早期创建的bean
提前添加到earlySingletonObjects
缓存中,那么在执行到第3
步属性填充时如果发生了循环依赖再次通过doGetBean
方法请求当前bean
时会在getSingleton(String, boolean)
方法的earlySingletonObjects
缓存中获取到这个bean
实例,这样就终止了循环依赖过程中无限创建bean
实例的循环。
至此,我们看到了完整的Spring
循环依赖解决方案。不难发现,这本质上就是我们上文解决循环依赖所提出的方案,即提前将尚未初始化属性的对象提前暴露出去。
关于
Spring
循环依赖的问题,如果读者想更进一步地了解整个处理流程可以阅读笔者之前的一篇博客《用6w字的篇幅向你全方位解析Spring IoC
》。
总结
总的来说,无论形态如何发生变化,本质上最终对循环依赖的解决方案都是相同的,即提前将尚未初始化属性的对象提前暴露出去。