Spring如何通过三级缓存解决循环依赖

Posted 诺浅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring如何通过三级缓存解决循环依赖相关的知识,希望对你有一定的参考价值。

什么是循环依赖

什么是循环依赖?比如如下一个例子

public class A
	private B b;

public class B
	private A a;

这个例子存在的问题:理论上spring创建A的时候依赖了B,然后spring就会去加载B,但是这个时候B又依赖了A,spring又去加载A,就会陷入一个死循环,但我们在实际使用spring的时候并没有出现这样的循环,这是因为spring设计之初就考虑了这个问题,那么spring是如何解决的呢?我们先要明确的是spring对循环依赖的处理有三种情况,分别为

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

Spring如何通过三级缓存解决循环依赖

Spring中有三级缓存,分别如下

  1. singletonObjects:完成初始化的单例对象的cache(一级缓存)
  2. earlySingletonObjects :完成实例化但是尚未初始化的,提前暴光的单例对象的Cache (二级缓存)
  3. singletonFactories : 进入实例化阶段的单例对象工厂的cache (三级缓存)

Spring获取一个Bean的流程就说从一级到三级依次去寻找这个Bean

protected Object getSingleton(String beanName, boolean allowEarlyReference) 
	// Quick check for existing instance without full singleton lock
	// 从一级缓存中获取
	Object singletonObject = this.singletonObjects.get(beanName);
	// 如果没有,且依赖的A正在初始化,那我们去尝试看看earlySingletonObjects(二级缓存)是否有对象
	// earlySingletonObjects(二级缓存)存放的是还未实例化好对象
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) 
		// 从二级缓存中获取
		singletonObject = this.earlySingletonObjects.get(beanName);
		// 如果获取不到 但是支持获取EarlyReference
		if (singletonObject == null && allowEarlyReference) 
			// 这里应该是Spring 5.x的优化,只有一二级缓存中都获取不到的时候才进行加锁,相对于5.x以前的一开始就加锁后移了,提高了性能
			synchronized (this.singletonObjects) 
				// Consistent creation of early reference within full singleton lock
				// 这里与5.x以前的一致
				singletonObject = this.singletonObjects.get(beanName);
				if (singletonObject == null) 
					singletonObject = this.earlySingletonObjects.get(beanName);
					if (singletonObject == null) 
						// 从三级缓存中获取,需要注意的是这里获取的不是singletonObject了,而是一个ObjectFactory
						// 这个ObjectFactory就是 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); 中的() -> getEarlyBeanReference(beanName, mbd, bean)
						ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
						if (singletonFactory != null) 
							// 通过从三级缓存中拿到的是一个工厂对象,这个工厂对象可以用于生产singletonObject并放入二级缓存中
							singletonObject = singletonFactory.getObject();
							// 把工厂生产出来的singletonObject放入二级缓存中
							this.earlySingletonObjects.put(beanName, singletonObject);
							this.singletonFactories.remove(beanName);


							// 也就是说,这里的逻辑是 从三级缓存中拿到的是一个工厂对象,这个工厂对象可以用于生产singletonObject并放入二级缓存中
							// 为什么不把原对象直接放入二级缓存中呢?因为可能有AOP,所以需要生成代理对象
							// 那为什么不直接把生成好的代理对象放入二级缓存中呢?因为为了性能考虑,如果没有循环引用,是不需要生成对象的。如果我们早早的就在doCreateBean方法中生成好了对象并放入二级缓存中,那是不是影响性能呢?
						
					
				
			
		
	
	return singletonObject;

如果一二三级均没有这个Bean,就会走新建方法,Spring创建一个Bean的时候也会有3个步骤,如下

  1. createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象
  2. populateBean:填充属性,这一步主要是多bean的依赖属性进行填充
  3. initializeBean:调用spring xml中的init 方法。

我们再来说上面的循环依赖的例子Spring的解决方案:

  1. A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行第二步填充属性
  2. 发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A)
  3. 尝试从一级缓存singletonObjects中获取(肯定没有,因为A还没初始化完全)
  4. 尝试从二级缓存earlySingletonObjects中获取(也没有)
  5. 尝试三级缓存,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过从三级缓存中拿到A的工厂对象,通过这个工厂对象构建出了A的Bean对象,B拿到A对象后顺利完成了初始化阶段1、2、3。B对象完全初始化之后将自己放入到一级缓存singletonObjects中
  6. 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中。

为了更好的理解,我们来看看往三级缓存中放入A工厂对象的关键代码

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
		isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) 
	if (logger.isTraceEnabled()) 
		logger.trace("Eagerly caching bean '" + beanName +
				"' to allow for resolving potential circular references");
	
	// 重点:Spring解决循环依赖的关键
	// 把当前bean对象的工厂对象加入到三级缓存中
	addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

() -> getEarlyBeanReference(beanName, mbd, bean)即为构建了一个ObjectFactory对象,后续可以通过这个工厂对象构建出Bean对象

为什么构造器循环依赖和多例循环依赖Spring无法解决

构造器循环依赖
循环依赖的解决是实例化后通过三级缓存来解决的,但构造器是在实例化时调用的,此时bean还没有实例化完成。如果此时出现了循环依赖,一二三级缓存并没有Bean实例的任何相关信息,因此当getBean的时候缓存并没有命中,这样就抛出了循环依赖的异常了。总结:spring的三级缓存解决的是实例化之后属性赋值的循环依赖,构造器被调用是在实例化之前,所以无法解决构造器的循环依赖!

多例循环依赖
对于prototype作用域bean, Spring 容器无法完成依赖注入,因为Spring 容器不进行缓存prototype作用域的bean ,因此无法提前暴露一个创建中的bean。

为什么不能只用一二级缓存来解决循环依赖?

这里面涉及到的问题很复杂,可以看这篇文章
总结起来就是几点:一级缓存是放完全初始化好的对象的,如果只需要IOC功能其实一级缓存就能解决。但涉及到循环依赖,我们就需要暴露出一个没有初始化好的对象,所以这个时候就需要引入二级缓存,把初始化好的放到一级缓存里面去,没初始化完全的放到二级缓存里面去。看起来可以完美解决问题了,但如果有代理对象的话,实际流程就会变成下面这样。我们假设上诉例子中A为代理对象

  1. A初始化的时候把能够生成A代理对象的一个A工厂对象放到三级缓存中
  2. A发现自己依赖B对象就去生成B对象,B对象发现自己需要A对象,就会去三级缓存中把这个A工厂对象取出来并且生成了A对象的代理对象,然后把代理对象放入到二级缓存,同时赋值B对象中的A,到此B对象构建完毕并放入一级缓存中。
  3. A对象使用构建好的B对象构建自己,A对象构建完毕。

以上是关于Spring如何通过三级缓存解决循环依赖的主要内容,如果未能解决你的问题,请参考以下文章

Spring如何通过三级缓存解决循环依赖

spring的三级缓存解决循环依赖

Spring三级缓存

Spring源码--02--三级缓存 解决 循环依赖

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

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