关于java:从设计角度深入分析-Spring-循环依赖的解决思路

3次阅读

共计 7050 个字符,预计需要花费 18 分钟才能阅读完成。

前言

Spring 的循环依赖曾经被说烂了,可能很多人也看吐了。但很多博客上说的还是不够分明,没有残缺的表白出 Spring 的设计目标。只介绍了 What,对于 Why 的介绍却不太够。

本文会从设计角度,一步一步详细分析 Spring 这个“三级缓存”的设计准则,说说为什么要这么设计。

Bean 创立流程

Spring 中的每一个 Bean 都由一个 BeanDefinition 创立而来,在注册实现 BeanDefinition 后。会遍历 BeanFactory 中的 beanDefinitionMap 对所有的 Bean 调用 getBean 进行初始化。

简略来说,一个 Bean 的创立流程次要分为以下几个阶段:

  1. Instantiate Bean – 实例化 Bean,通过默认的构造函数或者构造函数注入的形式
  2. Populate Bean – 解决 Bean 的属性依赖,能够是 Autowired 注入的,也能够是 XML 中配置的,或者是手动创立的 BeanDefinition 中的依赖(propertyValues)
  3. Initialize Bean – 初始化 Bean,执行初始化办法,执行 BeanPostProcessor。这个阶段是各种 Bean 的后置解决,比方 AOP 的代理对象替换就是在这个阶段

在实现下面的创立流程后,将 Bean 增加到缓存中 – singletonObjects,当前在 getBean 时先从缓存中查找,不存在才创立。

Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

先抛开 Spring 的源码不谈,先看看依照这个创立流程执行会遇到什么问题

1. Instantiate Bean

首先是第一阶段 – 实例化,就是调用 Bean Class 的构造函数,创立实例而已,没啥可说的。至于一些获取 BeanDefinition 构造方法的逻辑,不是循环依赖的重点。

2. Populate Bean

第二阶段 – 填充 Bean,其目标是查找以后 Bean 援用的所有 Bean,利用 BeanFactory 获取这些 Bean,而后注入到以后 Bean 的属性中。

失常状况下,没有循环援用关系时没什么问题。比方当初正在进行 ABean 的 populate 操作,发现了 BBean 的援用,通过 BeanFactory 去 getBean(BBean) 就能够实现,哪怕当初 BBean 还没有创立,在 getBean 中实现初始化也能够。实现后将 BBean 增加到已创立实现的 Bean 缓存中 – singletonObjects。

最初再将获取的 BBean 实例注入到 ABean 中就实现了这个 populate 操作,看着还挺简略。

此时援用关系产生了点变动,ABean 也依赖了 BBean,两个 Bean 的援用关系变成了相互援用,如下图所示:
再来看看当初 populate 该怎么执行:

首先还是先初始化 BBean,而后发现了 Bbean 援用的 ABean,当初 getBean(ABean),发现 ABean 也没有创立,开始执行对 ABean 的创立:先实例化,而后对 ABean 执行 populate,可 populate 时又发现了 ABean 援用了 BBean,可此时 BBean 还没有创立实现,Bean 缓存中也并不存在。这样就呈现死循环了,两个 Bean 互相援用,populate 操作齐全没法执行。

其实解决这个问题也很简略,呈现死循环的要害是两个 Bean 相互援用,populate 时另一个 Bean 还在创立中,没有创立实现。

只须要减少一个 中间状态的缓存容器 ,用来存储只执行了 instantiate 还未 populate 的那些 Bean。到了 populate 阶段时, 如果残缺状态缓存中不存在,就从中间状态缓存容器查找一遍,这样就防止了死循环的问题。

如下图所示,减少了一个中间状态的缓存容器 – earlySingletonObjects,用来存储刚执行 instantiate 的 Bean,在 Bean 实现创立后,从 earlySingletonObjects 删除,增加到 singletonObjects 中。

回到下面的例子,如果在 ABean 的 populate 阶段又发现了 BBean 的援用,那么先从 singletonObjects 查找,如果不存在,持续从 earlySingletonObjects 中查找,找到当前注入到 ABean 中,而后 ABean 创立实现(BeanPostProcessor 待会再说)。当初将 ABean 增加到 singletonObjects 中,接着返回到创立 BBean 的过程。最初把返回的 ABean 注入到 BBean 中,就实现了 BBean 的 populate 操作,如下图所示:

循环依赖的问题,就这么轻易的解决了,看着很简略,只是加了一个中间状态而已。但除了 instantiate 和 populate 阶段,还有最初一个执行 BeanPostProcessor 阶段,这个阶段可能会加强 / 替换原始 Bean

3. Initialize Bean

这个阶段分为执行初始化办法 – initMethod,和执行 BeanFactory 中定义的 BeanPostProcessor(BPP)。执行初始化办法没啥可说的,重点看看 执行 BeanPostProcessor 局部。

BeanPostProcessor 算是 Spring 的灵魂接口了,很多扩大的操作和性能都是通过这个接口,比方 AOP。在 populate 实现之后,Spring 会对 Bean 程序执行所有的 BeanPostProcessor,而后返回 BeanPostProcessor 返回的新 Bean 实例(可能有批改也可能没批改)

//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
    throws BeansException {

    Object result = existingBean;
    // 对以后 Bean 程序的执行所有的 BeanPostProcessor,并返回
    for (BeanPostProcessor processor : getBeanPostProcessors()) {Object current = processor.postProcessBeforeInitialization(result, beanName);
        if (current == null) {return result;}
        result = current;
    }
    return result;
}

Spring 的 AOP 加强性能,也是利用 BeanPostProcessor 实现的,如果该 Bean 有 AOP 加强的配置,那么执行完 BeanPostProcessor 之后就会返回一个新的 Bean,最初存储到 singletonObjects 中的也是这个加强之后的 Bean

可咱们下面的“中间状态缓存”解决方案中,存储的却只是刚执行实现 instantiate 的 Bean。如果在下面循环依赖的例子中,populate ABean 时因为 BBean 只实现了实例化,所以会从 earlySingletonObjects 获取只实现初始化 的 BBean 并注入到 ABean 中。

如果 BBean 有 AOP 的配置,那么此时注入到 ABean 中的 只是一个只实例化未 AOP 加强的对象。当 BBean 执行 BeanPostProcessor 后,又会创立一个加强的 BBean 实例,最终增加到 singletonObjects 中的,是加强的 BBean 实例,而不是那个刚实例化的 BBean 实例

如下图所示,BBean 中注入的是黄色的 只实现了初始化的 ABbean,而最终增加到 singletonObjects 却是执行完 AOP 的加强 ABean 实例:

因为 populate 之后还有一步 BeanPostProcessor 的加强,导致咱们下面的解决方案有效了。但也不是齐全无解,如果能够让增强型的 BeanPostProcessor 提前执行,而后增加到“中间状态的缓存容器”中,是不是也能够解决问题?

不过并不是所有的 Bean 都有 AOP(及其他执行 BPP 后返回新对象)的需要,如果让所有 Bean 都提前执行 BeanPostProcessor 并不适合。

所以这里能够采纳一种“提早解决”的形式,在两头减少一层 Factory,在这个 Factory 中实现“提前执行”的操作。

如果没有提前调用某个 Bean 的“提早解决”Factory,那么就不会导致提前执行 BeanPostProcessor,只有循环依赖场景下,才会呈现这种只实现初始化却未齐全创立的 Bean,才会调用这个 Factory。这个 Factory 的模式就叫提早解决,如果不调用 Factory 就不会提前执行 BPP。

instantiate 实现后,把这个 Factory 增加到“中间状态的缓存容器”中;这样当产生循环依赖时,原先获取的中间状态 Bean 实例就会变成这个 Factory,此时执行这个 Factory 就能够实现“提前执行 BeanPostProcessor”的操作,并且获取执行后的新 Bean 实例

当初减少一个 ObjectFactory,用来实现提早解决:

public interface ObjectFactory<T> {T getObject() throws BeansException;

}

而后再创立一个 singletonFactories,作为咱们新的中间状态缓存容器,不过这个容器存储的并不是 Bean 实例,而是创立 Bean 的实现代码

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

当初再写一个“提前执行”BeanPostProcessor 的 ObjectFactory,增加到 singletonFactories 中。

// 实现 bean 的 instantiate 后
// 创立一个对该 Bean 提前执行 BeanPostProcessor 的 ObjectFactory
// 最初增加到 singletonFactories 中
addSingletonFactory(beanName, 
                    () -> getEarlyBeanReference(beanName, mbd, bean)
                   );

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);
            ......
        }
    }
}

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        // 提前执行 BeanPostProcessor
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

再次回到下面循环依赖的例子,如果在对 ABean 执行 populate 时,又发现了 BBean 的援用,那么此时先从咱们这个新的 提早解决 + 提前执行的缓存容器 中查找,不过当初找到的曾经不再是一个 BBean 实例了,而是咱们下面定义的那个 getEarlyBeanReference 的 ObjectFactory,通过调用 ObjectFactory.getObject() 来获取提前执行 BeanPostProcessor 的这个 ABean 实例。

如下图所示,在对 ABean 执行 populate 时,发现了对 BBean 的援用,那么间接从 singletonFactories 中查找 BBean 的 ObjectFactory 并执行,获取 BeanPostProcessor 加强 / 替换后的新 Bean

当初因为咱们的中间状态数据从 Bean 实例变成了 ObjectFactory,所以还须要在初始化之后,再检查一下 singletonFactories 是否有以后 Bean,如果有的化须要手动调用一下 getObject 来获取最终的 Bean 实例。

通过 “提早执行”+“提前执行” 两个操作,终于解决了这个循环依赖的问题。不过提前执行 BeanPostProcessor 会导致最终执行两遍 BeanPostProcessor,这个执行两遍的问题还须要解决。

这个问题解决倒还算简略,在那些会更换原对象实例的 BeanPostProcessor 中减少一个缓存,用来存储曾经加强的 Bean,每次调用该 BeanPostProcessor 的时候,如果缓存中曾经存在那就阐明创立过了,间接返回上次创立的即可。Spring 为此还独自设计了一个接口,命名也很形象 – SmartInstantiationAwareBeanPostProcessor

如果你定义的 BeanPostProcessor 会加强并替换原有的 Bean 实例,肯定要实现这个接口,在实现内进行缓存,防止反复加强

貌似当初问题曾经解决了,一开始设计的 earlySingletonObjects 也不须要了,间接应用咱们这个中间状态缓存工厂 – singletonFactories 就搞定了问题。

不过……如果依赖关系再简单一点,比方像上面这样,ABean 中有两个属性都援用了 BBean

那么在对 ABean 执行 populate 时,先解决 refB 这个属性;此时从 singletonFactories 中查找到 BBean 的这个提前执行 BeanPostProcessor 的 ObjectFactory,调用 getObject 获取到提前执行 BeanPostProcessor 的 BBean 实例,注入到 refB 属性中。

那到了 refB1 这个属性时,因为 BBean 还是一个没有创立实现的状态(singletonObjects 中不存在),所以依然须要获取 BBean 的 ObjectFactory,执行 getObject,导致又对 BBean 执行了一遍 BeanPostProcessor。

为了解决这个屡次援用的问题,还是须要有一个中间状态的缓存容器 – earlySingletonObjects。不过这个缓存容器和一开始提到的那个 earlySingletonObjects 有一点点不同;一开始提到的 earlySingletonObjects 是存储只执行了 instantiate 状态的 Bean 实例,而咱们当初存储的是执行 instantiate 之后,又提前执行了 BeanPostProcessor 的那些 Bean。

在提前执行了 BeanPostProcessor 之后,将返回的新的 Bean 实例也增加到 earlySingletonObjects 这个缓存容器中。这样就算处于中间状态时有屡次援用(屡次 getBean),也能够从 earlySingletonObjects 获取曾经执行完 BeanPostProcessor 的那个 Bean,不会造成反复执行的问题。

总结

回顾一下下面一步步解决循环依赖的流程,最终咱们通过一个 提早解决 的缓存容器,加一个 提前执行结束 BeanPostProcessor 的中间状态容器 就完满解决了循环依赖的问题

至于 singletonObjects 这个缓存容器,它只用来存储所有创立实现的 Bean,和解决循环依赖关系并不大。

至于这个解决机制,叫不叫“三级缓存”……见仁见智吧,Spring 在源码 / 正文中也没有(3-level cache 之类的字眼)。而且要害的循环依赖解决,只是“二级”(提早解决的 Factory + 提前执行 BeanPostProcessor 的 Bean),所谓的“第三级”是应该是指 singletonObjects。

上面用一张图,简略总结一下解决循环依赖的外围机制:

不过提前执行 BeanPostProcessor 这个操作,算不算突破了原有的设计呢?本来 BeanPostProcessor 可是在创立 Bean 的最初阶段执行的,可当初为了解决循环依赖,给挪动到 populate 之前了。尽管是一个不太优雅的设计,但用来解决循环依赖也不错。

只管 Spring 反对了循环依赖(仅反对属性依赖形式,构造方法依赖不反对,因为实例化都实现不了),但理论我的项目中,这种循环依赖的关系往往是不合理的,应该从设计上就防止。

原创不易,禁止未受权的转载。如果我的文章对您有帮忙,就请点赞 / 珍藏 / 关注激励反对一下吧❤❤❤❤❤❤

正文完
 0