Java Spring处理循环依赖详解
作者:Samson_bu 时间:2022-07-14 05:06:16
01-前言:什么是循环依赖?
首先,我们先明确下依赖的定义。 如果一个 Bean bar 的属性,引用了容器中的另外一个 Bean foo,那么称 foo 为 bar 的依赖,或称 bar 依赖 foo。 如果用代码表示,可以表示为:
@Component("foo")
public Class Foo {
@Autowired
private Bar bar; // 称 foo 依赖 bar
}
@Component("bar")
public Class Bar {
@Autowired
private Baz baz; // bar 依赖 baz
}
其次,循环引用的概念是指多个 Bean 之间的依赖关系形成了环。 接着上面的例子,如果 baz 依赖 foo 或 bar 都将形成循环依赖。
@Component("baz")
public class Baz {
@Autowired
private Foo foo; // 形成了循环依赖,baz -> (依赖) foo -> bar -> baz ...
}
02-Spring 如何处理循环依赖?
在之前的文章中,我跟大家一块学习了 Spring 创建 Bean 过程的源码。 我们知道:在 createBeanInstance 阶段,需要解决构造器、工厂方法参数的依赖; 在 populateBean 阶段,需要解决类属性中对其他 Bean 的依赖。 这其实对应了 Spring 中支持的两种依赖注入方式,基于构造器的依赖注入和基于 setter 方法的依赖注入,分别对应前面的两种情况。
接下来,我会通过上节介绍的示例,来分情况讨论产生循环依赖的场景。 为了使讨论过程更清楚、更简洁,我会让 foo 依赖 bar,而 bar 依赖 foo。 在接下来的描述中,我假设 Spring 会先创建 Bar 的对象,再创建 Foo 的对象。 针对不同的依赖情况,可以分为四种场景:
第一种场景,Bar 在构造器参数中依赖 Foo,Foo 在构造器参数中依赖 Bar。这种场景下,依赖的注入发生在 Bar 和 Foo 的实例化阶段。
第二种场景,Bar 在构造器参数中依赖 Foo,Foo 通过 setter 函数依赖 Bar。这种场景下,Bar 中注入 Foo 发生在实例化阶段,Foo 中注入 Bar 发生在属性填充阶段。
第三种场景,Bar 通过 setter 函数依赖 Foo,Foo 在构造器参数中依赖 Bar。这种场景下,Bar 中输入 Foo 发生在属性填充阶段,而 Foo 中注入 Bar 发生在实例化阶段。
第四种场景,Bar 通过 setter 函数依赖 Foo,Foo 通过 setter 函数依赖 Bar。 这种场景下,依赖注入均发生在属性填充阶段。
在具体分析上述四种场景之前,先说下结论: Spring 可以解决场景三、四中出现循环依赖的情况,而第一、二种场景,Spring 无法解决,需要重构依赖或者延迟延迟依赖注入的时机(例如使用 @Lazy 等)。 细心的读者可能会问第二种、第三种场景有什么不同呢? 其实第二、第三种场景本质上是同一种情况,唯一的不同是实例化的先后顺序。 结合这个信息,可以得出,先创建的类以构造器参数方式依赖其他 Bean,则会发生循环依赖异常。 反过来,如果先创建的类以 setter 方式依赖其他 Bean,则不会发生循环依赖异常。
接下来,我会详细分析每一种场景,并指出抛循环依赖异常的时机。
首先,所有的单例 Bean 会在容器启动后被创建 ConfigurableListableBeanFactory#preInstantiateSingletons,即所谓的 “eager registration of singletons” 过程。
第一种场景,会先触发 Bar 类的实例 bar 的创建。在 createBeanInstance 阶段,会通过 ConstructorResolver#autowireConstructor 来创建实例。 ConstructorResolver#createArgumentArray 会解析构造器中的参数,并处理对其他 Bean 依赖的引用 ConstructorResolver#resolveAutowiredArgument。 处理依赖的方式就是通过 DefaultListableBeanFactory#resolveDependency 来查找符合条件的 Bean,最终还是通过 AbstractBeanFactory#getBean 来从容器中取。 当通过 getBean("bar") 来触发 Spring 创建 bar 时,在实例化阶段,根据构造器参数来 getBean("foo") 并触发 foo 的创建。 在 foo 的实例化过程与 bar 的是完全一样的,最终 getBean("bar")。这是容器中的 bar 还没有创建好,所以会再次触发创建过程。 在真正创建过程之前,在 DefaultSingletonBeanRegistry#getSingleton 中会有一次检查,DefaultSingletonBeanRegistry#beforeSingletonCreation 如果发现要创建的 bean 正在创建过程中,则抛出异常。
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'bar': Requested bean is currently in creation: Is there an unresolvable circular reference?
第二种场景,同样先构建 bar 实例。与第一种场景不同之处在于 foo 创建时,它的 createBeanInstance 阶段能够执行完毕。 原因是 foo 只有一个无参构造器(即默认构造器),不需要注入其他依赖。 foo 的 createBeanInstance 阶段执行完毕后,会进入 populateBean 阶段。 在这个阶段中,AutowiredAnnotationBeanPostProcessor#postProcessProperties 会处理 setter 函数依赖的 Bean。 大致处理过程为:AutowiredAnnotationBeanPostProcessor 识别到 foo 中包含需要注入依赖的 setter 函数,将其映射为 AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement 对象。 然后调用 AutowiredMethodElement#inject 方法注入依赖。 在 inject 方法中,会调用 DefaultListableBeanFactory#resolveDependency 来查找对应的依赖。 到这里为止,后续的过程与第一种场景完全一致了。 从容器中尝试获取 bar,发现不存在,会出发 bar 的再次创建,最终在 DefaultSingletonBeanRegistry#beforeSingletonCreation 中抛出异常。
第三种场景,同样先构建 bar 实例。由于它只包含一个默认构造器,所以它的 createBeanInstance 阶段会顺利完成,然后进入 populateBean 阶段。 当你仔细回看一下 Spring 创建 Bean 过程的源码,你会发现下面这段代码:
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
这段逻辑发生在 createBeanInstance 之后,尚未进入 populateBean 之前。 这里其实就是 Spring 解决循环依赖机制的核心点之一,这里我暂且不深入介绍,后面会有详细的分析。
继续前面的分析。bar 实例创建进入到 populateBean 阶段后,会检查其自身依赖情况,然后注入对应的依赖 Bean。 这里的处理逻辑依然是 AutowiredAnnotationBeanPostProcessor#postProcessProperties 来处理。 当尝试注入 foo 时,会出发 foo 实例的创建过程。foo 通过构造器依赖 bar,因此在其 createBeanInstance 阶段,会通过 ConstructorResolver#autowireConstructor 完成依赖注入。 此时通过 getBean("bar") 从容器中尝试获取 bar 时,能够“获取到”。 注:这里为什么不会出发 bar 的创建,反而能够直接得到 bar 对象呢?上面的获取到我加了引号,它其实获得的并不是一个完整、可用的 bar。 它获得的是通过 earlySingletonExposure 提前暴露出的对象。 这个过程在后面介绍 * 缓存时会详细介绍。
篇幅原因,第四种场景我不在这里继续分析,感兴趣的读者可以自己尝试分析下。 简单提示下,它的过程有点像第三种场景前半段、第二种场景的后半段结合起来。
在上述四种场景下,第一种情况,依赖双方都是通过构造器依赖对方,这种情况下 Spring 是无法处理的。 而且,我认为出现这一情况,属于是设计上的缺陷,应当通过重新设计依赖关系来解决,例如可以将基于构造器的注入修改为基于 setter 的注入,或者通过 @Lazy 将依赖的初始化延迟到使用时。 通过 Foo、Bar 类来举例说明。
@Component
public class Foo {
private Bar bar;
@Autowired
public Foo(@Lazy Bar bar) {
this.bar = bar;
}
}
@Component
public class Bar {
private Foo foo;
@Autowired
public Bar(Foo foo) { // 或者将对 foo 的依赖,注解为 @Lazy 表示使用时才初始化
this.foo = foo;
}
}
另一种修改方式就是,将第一种情况,修改为第二种情况,即:
@Component
public class Bar {
private Foo foo;
@Autowired
public void setFoo(Foo foo) {
this.foo = foo;
}
}
03-Spring 中解决循环依赖的 * 缓存
Spring 中设计了一个 * 缓存用来解决前面介绍的循环依赖问题的处理。 * 缓存包括:
singletonObjects,为一级缓存,保存了 beanName -> bean instance 的映射关系。存放的是完全可用的单例 Bean 对象。
earlySingletonObjects,为二级缓存,保存了 beanName -> bean instance 的映射关系。 在一级、二级缓存都没有发现目标对象,但 * 缓存中存在 ObjectFactory 对象时,调用 ObjectFactory#getObject 创建实例,放入二级缓存,删除 * 缓存中的 ObjectFactory 对象。
singletonFactories,为 * 缓存,保存了 beanName -> ObjectFactory 的映射关系。 在 doCreateBean 时,会向这个 map 中添加
beanName: () -> getEarlyBeanReference(beanName, mbd, bean)
的映射关系,value 是一个函数式接口 ObjectFactory。
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
大概了解了 Spring 中的 * 缓存后,我们再回过头来看一下 AbstractBeanFactory#getBean 过程。 它的实际工作是在 AbstractBeanFactory#doGetBean 中完成的。 doGetBean 方法的具体实现可以简化、抽象为:
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 缓存中存在
/**
* 如果 beanName 是一个 FactoryBean,则获取对应的 Bean
* 如果 beanName 是一个普通的 Bean,则返回这个 Bean 本身
*/
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// 缓存中不存在
// 创建对象
if (mbd.isSingleton()) {
/**
* 这里的 getSingleton 是 getSingleton(beanName) 的重载版本
* 它接受一个 beanName 和 一个 ObjectFactory 作为参数
* 调用 ObjectFactory#getObject 产生一个实例
* 并通过 addSingleton(beanName, singletonObject); 将实例添加到 singletonObjects 中
* 这里 createBean 的代码就是前面提到的 Spring 创建 Bean 实例的过程 doCreateBean
*/
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
});
/**
* 同前面分支中的作用一样
*/
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
/**
* 判断 Bean 类型与 requiredType 类型是否一直,一致则直接返回,不一致则需要进行转换
*/
return adaptBeanInstance(name, beanInstance, requiredType);
}
从上面的源码可以知道,当 DefaultSingletonBeanRegistry#getSingleton(beanName) 时,会先从多级缓存中取对象(可能是 bean instance,也可能是对应的 ObjectFactory)。 从多级缓存中取对象的源码如下:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
/**
* 判断第一级缓存中是否有完全可以可用的 Bean 实例,若有则返回;
* 若没有,则根据情况判断
* isSingletonCurrentlyInCreation(beanName) 检查的是在 `Set<String> singletonsCurrentlyInCreation` 集合中是否包含要获取的 Bean 实例
* beanName 只在调用 beforeSingletonCreation(String beanName) 时被添加到 singletonsCurrentlyInCreation 集合中
* beforeSingletonCreation 在创建 bean,即 doCreateBean 之前调用,在创建过程完成以后,调用 afterSingletonCreation 从集合中移除 beanName
*/
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
/**
* 执行到这个分支,其实说明 Bean 已经在创建过程中了,只不过是尚未完全可用(即一级缓存中没有)
* 检查二级缓存,是否包含指定的 Bean
*
* 二级缓存里的内容何时被添加或设置进来的呢?
* 我们可以检查下 earlySingletonObjects.put 方法都在哪里调用。
* 检查后发现,其实 earlySingletonObjects 就是在当前方法中设置的,我们接着往下看。
*/
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
/**
* 这里的 allowEarlyReference 的意思就是指是否允许在二级缓存中创建一个对象,即是否允许暴露未完全可用的对象
* 如果 allowEarlyReference 为假,则不会操作二级、 * 缓存,而仅检查一级缓存中是否有完全可用的 Bean 实例
* 这也意味着,不允许返回未完全可用状态的 Bean
*
* 当发现二级缓存中没有对象,同时又允许提前引用(即 allowEarlyReference 值为真)
* 则检查 * 缓存中是否有对应的 ObjectFactory 对象,若有,则调用它的 getObject 方法产生对象,然后将其放置到二级缓存中,同时删除 * 缓存中的对象工厂实例
* 若 * 缓存中也没有对象工厂实例,则说面 bean 还未创建
*/
synchronized (this.singletonObjects) {
/**
* 这里会进行一个 double-check,避免多线程间的线程安全问题
*/
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
/**
* * 缓存中存在对象工厂实例,则通过它产生一个 Bean 实例
* 加入到二级缓存中,同时删除 * 缓存中的对象工厂实例
*/
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
注:
isSingletonCurrentlyInCreation(beanName) 意味着什么呢?意味着对应的 Bean 在 doCreateBean 过程中,可能在 createBeanInstance \ populateBean \ initializeBean 阶段中。
在前面提到,createBeanInstance 后,Bean 会被添加到上述多级缓存中的第 * 缓存中,存入对象是 beanName -> objectFactory 映射关系。 当其他的 Bean 依赖当前 Bean 时,而且允许引用提前暴露的 Bean(即未完全可用的 Bean),会检查第二级缓存,如果没有还会检查第 * 缓存,并在得到对应 objectFactory 时,获得对象并将其从第 * 移动到第二级。
有些读者看到这里可能会有个疑问,那二级缓存中的对象什么时候删除呢? 我们再来回头看下 doGetBean 中的代码片段:
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
});
这里的 singleton
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
/**
* 检查 Bean 是否在创建过程中,避免重复创建,
* 不能解决的循环依赖也是在这里抛出异常
*/
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
/**
* 这里调用的其实就是 AbstractAutowireCapableBeanFactory#createBean
* 然后会执行 doCreateBean(三个阶段)
*/
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// Has the singleton object implicitly appeared in the meantime ->
// if yes, proceed with it since the exception indicates that state.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
throw ex;
}
finally {
afterSingletonCreation(beanName);
}
if (newSingleton) {
/**
* 这里说明一个 bean 创建过程的三个阶段都执行完毕了
*/
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}
/**
* 将 Bean 实例添加到第一级缓存
* 将第二级、第 * 缓存中的对象删除
*/
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
到这里为止,相信你对 Spring 创建 Bean 的过程以及处理循环依赖的机制能够有个大致的了解。 如果想了解的更深或跟多细节,可以自己单步调试下。 希望今天的内容能够对你有所帮助。如果有写得不对的地方,也请大家多批评指正。
来源:https://juejin.cn/post/7224055763543064631