图解 Spring 解决循环依赖,学不会接着砍!
Posted 流楚丶格念
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解 Spring 解决循环依赖,学不会接着砍!相关的知识,希望对你有一定的参考价值。
文章目录
SpringBean循环依赖
循环依赖的三种情况
首先,需要明确的是spring对循环依赖的处理有三种情况:
- 构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
- 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
- 非单例循环依赖:无法处理。
解决单例模式下的setter循环依赖
接下来,我们具体看看spring是如何处理第二种循环依赖的。
Spring单例对象的初始化大略分为三步:
- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象;
- populateBean:填充属性,这一步主要是多bean的依赖属性进行填充;
- initializeBean:调用spring xml中的init 方法。
从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一步、第二步。也就是构造器循环依赖和field循环依赖。 Spring为了解决单例的循环依赖问题,使用了三级缓存。
三级缓存的引入
Spring的三个级别的缓存声明如下(DefaultSingletonBeanRegistry类中
):
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
这三级缓存的作用分别是:
缓存 | 作用 |
---|---|
singletonFactories | 进入实例化阶段的单例对象工厂的cache (三级缓存); |
earlySingletonObjects | 完成实例化但是尚未初始化的,提前暴光的单例对象的Cache (二级缓存); |
singletonObjects | 完成初始化的单例对象的cache(一级缓存)。 |
在执行到doGetBean
方法中,会调用getSingleton()
方法来检查缓存中是否有这个bean单例,
我们进入getSingleton()
方法,追踪源码发现,我们在创建bean的时候,会首先从cache中获取这个bean,这个缓存就是sigletonObjects(一级缓存)
。
主要的调用方法是:
protected Object getSingleton(String beanName, boolean allowEarlyReference)
Object singletonObject = this.singletonObjects.get(beanName);
// isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName))
synchronized (this.singletonObjects)
singletonObject = this.earlySingletonObjects.get(beanName);
// allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象
if (singletonObject == null && allowEarlyReference)
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null)
singletonObject = singletonFactory.getObject();
// 从singletonFactories中移除,并放入earlySingletonObjects中。
// 其实也就是从三级缓存移动到了二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
return singletonObject;
从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。
这个cache的类型是ObjectFactory,定义如下:
public interface ObjectFactory<T>
T getObject() throws BeansException;
这个接口在AbstractBeanFactory里实现,并在核心方法doCreateBean()
引用下面的方法:
addSingletonFactory
方法内部实现如下:
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);
那么到这我们就能大概摸清三级缓存的作用了,进行三级缓存的这段代码发生在createBeanInstance之后,populateBean() 之前,也就是说单例对象此时已经被创建出来(调用了构造器)。
关键来了,也就是说,创建bean之后,初始化bean之前,这个bean对象(半成品)已经被生产出来了放在三级缓存里了,此时将这个对象提前曝光出来,让大家使用。
这样做有什么好处呢?下面我们带入解决一下现实中的循环依赖问题,就能更好的理解了。
解决循环依赖案例
让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。
出现这种循环依赖的代码如下所示:
class A
private B b;
@Autowired
public void setB(B b)
this.b = b;
class B
private A a;
@Autowired
public void setA(A a)
this.a = a;
我们按照三级缓存的设置看看能不能解决循环依赖的问题:
A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories(三级缓存)中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在拿到的的A对象就是完成了初始化的A对象。
上述分析的简化流程图如下所示:
深入探究原理
虽然我们解读了三级缓存的简单作用,但是里面大有深奥,一起来从一级缓存探究一下吧
一级缓存作用
首先我们模拟一级缓存作用,它保证单例对象仅被创建一次
- 第一次走
getBean("a")
流程后,最后会将成品 a 放入 singletonObjects 一级缓存 - 后续再走
getBean("a")
流程时,先从一级缓存中找,这时已经有成品 a,就无需再次创建
一级缓存发生循环依赖
但是一级缓存无法解决循环依赖问题,分析如下
- 无论是获取 bean a 还是获取 bean b,走的方法都是同一个 getBean 方法,假设先走
getBean("a")
- 当 a 的实例对象创建,接下来执行
a.setB()
时,需要走getBean("b")
流程,红色箭头 1 - 当 b 的实例对象创建,接下来执行
b.setA()
时,又回到了getBean("a")
的流程,红色箭头 2 - 但此时 singletonObjects 一级缓存内没有成品的 a,陷入了死循环
加一个二级缓存尝试解决循环依赖
上面一级缓存根本无法解决循环依赖问题,那么我们再加一个缓存这次看看能不能解决。
解决思路如下:
-
再增加一个 singletonFactories 缓存
在这次尝试里叫(第)二级缓存,在最后的Spring中,这个是三级缓存
-
在依赖注入前,即
a.setB()
以及b.setA()
将 a 及 b 的半成品对象(未完成依赖注入和初始化)放入此缓存 -
执行依赖注入时,先看看 singletonFactories 缓存中是否有半成品的对象,如果有拿来注入,顺利走完流程
对于上面的图
a = new A()
执行之后就会把这个半成品的 a 放入 singletonFactories 缓存,即factories.put(a)
- 接下来执行
a.setB()
,走入getBean("b")
流程,红色箭头 3 - 这回再执行到
b.setA()
时,需要一个 a 对象,有没有呢?有! factories.get()
在 singletonFactories 缓存中就可以找到,红色箭头 4 和 5- b 的流程能够顺利走完,将 b 成品放入 singletonObject 一级缓存,返回到 a 的依赖注入流程,红色箭头 6
二级缓存无法处理创建代理的场景
二级缓存无法正确处理循环依赖并且包含有代理创建的场景,分析如下
- spring 默认要求,在
a.init
完成之后才能创建代理pa = proxy(a)
- 由于 a 的代理创建时机靠后,在执行
factories.put(a)
向 singletonFactories 中放入的还是原始对象 - 接下来箭头 3、4、5 这几步 b 对象拿到和注入的都是原始对象
三级缓存解决上述所有问题
上面有代理对象情况更加复杂,所以这时候,spring引入了三级缓存,增加了一个缓存singletonObjects
(在最后spring中,这个叫二级缓存,这只是第三次引入的)
简单分析上述场景的话,只需要将代理的创建时机放在依赖注入之前即可,但 spring 仍然希望代理的创建时机在 init 之后,只有出现循环依赖时,才会将代理的创建时机提前。所以解决思路稍显复杂:
- 图中
factories.put(fa)
放入的既不是原始对象,也不是代理对象而是工厂对象 fa - 当检查出发生循环依赖时,fa 的产品就是代理 pa,没有发生循环依赖,fa 的产品是原始对象 a
- 假设出现了循环依赖,拿到了 singletonFactories 中的工厂对象,通过在依赖注入前获得了 pa,红色箭头 5
- 这回
b.setA()
注入的就是代理对象,保证了正确性,红色箭头 7 - 还需要把 pa 存入新加的 earlySingletonObjects 缓存,红色箭头 6
a.init
完成后,无需二次创建代理,从哪儿找到 pa 呢?earlySingletonObjects 已经缓存,蓝色箭头 9
当成品对象产生,放入 singletonObject 后,singletonFactories 和 earlySingletonObjects 就中的对象就没有用处,清除即可
以上是关于图解 Spring 解决循环依赖,学不会接着砍!的主要内容,如果未能解决你的问题,请参考以下文章
(已发)写得太好了!步步图解让你明白-Spring-循环依赖!