Spring IoC 源码解析:循环依赖的检测与处理

Spring 为开发人员提供了极其灵活和强大的配置使用方式,在方便开发的同时也为容器的初始化过程带来了不确定性。本文所要介绍的循环依赖就是其中之一,尤其在一些大型项目中,循环依赖的配置往往是我们不经意而为之的,幸好 Spring 能够在初始化的过程中检测到对象之间的循环依赖,并能够在一定程度上予以处理。

什么是循环依赖

以最简单的循环依赖举例,假设我们定义了两个类 A 和 B,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class A {

private B b;

public A(@Autowired B b) {
this.b = b;
}

// ... getter & setter
}

@Component
public class B {

private A a;

public B(@Autowired A a) {
this.a = a;
}

// ... getter & setter
}

上述示例中 A 对象引用了 B 对象,B 对象反过来又引用了 A 对象,此时如果我们基于 Spring 管理对象 A 和 B 之间的依赖关系,就会存在循环依赖的问题。启动容器会看到抛出的异常中包含如下字样:

Requested bean is currently in creation: Is there an unresolvable circular reference?

当然,上述示例只是一个演示,实际开发中我们不会犯这么低级的配置错误,但是如果项目规模足够大,经过多层引用之后难免出现循环依赖,这往往是我们不经意而为之的。

循环依赖的检测与处理

那么 Spring 如何检测和处理循环依赖呢?我们先给出结论:

Spring 仅能够处理 singleton 对象之间基于 setter 注入方式造成的循环依赖,除此之外全部抛出 BeanCurrentlyInCreationException 异常。

也就是说如果按照如下配置,Spring 是能够正常完成初始化的:

1
2
3
4
5
6
7
<!--单例:setter注入-->
<bean id="a" class="org.zhenchao.spring.ioc.A">
<property name="b" ref="b"/>
</bean>
<bean id="b" class="org.zhenchao.spring.ioc.B">
<property name="a" ref="a"/>
</bean>

当然, 如果是属性注入方式同样能够被解决,该注入方式本质上还是 setter 注入 ,只是不再被 Spring 推荐使用。然而,如果是采用构造方法注入,或者造成循环依赖的对象不是 singleton 类型,则容器只能以抛出 BeanCurrentlyInCreationException 异常而结束。

那么 Spring 又是怎么检测出循环依赖配置的呢?由前面文章对于容器初始化过程的分析,我们知道实例化一个 bean 的过程主要分为三步:

  1. 创建 bean 实例,主要由 AbstractAutowireCapableBeanFactory#createBeanInstance 方法完成;
  2. 填充 bean 属性,依赖注入的过程发生于此,由 AbstractAutowireCapableBeanFactory#populateBean 方法完成;
  3. 初始化 bean 实例,主要是调用初始化方法,由 AbstractAutowireCapableBeanFactory#initializeBean 方法完成。

循环依赖主要发生在上述步骤中的第 1 和第 2 步。设想,如果我们在创建 A 对象的时候发现需要填充类型为 B 的属性,这个时候就需要转而去创建 B 对象,但是在创建 B 对象的时候发现又需要填充类型为 A 的属性,这个时候 A 对象和 B 对象都处于创建过程中,造成了死循环。如果我们把这两步拆开又会怎么样呢?即先把 A 对象和 B 对象先创建好,对应的属性先用 null 填充,然后再使用相应类型的对象填充属性,这样就破解了环路,这也是 Spring 解决基于 setter 注入方式导致的循环依赖的基本思路。

下面具体分析 Spring 是如何实现这一思路的。首先列出整个过程中需要用到的几个用于记录状态的集合类型属性:

  • DefaultSingletonBeanRegistry#singletonFactories:Map 类型,用于记录 beanName 和创建 bean 对象的工厂之间的映射关系。
  • DefaultSingletonBeanRegistry#earlySingletonObjects:Map 类型,用于记录 beanName 和原始 bean 实例之间的映射关系,此时的 bean 对象刚刚被创建,还没有注入属性。
  • DefaultSingletonBeanRegistry#singletonObjects:Map 类型,用于记录 beanName 和最终 bean 实例之间的映射关系。

这三个属性构成了一些人口中描述的三级缓存。其中 singletonObjects 和 earlySingletonObjects 两个属性虽然都是记录 beanName 与 bean 实例之间的映射关系,但是区别在于后者中记录的 bean 实例还没有填充属性值,并且这两个集合中存放的内容是互斥的。

Spring 在完成创建 bean 实例之后,且在填充 bean 属性之前,即上述步骤中的 1 和 2 之间,会执行如下这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean earlySingletonExposure = (mbd.isSingleton() // 单例
&& this.allowCircularReferences // 允许自动解决循环依赖
&& this.isSingletonCurrentlyInCreation(beanName)); // 当前 bean 正在创建中
if (earlySingletonExposure) {
// 为避免循环依赖,在完成 bean 实例化之前,将对应的 ObjectFactory 注册到容器中
this.addSingletonFactory(beanName,
// 获取 bean 的提前引用
() -> this.getEarlyBeanReference(beanName, mbd, bean));
}

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

上述实现的主要逻辑就是判断是否需要提前曝光正在实例化的 bean 对象,如果需要则将创建 bean 实例的 ObjectFactory 对象记录到 singletonFactories 属性中。

如果此时正好有另外一个操作试图获取正在创建中的 bean 实例,则会进入 DefaultSingletonBeanRegistry#getSingleton(java.lang.String) 方法。该方法将获取我们之前缓存的 ObjectFactory 对象,并调用 ObjectFactory#getObject 方法获取到之前创建的目标 bean 实例,并记录到 earlySingletonObjects 中,同时移除 singletonFactories 中缓存的 ObjectFactory 对象。而实例化过程也会很快调用 DefaultSingletonBeanRegistry#addSingleton 方法,将最终的 bean 实例记录到 singletonObjects 属性中,并移除所有的临时记录。

那么为什么用构造方法注入就会抛异常,而 setter 注入则不会呢?这是因为在创建 singleton 对象之前,Spring 会调用 DefaultSingletonBeanRegistry#beforeSingletonCreation 方法检查指定 bean 是否正在被创建,实现如下:

1
2
3
4
5
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}

上述方法在每次创建 singleton 对象之前都会被调用,对于创建同一个 bean 实例的第二次之后的调用就会触发该方法抛出异常。如果是构造方法注入,因为创建目标 bean 对象需要调用包含依赖对象类型参数的构造方法,而循环依赖势必导致当前构造方法的循环调用,从而触发上述方法抛出异常。然而,对于 setter 注入来说就不存在这样的问题,因为 Spring 对于 bean 实例的构造是分两步走的:第一步创建目标 bean 对象;第二步执行属性填充,将相应的依赖注入到该对象中。这样即使有循环依赖也不会阻碍对象的创建,因为此时调用的是无参构造方法(即使有参数,参数中也不包含循环依赖的对象),所以基于 setter 方式注入的 singleton 对象导致的循环依赖,容器的初始化机制能够很好的予以处理。

那么非 singleton 的怎么就不行了呢?我们先来看一下相关实现,Spring 定义了一个 AbstractBeanFactory#prototypesCurrentlyInCreation 集合变量记录当前线程内正在创建 bean 实例的 beanName,并且在创建一个非 singleton bean 之前,容器会调用 AbstractBeanFactory#isPrototypeCurrentlyInCreation 方法进行校验,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
if (this.isPrototypeCurrentlyInCreation(beanName)) {
/*
* 只有在单例模式下才会尝试解决循环依赖问题,
* 对于原型模式,如果存在循环依赖,直接抛出异常
*/
throw new BeanCurrentlyInCreationException(beanName);
}

protected boolean isPrototypeCurrentlyInCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
return (curVal != null && (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}

如果存在循环依赖则抛出 BeanCurrentlyInCreationException 异常。

Spring 为什么需要这样设计呢?一些解释是 Spring 没有缓存创建非 singleton bean 实例的中间状态,我个人觉得这只考虑到了一个方面。对于非 singleton bean 而言,完全可以复用 singleton 那一套予以实现,保证好线程安全即可。之所以 Spring 不这么做,个人认为还出于性能方面的考量。非 singleton bean 对象的特点是每次获取都会返回一个新的对象,并且这个过程可能是频繁调用的,这样就会降低框架的性能,同时增加内存占用,而很多时候循环依赖是因为开发者的错误配置导致的,这个时候还不如直接抛出异常,快速失败为好。

总结

本文我们分析了 Spring 如何检测和处理循环依赖。Spring 通过拆分对象创建和属性注入为两个独立过程,巧妙的破解了对于 singleton 对象执行 setter 注入场景下的依赖环路,从而在一定程度上解决了循环依赖问题。尽管如此,还是建议大家在日常开发中在编码层面避免循环依赖问题,让实现更加优雅。