图解 Spring 解决循环依赖,学不会接着砍!

Posted 流楚丶格念

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解 Spring 解决循环依赖,学不会接着砍!相关的知识,希望对你有一定的参考价值。

文章目录

SpringBean循环依赖

循环依赖的三种情况

首先,需要明确的是spring对循环依赖的处理有三种情况:

  1. 构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
  2. 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
  3. 非单例循环依赖:无法处理。

解决单例模式下的setter循环依赖

接下来,我们具体看看spring是如何处理第二种循环依赖的。

Spring单例对象的初始化大略分为三步:

  1. createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象;
  2. populateBean:填充属性,这一步主要是多bean的依赖属性进行填充;
  3. 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-循环依赖!

Spring 为何需要三级缓存解决循环依赖,而不是二级缓存

人人都能看懂的Spring源码解析,Spring如何解决循环依赖

Spring中-IOC-Bean的初始化-循环依赖的解决

Spring循环依赖冤冤相报何时了

图解23种设计模式,不信你学不会!(建议收藏)