今天我们讨论 Spring 框架所提供的核心功能之一:依赖注入。
依赖注入可以说是使用 Spring 框架的基本手段,我们通过它获取所需的各种 Bean。但在使用不同的依赖注入类型时,经常会碰到循环依赖问题。为了深入分析这一问题的解决方案,我们先从 Spring 依赖注入的类型和循环依赖的基本概念开始讲起。
Spring 依赖注入和循环依赖
Spring 为开发人员提供了三种不同的依赖注入类型,分别是字段注入、构造器注入和 Setter 方法注入。
Spring 框架的三种依赖注入类型
其中,字段注入是最常用、也是最容易使用的一种。但是,它也是三种注入方式中最应该避免使用的。因为它可能导致潜在的循环依赖。所谓循环依赖,就是两个类之间互相注入,例如这段示例代码:
显然,这里的 ClassA 和 ClassB 通过@Autowired 注解相互注入,发生了循环依赖。在 Spring 中,上述代码是合法的,容器启动时并不会报任何错误,只有在使用到具体某个 ClassA 或 ClassB 时,才会报错。
事实上,Spring 官方也不推荐开发人员使用字段注入这种注入模式,而是推荐构造器注入。基于构造器注入,前面介绍的 ClassA 和 ClassB 之间的循环依赖关系是这样的:
这时候,如果启动 Spring 容器,就会抛出一个循环依赖异常,提醒你应该避免循环依赖。
其实,Setter 方法注入可以很好地解决循环依赖问题,如下所示的代码是可以正确执行的:
请注意,上述代码能够正确执行的前提是:ClassA 和 ClassB 的作用域都是“Singleton”,即单例。所谓的单例,指的就是不管对某一个类的引用有多少个,容器只会创建该类的一个实例。
讲到这里,你可能会好奇,这就需要剖析 Spring 中对于单例 Bean 的存储和获取方式,让我们一起来看一下。
Spring 循环依赖解决方案
对于单例作用域来说,在 Spring 容器的整个生命周期内,有且仅有一个 Bean 对象,所以很容易想到这个对象应该位于缓存中。Spring 为了解决单例 Bean 的循环依赖问题,使用了三级缓存。这是 Spring 在设计和实现上的一大特色,也是面试过程中经常遇到的话题。
三级缓存结构
所谓的三级缓存,在 Spring 中表现为三个 Map 对象,定义在 DefaultSingletonBeanRegistry 类中,如下所示:
请注意:
这里的 singletonObjects 就是第一级缓存,用来持有完整的 Bean 实例。
而 earlySingletonObjects 中存放的是那些提前暴露的对象,也就是已经创建但还没有完成属性注入的对象,属于第二级缓存。
最后的 singletonFactories 存放用来创建 earlySingletonObjects 的工厂对象,属于第三级缓存。
三级缓存与保存的对象
那么三级缓存是如何发挥作用的呢?让我们来分析获取 Bean 的代码流程:
我们首先从一级缓存 singletonObjects 中获取目前对象,如果获取不到,则从二级缓存 earlySingletonObjects 中获取;如果还是获取不到,就从三级缓存 singletonFactory 中通过 ObjectFactory 进行获取。而一旦获取成功,就会把目标对象从第三级缓存移动到第二级缓存中,从而为下一次对象获取过程做准备。
通过这段代码,我们了解了三级缓存的依次访问过程,但可能你还是不理解 Spring 为什么要这样设计。事实上,解决循环依赖的关键点还是在 Bean 的生命周期上。
在通过@Autowired 注解注入 Bean 时,真正完成 Bean 的创建是在一个 doCreateBean 方法中,该方法包括三个核心步骤:
通过 createBeanInstance 方法实例化 Bean。
通过 populateBean 方法实现属性的注入。
最后通过 initializeBean 方法对 Bean 进行扩展。
单例对象的初始化步骤示意图
对应的代码结构如下所示:
上面的第 1 步完成了 Bean 的初始化,而第 2 步才完成 Bean 的完整实例化。我们看到在第 1 步和第 2 步之间,存在一个 addSingletonFactory 方法,用于初始化这个第三级缓存中的数据。
Spring 解决循环依赖的诀窍就在于 singletonFactories 这个第三级缓存:
请注意,这段代码的执行时机是 Bean 已经通过构造函数进行创建,但还没有完成 Bean 中完整属性的注入。换句话说,Bean 已经可以被暴露出来进行识别了,但还不能正常使用。接下来我们就来分析一下为什么通过这种机制就能解决循环依赖问题。
循环依赖解决方案
我们继续讨论 ClassA 和 ClassB 的循环依赖关系,基于 Setter 方法注入,整个流程如下所示,我们用红色部分表示 ClassA 的创建过程,用黄色部分表示 ClassB 的创建过程。
基于 Setter 注入的循环依赖解决流程
图中,假设我们先初始化 ClassA。ClassA 首先通过 createBeanInstance 方法创建了实例,并且将这个实例提前暴露到第三级缓存 singletonFactories 中。然后,ClassA 尝试通过 populateBean 方法注入属性,发现自己依赖 ClassB 这个属性,就会尝试去获取 ClassB 的实例。
显然,这时候 ClassB 还没有被创建,所以走创建流程。
ClassB 在初始化第一步的时候发现自己依赖了 ClassA,就会尝试从第一级缓存 singletonObjects 去获取 ClassA 的实例。因为 ClassA 这时候还没有完全创建完毕,所以第一级缓存中不存在,同样第二级缓存中也不存在。当尝试访问第三级缓存时,因为 ClassA 已经提前暴露了,所以 ClassB 能够通过 singletonFactories 拿到 ClassA 对象并顺利完成所有初始化流程。
ClassB 对象创建完成之后会把自己放到第一级缓存中,这时候 ClassA 就能从第一级缓存中获取 ClassB 的实例,进而完成 ClassA 的所有初始化流程。
讲到这里,相信你能理解为什么构造器注入无法解决循环依赖问题了。这是因为构造器注入过程是发生在创建 Bean 的第一个步骤 createBeanInstance 中,而这个步骤中还没有调用 addSingletonFactory 方法完成第三级缓存的构建,自然也就无法从该缓存中获取目标对象。
总结
今天我们系统分析了 Spring 为开发人员提供的循环依赖解决方案。虽然基于 Spring 框架实现 Bean 的依赖注入比较简单,但也存在一些最佳实践,尤其是在使用 Spring 的过程中,经常碰到的循环依赖问题,需要开发人员对框架的底层原理有一定的了解。
于是基于 Spring 提供的三层缓存机制,我们对这一主题进行了源码级的深入分析。从源码中,我们发现 Spring 中解决循环依赖的核心思想在于:基于 Bean 的作用域和生命周期,把 Bean 的创建过程拆分成“实例化”和“属性填充”这两个阶段,确保 Bean 对象能够在第一个阶段就能够尽早暴露出来供其他 Bean 进行使用。
而在 Spring 所提供的三种依赖注入类型中,也只有 Setter 方法注入能够解决循环依赖问题,原因就在于这种注入类型生效的时机恰恰就在“实例化”阶段之后、“属性填充”阶段之前。