前面的文章一直在研究 Spring 创建 Bean 的整个过程, 创建一个 bean 是一个非常复杂的过程, 而其中最难以理解的就是对循环依赖的处理, 本文就来研究一下 spring 是如何处理循环依赖的.
1. 什么是循环依赖
不管之前是否研究过循环依赖, 这里先对这个知识做一点回顾.
循环依赖就是循环引用, 就是两个或者多个 bean 相互之间的持有对方, 比如 A 引用 B,B 引用 C,C 引用 A, 则它们最终反映为一个环, 参考下图:
了解了什么是循环依赖之后, 我们知道这是一种不可避免会出现的情况, 那作为 Bean 容器的 Spring 又是怎么处理这一问题呢? 我们接着往下看.
2. Spring 如何处理循环依赖
Spring 容器循环依赖包括构造器循环依赖和 setter 循环依赖, 那 Spring 容器又是如何解决循环依赖的呢? 我们来测试一下, 首先我们来定义循环引用类:
- public class TestA{
- private TestB testB;
- public void a(){
- testB.b();
- }
- public TestB getTestB(){
- return testB;
- }
- public void setTestB(TestB testB){
- this.testB = testB;
- }
- }
- public class TestB{
- private TestC testC;
- public void b(){
- testC.c();
- }
- public TestC getTestC(){
- return testC;
- }
- public void setTestC(TestC testC){
- this.testC = testC;
- }
- }
- public class TestC{
- private TestA testA;
- public void c(){
- testA.a();
- }
- public TestA getTestA(){
- return testA;
- }
- public void setTestA(TestA testA){
- this.testA = testA;
- }
- }
在 Spring 中将循环依赖的处理分成了 3 种情况:
2.1 构造器循环依赖处理
这表示通过构造器注入构成的循环依赖, 此依赖是无法解决的, 只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖.
比如在创建 TestA 类时, 构造器需要 TestB 类, 那么将去创建 TestB, 在创建 TestB 类时又发现需要 TestC 类, 则又去创建 TestC, 最终在创建 TestC 时发现又需要 TestA, 从而形成一个环, 没办法创建.
Spring 容器将每一个正在创建的 bean 标识符放在一个 "当前创建 bean 池" 中, bean 标识符在创建过程中将一直保持在这个池中, 因此如果在创建 bean 的过程中发现自己已经在 "当前创建 bean 池" 里时, 则抛出 BeanCurrentlyInCreationException 异常表示出现了循环依赖; 而对于创建完毕的 bean 将从 "当前创建 bean 池" 中清除掉, 这个 "当前创建 bean 池" 实际上是一个 ConcurrentHashMap, 即 DefaultSingletonBeanRegistry 中的 singletonsCurrentlyInCreation.
我们通过一个直观的测试用例来进行分析:
xml 配置如下:
- <bean id = "testA" class = "xxx.xxx">
- <constructor-arg index = "0" ref = "testB"/>
- </bean>
- <bean id = "testB" class = "xxx.xxx">
- <constructor-arg index = "0" ref = "testC"/>
- </bean>
- <bean id = "testC" class = "xxx.xxx">
- <constructor-arg index = "0" ref = "testA"/>
- </bean>
创建测试用例:
- public static void main(String[] args) {
- try{
- new ClassPathXmlApplicationContext("beans.xml");
- }catch (Exception e){
- e.printStackTrace();
- }
- }
这个执行过程中会抛出异常 BeanCurrentlyInCreationException, 通过 debug 可以快速找到异常抛出的位置在 getSingleton()方法中的 beforeSingletonCreation():
- protected void beforeSingletonCreation(String beanName) {
- if (!this.inCreationCheckExclusions.containsKey(beanName) &&
- this.singletonsCurrentlyInCreation.put(beanName, Boolean.TRUE) != null) {
- throw new BeanCurrentlyInCreationException(beanName);
- }
- }
由此可知, Spring 在对构造器循环依赖的处理策略上是选择了直接抛异常, 而且对循环依赖的判断是发生在加载单例时调用 ObjectFactory 的 getObject()方法实例化 bean 之前.
2.2 setter 循环依赖处理
这个表示通过 setter 注入方式构成的循环依赖. 对于 setter 注入造成的循环依赖 Spring 是通过提前暴露刚完成构造器注入但还未完成其他步骤 (如 setter 注入) 的 bean 来完成的, 而且只能解决单例作用域的 bean 循环代码, 我们这里来详细分析一下 Spring 是如何处理的.
关于这部分的处理逻辑, 在 AbstractAutowireCapableBeanFactory 的 doCreateBean()方法中有一段代码, 如下所示:
- // Eagerly cache singletons to be able to resolve circular references
- // even when triggered by lifecycle interfaces like BeanFactoryAware.
- boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
- isSingletonCurrentlyInCreation(beanName));
- if (earlySingletonExposure) {
- if (logger.isDebugEnabled()) {
- logger.debug("Eagerly caching bean'" + beanName +
- "'to allow for resolving potential circular references");
- }
- // 为避免后期循环依赖, 可以在 bean 初始化完成前将创建实例的 ObjectFactory 加入工厂
- addSingletonFactory(beanName, new ObjectFactory<Object>() {
- public Object getObject() throws BeansException {
- // 对 bean 再一次依赖引用, 主要应用 SmartInstantiationAwareBeanPostProcessor,
- // 其中我们熟知的 AOP 就是在这里将 advice 动态织入 bean 中, 若没有则直接返回 bean, 不做任何处理
- return getEarlyBeanReference(beanName, mbd, bean);
- }
- });
- }
这段代码不是很复杂, 但是如果是一开始看这段代码的时候不太容易理解其作用, 因为仅仅从函数中去理解是很难弄懂其中的含义, 这里需要从全局的角度去思考 Spring 的依赖解决办法才能更好理解.
earlySingletonExposure: 从字面的意思理解就是是否提早曝光单例
mbd.isSingleton(): 是否是单例
this.allowCircularReference: 是否允许循环依赖, 在 AbstractRefreshableApplicationContext 中提供了设置函数, 可以通过硬编码的方式进行设置或者可以通过自定义命名空间进行配置, 硬编码的方式代码如下:
- ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml");
- bf.setAllowBeanDefinitionOverriding(false);
isSingletonCurrentlyInCreation(beanName): 该 bean 是否在创建中. 在 Spring 中, 会有一个专门的属性 (类 DefaultSingletonBeanRegistry 中的 singletonsCurrentlyInCreation) 来记录 bean 的加载状态, 在 bean 开始创建前会将 beanName 记录在属性中, 在 bean 创建结束后会将 beanName 从属性中移除. 我们跟随代码一路走来或许对这个属性的记录并没有多少印象, 不经会拍脑门问这个状态是在哪里记录的呢? 不同 scope 的记录位置并不一样, 我们以 singleton 为例, 在 singleton 下记录属性的函数是在 DefaultSingletonBeanRegistry 类的 getSingleton(String beanName,ObjectFactory singletonFactory)函数中的 beforeSingletonCreation(beanName)和 afterSingletonCreation(beanName)中, 在这两段函数中分别通过 this.singlesCurrentlyInCreation.add(beanName)与 this.singlesCurrentlyInCreation.remove(beanName)来进行状态的记录与移除.
经过上面的分析可以知道变量 earlySingletonExposure 为是否是单例, 是否允许循环依赖, 是否对应的 bean 正在创建这三个条件的综合. 当这 3 个条件都满足时会执行 addSingletonFactory 操作, 那么加入 SingletonFactory 的作用又是什么呢?
这里还是用一个最简单的 AB 循环依赖为例, 类 A 中含有属性类 B, 而类 B 中又会含有属性类 A, 那么初始化 beanA 的过程如下图所示:
上图展示了创建 beanA 的流程, 图中我们看到, 在创建 A 的时候首先会记录类 A 所对应的 beanName, 并将 beanA 的创建工厂加入缓存中, 而在对 A 的属性填充也就是调用 populate()方法的时候又会再一次的对 B 进行递归创建. 同样的, 因为在 B 中同样存在 A 属性, 因此在实例化 B 时的 populate()方法中又会再次地初始化 A, 也就是图形的最后, 调用 getBean(A). 关键就是在这里, 在这个 getBean()函数中并不是直接去实例化 A, 而是先去检测缓存中是否有已经创建好的对应 bean, 或者是否有已经创建好的 ObjectFactory, 而此时对于 A 的 ObjectFactory 我们早已创建好了, 所以便不会再去向后执行, 而是直接调用 ObjectFactory 去获取 A.
到这里基本可以理清 Spring 处理循环依赖的解决办法, 这里再从代码层面总结一下:
在创建 bean 的过程中, 实例化 bean 结束之后, 属性注入之前, 有一段这样的代码 (代码位置为 AbstractAutowireCapableBeanFactory 类中的 doCreateBean() 方法中 bean 实例化之后):
- boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
- isSingletonCurrentlyInCreation(beanName));
- if (earlySingletonExposure) {
- if (logger.isDebugEnabled()) {
- logger.debug("Eagerly caching bean'" + beanName +
- "'to allow for resolving potential circular references");
- }
- addSingletonFactory(beanName, new ObjectFactory<Object>() {
- public Object getObject() throws BeansException {
- return getEarlyBeanReference(beanName, mbd, bean);
- }
- });
- }
这段代码前面也说过, 主要做的事情是在 addSingletonFactory()方法中, 即在必要的时候将创建 bean 的 ObjectFactory 添加到缓存中. 再结合前面的例子来看, 在第一次创建 beanA 时, 这里是会将 ObjectFactory 加入到 singletonFactories 中, 当创建 beanB 时, 在对 beanB 的属性注入时又会调用 getBean()去获取 beanA, 同样是前面说到过, 会先去缓存获取 beanA, 这时候是可以获取到刚才放到缓存中的 ObjectFactory 的, 这时候就会把实例化好但是还未完成属性注入的 beanA 找出来注入到 beanB 中去, 这样就解决了循环依赖的问题, 需要结合下面的代码细品一下.
- protected <T> T doGetBean(
- final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
- throws BeansException {
- final String beanName = transformedBeanName(name);
- Object bean;
- // Eagerly check singleton cache for manually registered singletons.
- Object sharedInstance = getSingleton(beanName);
- ...
- }
- protected Object getSingleton(String beanName, boolean allowEarlyReference) {
- Object singletonObject = this.singletonObjects.get(beanName);
- if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
- synchronized (this.singletonObjects) {
- singletonObject = this.earlySingletonObjects.get(beanName);
- if (singletonObject == null && allowEarlyReference) {
- ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
- if (singletonFactory != null) {
- singletonObject = singletonFactory.getObject();
- this.earlySingletonObjects.put(beanName, singletonObject);
- this.singletonFactories.remove(beanName);
- }
- }
- }
- }
- return (singletonObject != NULL_OBJECT ? singletonObject : null);
- }
2.3 prototype 范围的依赖处理
对于 "prototype" 作用域的 bean,Spring 容器并不会对其进行缓存, 因此无法提前暴露一个创建中的 bean, 所以也是通过抛出异常的方式来处理循环依赖, 这里仍然是用一个 demo 来测试一下代码是在哪抛的异常.
配置文件:
- <bean id = "testA" class = "xxx" scope = "prototype">
- <property name = "testB" ref = "testB"/>
- </bean>
- <bean id = "testB" class = "xxx">
- <property name = "testC" ref = "testC"/>
- </bean>
- <bean id = "testC" class = "xxx">
- <property name = "testA" ref = "testA"/>
- </bean>
测试代码:
- public static void main(String[] args) {
- try{
- ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
- System.out.println(ctx.getBean("testA"));
- }catch (Exception e){
- e.printStackTrace();
- }
- }
同样通过断点我们可以定位异常的抛出位置是在 AbstractBeanFactory 类的 doGetBean 方法中, 在方法开始获取缓存失败之后(prototype 不会加入到缓存中), 会首先判断 prototype 的 bean 是否已创建, 如果是就认为存在循环依赖, 抛出 BeanCurrentlyInCreationException 异常.
- if (isPrototypeCurrentlyInCreation(beanName)) {
- throw new BeanCurrentlyInCreationException(beanName);
- }
3. 总结
Spring 中对于循环依赖的处理存在 3 中场景:
构造器循环依赖处理;
setter 循环依赖处理;
prototype 范围的依赖处理;
其中对于构造器和 prototype 范围的循环依赖, Spring 是直接抛出异常. 而对于单例的 setter 循环依赖, Spring 是通过在 bean 加载过程中提前将 bean 的 ObjectFactory 加入到 singletonFactories 这个缓存用的 map 中来解决循环依赖的.
posted on 2020-04-27 20:12 木瓜芒果 阅读(...) 评论(...) 编辑 收藏
来源: https://www.cnblogs.com/volcano-liu/p/12291501.html