【源码分析】Spring的循环依赖(setter注入、构造器注入、多例、AOP)

写在前面

首先最简单的循环依赖demo就是:A->B 且 B->A。本文围绕这个例子去讲解setter注入的循环依赖、构造器注入循环依赖、多例的循环依赖、带AOP的循环依赖。以下是一些结论:

  • Spring并不能解决所有循环依赖,例如构造器注入的循环依赖等。
  • Spring解决循环依赖依靠的是提前暴露早期对象,是通过三级缓存来实现的。
  • 三级缓存池在源码中分别叫:singletonObjects、earlySingletonObjects、singletonFactories。
  • Spring是所有对象都是通过getBean去获取的,即从三级缓存里面找,如果有就直接从缓存池中获取并返回,没有就执行创建。
  • Spring中对象创建大概分为以下几个步骤:
    • 选择构造器
    • 通过反射实例化对象
    • 将实例化完的空对象放入三级缓存池中(singletonFactories)
    • 为实例化完的空对象属性赋值(依赖注入)
    • 执行一些初始化方法
    • 将创建完的对象放入到一级缓存池中(singletonObjects)
  • 在为对象进行属性赋值的时候会进行setter注入,注入方法是通过getBean获得对象。
  • 在实例化阶段会进行构造器注入(如果有),注入方法是通过getBean获得对象。
  • 对象创建的顺序决定了Spring能解决setter注入的循环依赖,而不能解决构造器注入的循环依赖。
  • 当发生了Spring无法解决的循环依赖时,Spring会通过一些标记检测出来,并抛异常结束程序。
  • 三级缓存中存放的是ObjectFactory而不是Object,ObjectFactory被调用getObject会执行getEarlySingleton()去初始化对象并返回。
  • 无论有没有发生循环依赖,都会将实例化完的对象放入三级缓存池中,因为放入三级缓存池时并不知道会不会发生循环依赖。
  • 没有发生循环依赖时,三级缓存不会用于初始化对象,二级缓存也不会被使用到。
  • 只有发生了循环依赖才会通过三级缓存的BeanFactory去初始化对象并将其放入到二级缓存池中。
  • 二级缓存中可能是原对象,也可能是代理对象,取决于ObjectFactory.getObject()的结果。
  • 三级缓存池的存在是为了应对当循环依赖发生时,提前执行A的AOP操作,从而使得B能正确的引用到A的代理对象。
  • 一级缓存存放的是创建完成的对象。

循环依赖的核心问题就是在不干预他的情况下,会无限嵌套调用getBean。

  • getBean(A)
    • getBean(B)
      • getBean(A)
        • getBean(B)
          • getBean(A)
            • …………

所以,要搞清楚为什么Spring能解决某些循环依赖其实就是要搞清楚,Sping为什么能阻止某些情况下getBean的无限嵌套。

getBean的逻辑

点进源码一看,getBean其实只是一个“替身”。

	@Override
	public Object getBean(String name) throws BeansException {
    
    
		return doGetBean(name, null, null, false);
	}

	@Override
	public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    
    
		return doGetBean(name, requiredType, null, false);
	}

	@Override
	public Object getBean(String name, Object... args) throws BeansException {
    
    
		return doGetBean(name, null, args, false);
	}

getBean是Spring中很重要的方法,他有多个重载,根据参数去决定如何调用doGetBean,而doGetBean代码太长就不贴了,精简过后的流程如下:
在这里插入图片描述
此方法主要就是调用了两次getSingleton,这个getSIngleton也是一个有多个重载的方法,第一次getSingleton是查缓存的逻辑,第二次getSingleton是创建逻辑。通过先后调用这两个getSingleton就实现了我们上面说的getBean的逻辑:有则返回,无则创建。
当然,Spring的Bean不只有单例类型,还有多例类型、其他类型等。上述的流程建立在创建的对象是单例的前提之上,实际上doGetBean还有如何处理多例类型的Bean、其他类型的Bean的逻辑。这里暂时不赘述,后面再讲。

getSingleton获取缓存逻辑

下面具体来看一下第一次getSingleton的逻辑:

	@Nullable // doGetBean 第一次调用 getSingleton 的是这个
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    
    
		// Quick check for existing instance without full singleton lock
		Object singletonObject = this.singletonObjects.get(beanName); //先检查单例缓存池有没有这个对象,一级缓存,有就直接返回
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    
     // 是否正在创建,没有正在创建的话就返回
			singletonObject = this.earlySingletonObjects.get(beanName); // 查看是否有缓存,二级缓存,早期单例池,有也直接返回
			if (singletonObject == null && allowEarlyReference) {
    
    
				synchronized (this.singletonObjects) {
    
     // 锁 争夺锁的条件: 无一级,正在创建标记,无二级,允许早期引用
					// Consistent creation of early reference within full singleton lock
					singletonObject = this.singletonObjects.get(beanName); // 单例模式的双检,加锁之后再查一次一级缓存
					if (singletonObject == null) {
    
     // 经过双检,一级缓存确实没有这个bean
						singletonObject = this.earlySingletonObjects.get(beanName); // 单例模式的双检,加锁之后再查一次二级缓存
						if (singletonObject == null) {
    
    // 经过双检,二级缓存确实没有这个bean (可是这个锁锁的是一级而不是二级缓存,这样能达成双检的目的吗)
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);//三级缓存,单例工厂池
							if (singletonFactory != null) {
    
     //在三级缓存中存在
								singletonObject = singletonFactory.getObject();//获取三级缓存 (getEarlyBeanReference)
								this.earlySingletonObjects.put(beanName, singletonObject); // 放入二级缓存
								this.singletonFactories.remove(beanName); // 移除三级缓存
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

这里的singletonObjects、earlySingletonObjects、singletonFactories就是我们所说的三级缓存。除了三级缓存之外,还有两个会影响到获取缓存的逻辑的变量:allowEarlyReference和isSingletonCurrentlyInCreation(方法内部就是查看集合singletonsCurrentlyInCreation否有这个beanName)
其中allowEarlyReference是Spring可配置的一个属性,默认是true,也就是允许早期引用,只有为true的时候Spring才可以解决一些循环依赖。而singletonsCurrentlyInCreation其实就是一个标记,在创建对象前后会进行标记和移除标记,具体发生在第二个getSingleton中,这个后面再说。
这段代码嵌套很多层,看起来也让人头痛用,伪代码翻译一下就是:

if(不存在一级缓存 && 对象没有被标记为正在创建){
    
    
	if(不存在二级缓存&&允许早期引用){
    
    
		synchronized(一级缓存池){
    
    
			if(不存在一级缓存){
    
    
				if(不存在二级缓存){
    
    
					if(存在三级缓存){
    
    
						将三级缓存移动到二级缓存
					}
				}
			}
		}
	}
}
返回对象

总结一下,逻辑就是

  • 如果存在一级缓存就直接返回
  • 如果不是正在创建的直接返回(只有这个对象是正在创建的才可能可以访问二级缓存和三级缓存)
  • 如果存在二级缓存就直接返回
  • 如果不允许引用早期对象直接返回(只有允许引用早期对象才可能可以访问二级缓存和三级缓存)
  • 如果存在一级缓存就直接返回
  • 如果存在二级缓存就直接返回
  • 如果存在三级缓存,先把他放到二级缓存,再返回

注意,这里查询和检查了两次一级缓存和二级缓存,如果了解单例模式的话,那么很容易就知道这里用到了一个双检操作,其中后两次检测是为了保证对象一定是单例的。为了帮助我们理解这段代码的逻辑,我们不考虑多线程的情况,将其转化为单线程后逻辑就是:

if(不存在一级缓存 && 对象没有被标记为正在创建){
    
    
	if(不存在二级缓存 && 允许早期引用){
    
    
		if(存在三级缓存){
    
    
			将三级缓存移动到二级缓存
		}
	}
}
返回对象

如果连三级缓存都没有,那么就会直接返回null,后续会执行创建对象的逻辑。
当然,即使是已经简化到这种地步了,你也很可能是一脸懵逼,心中有很多疑惑:为什么要设计三个缓存?他们的区别是什么?判断的先后顺序为什么是这样的?为什么要将三级缓存移动到二级缓存?等等等等等
这些下面都会一一解答,目前对于这个流程有个大概印象即可。

比较细节的是这里sychronized的锁的是一级缓存池的对象监视器,这样锁的粒度会比类监视器要小,毕竟这个方法所在的类有六百多行,好多个方法用到了锁。而用一级缓存池的对象监视器作为锁的好处是符合逻辑同时也提高了并发度、减少死锁的可能。

getSingleton创建对象逻辑

第二次getSingleton执行创建对象的逻辑,主要做的就只有四件事情:

  • 标记这个对象正在创建(singletonsCurrentlyInCreation.add(beanName))
  • 调用singletonFactory.getObject()创建对象
  • 取消这的对象正在创建的标记(singletonsCurrentlyInCreation.remove(beanName))
  • 把对象从二级和三级缓存池中删除,加入到一级缓存池中

singletonFactory是一个ObjectFactory类型的对象,这个所谓的ObjectFactory是一个函数式接口:

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

看到这不免有些疑惑,这个getObject方法既没有参数,ObjectFactory也没有成员变量,他是怎么知道我要创建什么对象的呢?追溯这对象的来源,我们可以发现这个singletonFactory是getSingleton的一个入参,我们在doGetBean调用第二次getSingleton(也就是这个)的时候传入的是一个lambda表达式。

sharedInstance = getSingleton(beanName, () -> {
    
    
	try {
    
    
		return createBean(beanName, mbd, args); // 创建对象的实例
	}
	catch (BeansException ex) {
    
    
		// Explicitly remove instance from singleton cache: It might have been put there
		// eagerly by the creation process, to allow for circular reference resolution.
		// Also remove any beans that received a temporary reference to the bean.
		destroySingleton(beanName);
		throw ex;
	}
});

这里涉及到一个闭包的概念,闭包的定义是: 要执行的 代码块 和为 自由变量 提供绑定的计算环境
而在Java中,lambda表达式就是一个闭包。
lambda表达式有3个部分:

  1. 代码块
  2. 参数
  3. 自由变量的值

所谓的自由变量就是:不是参数、不在lambda表达式的代码块中定义、不在任何全局上下文中定义,而是在定义lambda表达的环境中定义的局部变量。(在本例中就是doCreateBean的局部变量,即beanName, mbd, args)

lambda表达式的必须存储自由变量的值,这个过程我们称为“自由变量被lambda表达式捕获”。
捕获有一个重要的限制:在lambda表达式中,只能引用值不会改变变量。否则并发执行会出现线程不安全的问题。另外如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的。所以 lambda 表达式只能引用显式或隐式声明为final 的变量。

扯远了。

这里想表达的是:虽然说ObjectFactory没有任何成员变量,getObject也没有任何入参,但是这里我们传入的是lambda表达式,由于其闭包的特性,Spring能够正确的创建我们需要的bean。

createBean和doCreateBean创建对象

getObject是通过createBean创建对象的,createBean主要流程为:

  • 准备Bean定义信息(包含Scope之类的信息)
  • 调用doCreateBean创建对象

(这一层层嵌套什么时候是个尽头啊)

而doCreateBean的主要流程为:

  • 选择构造器
  • 通过反射实例化对象
  • 将实例化完的空对象放入三级缓存池中(singletonFactories)
  • 为实例化完的空对象属性赋值(依赖注入)
  • 执行一些初始化方法
  • 将创建完的对象放入到一级缓存池中(singletonObjects)

终于,经过一层层的调用,我们找到了真正定义了如何创建bean的方法!
这一段流程是循环依赖问题最重要的一段逻辑,这段流程揭示了为什么Spring能解决setter循环依赖,为何不能解决构造器循环依赖。

Spring是如何解决setter注入的循环依赖的?

解决setter循环依赖的本质是在大家都是单例的前提下允许为bean对象赋予没有构建完的依赖,这些依赖存在于二级和三级缓存之中。
创建A的时候先用无参构造器实例化出来,并把此时未赋值完成的A放入三级缓存中,之后再进行属性赋值(setter注入B),因为此时B没有创建,就会执行创建流程,在B属性赋值阶段也会setter注入A,通过getBean方法获取A的时候发现A在三级缓存之中,就不执行A的创建流程了,而是直接返回三级缓存。B就此赋值完成,返回初始化完成的B给A,就此A也初始化完成了。由于A和B都是单例,所以B持有的是引用,之前未赋值的A也变成了初始化完成的A。
简化后的流程如下:
在这里插入图片描述
可以看到,因为第一次getBean(A)的时候,在属性赋值之前就将自己放入到了三级缓存之中,所以在第二次getBean(A)的时候,getSingletonObject能拿到缓存(当然,还因为之前标记了正在创建),结束了无限嵌套。也就是解决了循环依赖。

Spring为何无法解决构造器注入的循环依赖?

如果是构造器注入,在实例化阶段就会进行注入,注入的方法同样也会通过调用getBean去获得依赖。但是由于调用getBean之前没有把自己放入到缓存池当中,因此在B调用getBean获取A的时候获取不到三级缓存,从而会继续走创建流程,如果不干预,则会造成无限循环。
在这里插入图片描述
注意!!!实现Spring不会无限嵌套,敏锐的读者可以发现,虽然第一次和第二次的getBean(A)都会走创建流程,但是他们在getSingleton#1中的判断是不一样的。也就是说第一次和第二次的getBean(A)并不是完全一模一样,他们的一些状态不同了!Spring通过这些状态识别出哪些循环依赖时无法解决,从而抛异常。也就是图中右下角画的无限循环是一种省略画法,实际并不会发生。

Spring是如何发现构造器注入的循环依赖?

按照我们的想法,循环依赖导致的结果应该是无限循环,但是很显然,当我们写出了一段循环依赖的代码出来时,Spring会抛异常来结束程序

Unsatisfied dependency expressed through constructor parameter 0

那么Spring是在哪里抛出的这个异常的呢?他是如何检测判断出来的呢?
当发生构造器注入的循环依赖时,A会进入第二次创建流程,也就是会调用第二次getSingleton,上文也提到getSingleton创建对象的时候会先进行标记此对象正在被创建(singletonsCurrentlyInCreation),我们详细看看它是怎么实现的:

	protected void beforeSingletonCreation(String beanName) {
    
     // 查看是否存在,不存在则添加。如果存在或者添加失败则抛异常(构造器注入循环依赖在此发现)
		if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
    
    
			throw new BeanCurrentlyInCreationException(beanName);
		}
	}

可以看到,如果发现这个bean已经被标记为正在创建了就会抛异常。第二次getBean(A)执行到第二个getSingleton时必然会检测到第一次创建流程时的标记,所以就会抛异常,结束程序。

Spring是如何发现多实例Bean的循环依赖的?

上面讲的两种循环依赖都是在单例的前提下讲的。
多例和单例其实差不多,对于setter注入,循环依赖能够被解决,对于构造器注入,不能被解决。但是由于多实例并不会调用getSingleton方法,自然也没有标记。也就是说,无论多少次创建多实例bean都不会调用beforeSingletonCreation,但是Spring确实也能检测的到,并报错:

Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

所以多实例Bean是在那里抛异常的呢?
无论时单例还是多例,调用的都是同一个createBean,单实例在getSingleton,也就是createBean的外层检测的,那么多例自然也是在createBean外层,也就是doGetBean中检测的
在doGetBean在调用getSingleton或者createBean之前有这么一段代码:

if (isPrototypeCurrentlyInCreation(beanName)) {
    
    //如果是多实例bean,并且已经创建过了,就会判断为多实例循环依赖(构造器注入),抛异常
	throw new BeanCurrentlyInCreationException(beanName);
}
-------------------
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
    
    
	Object curVal = this.prototypesCurrentlyInCreation.get();
	return (curVal != null &&
			(curVal.equals(beanName) || (curVal instanceof Set<?> set && set.contains(beanName))));
}

望文生义,这段代码就是为了解决多实例bean的循环依赖的。
那么这个标记有时什么时候进行的呢?当然还是在doGetBean当中了

else if (mbd.isPrototype()) {
    
     //多实例创建
	// It's a prototype -> create a new instance.
	Object prototypeInstance = null;
	try {
    
    
		beforePrototypeCreation(beanName);
		prototypeInstance = createBean(beanName, mbd, args);
	}
	finally {
    
    
		afterPrototypeCreation(beanName);
	}
	beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

这部分的逻辑和getSingleton的逻辑很相似,也就是无论是单例还是多例,都会进行标记,区别在于标记的地方不同而已。
在这里插入图片描述

三级缓存

还记得上文写到的getSingleton#1的创建逻辑吗?按照创建流程,我们先把对象放到三级缓存,然后发生循环依赖了就把对象从三级缓存放到了二级缓存,初始化完毕后再把二级缓存放到一级缓存。似乎有点难理解为什么这对象要这么挪来挪去,或者说如果不这么做会有什么后果?比如我只用一级缓存,对象实例化后就直接放进去,发生循环依赖的时候也能从一级缓存中找到,同样解决了循环依赖,并且还少了很多步骤。这些问题将在下文解答,这时候我们可以先看一下三级缓存在源码中是如何定义的:

	/** Cache of singleton objects: bean name to bean instance.一级缓存 */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of early singleton objects: bean name to bean instance. 二级缓存 */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

	/** Cache of singleton factories: bean name to ObjectFactory. 三级缓存 */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

我们会惊讶的发现,而一二级缓存不一样,第三级缓存里面存的不是Object,而是ObjectFactory。记忆力好的读者可能记得我们在上文提到在调用getSingleton#2的时候我们传入的参数也是一个ObjectFactory。事实上,此时记忆力差点也不是什么坏事,因为上文提到的ObjectFactory和此处三级缓存存的ObjectFactory在语义上几乎没有什么联系,除了他们是同一个类、调用getObject都能创建一个对象之外。也就是说他们是两种工厂,生产两种不同对象。因为ObjectFactory只是个函数式接口,只能说明实现它的lambda表达式具有某种能力。正如实现两个类都实现了Comparable,但他们之间可能没有任何语义上的联系,只能说明他们都能和自己比较。下面是官方对于这个接口的描述:

Defines a factory which can return an Object instance (possibly shared or independent) when invoked.
This interface is typically used to encapsulate a generic factory which returns a new instance (prototype) of some target object on each invocation.
This interface is similar to FactoryBean, but implementations of the latter are normally meant to be defined as SPI instances in a BeanFactory, while implementations of this class are normally meant to be fed as an API to other beans (through injection). As such, the getObject() method has different exception handling behavior.

简单来讲就是:实现这个接口的类意味着能对外提供一个创建对象的API,这个API创建的对象是多例的,也就是这个工厂调用多少次就会产生多少个对象。上文提到,在发生循环依赖的时候会调用这个产生多例API,这样会不会导致Spring容器的Bean不是单例的呢?当然不会,一是因为如果循环依赖能解决,那么多这个接口只会被调用一次。不能解决的话在第二次调用这个API之前就已经抛异常结束程序了。二是因为调用这个API的getSingleton本身就是用双检保证了这个API只会被调用一次。最后是因为每一级缓存池的数据结构都是哈希表,其Key唯一的特性保证了一个beanName只会有一个实例。
(又扯远了哈哈,但我认为这些是必须明确)

三级缓存池添加了什么?

上文提到我们给getSingleton#2传入的ObjectFactory和三级缓存的ObjectFactory是两种东西,我们知道,前者主要是调用了一个createBean方法,那么后者是什么呢?
看看源码中添加三级缓存的语句:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

简洁明了,调用了getEarlyBeanReference,自由变量beanName, mbd, bean也是被lambda表达式捕获了。如果说上文给getSingleton#2传入的lambda体现不出闭包的优势,那么这次这个lambda表达式就能将其表现的比较明显了:当要执行代码块的时候,自由变量很可能已经不存在于上下文之中或者已经被改变了,为了能够正确的执行我们所希望的计算,有一个能捕获这些自由变量的计算环境尤为重要,这个计算环境和代码块的组合即是闭包,在Java中就是lambda表达式。
那么这个方法会做什么呢?其代码如下:

	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    
    
		Object exposedObject = bean; // 为了让我们自己实现的后置处理器能增强bean,不直接放bean而是放工厂(lambda表达式)
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    
    
			// 如果有后置增强器就执行 (AutowiredAnnotationBeanPostProcessor 在此提供接口进行增强)
			for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
    
    
				exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
			}
		}
		return exposedObject; // 否则返回原来的bean
	}

这里调用了一系列的后置处理器,除了一些Spring自带的后置处理器之外,如果我们开启了AOP的话,此处就会多一个AnnotationAwareAspectJAutoProxyCreator,为对象生成代理。当调用三级缓存的getObject获得对象的时就会执行AOP生成代理对象。
那么我们什么时候会去调用呢?记忆力好的读者大概能想起来上文提到过,记忆力不好读者或许可以试一下全局搜索,真的很好用。
在这里插入图片描述
其实Spring使用到这个接口的地方不多,主要有两处,你看这两处除了我写的注释不一样之外,几乎是一模一样的,甚至都在同一个类,但是上文也提到过,这是两种不同的工厂。

singletonObject = singletonFactory.getObject();//获取三级缓存 (getEarlyBeanReference)
this.earlySingletonObjects.put(beanName, singletonObject); // 放入二级缓存
this.singletonFactories.remove(beanName); // 移除三级缓存

虽然是找到了调用的地方了,但是还是没有回答“什么时候调用”这个问题。你可能会说:就是在getSingleton#1中“将三级缓存放入到二级缓存之中”的那时候调用了getObject啊。
是这样的没错,但是我觉得更好的回答是:当发生了循环依赖时,第二次getBean(A)的时候会调用。
当且仅当这时才会这样操作,这大概率意味着,这部分代码就是为发生这种情况的时候准备的。

三级缓存的十万个为什么?

当发生了循环依赖时,第二次getBean(A)的时候会将A的三级缓存放入到二级缓存池之中,并且返回这个二级缓存。为什么要返回二级缓存? 因为发生循环依赖了,B需要获得A的引用。为什么要有将三级缓存放到二级缓存之中这一操作呢? 因为B需要的是A的引用而不是工厂的引用。那么当初直接存一个引用到二级(甚至是一级)缓存不行吗? 不行,三级缓存除了能返回一个A的引用之外,还对A进行了一系列后置处理。B只是要一个A的引用,至于A有没有执行后置处理,有没有初始化完成,B不关心啊,无论A怎么变化,怎么处理,B只要能找到A就可以了,这些后置处理之后再处理不可以吗? 而且本来就是之后才处理的,为什么非要提前呢?
问题就出在了这里,B的需求是正确的引用到A,而一般来说后置处理先执行后执行好像没有什么问题,因为大部分后置处理都不会改变对象的引用。但是前面也提到了,如果开启了AOP,那么这里会执行AOP代理,代理是一个引用,原本的A又是另一个引用,你说此时B应该持有代理的引用呢?还是A的引用呢? 当然是代理的引用啊。在发生循环依赖的时候,给B注入A的时候,A才执行到赋值阶段,A的初始化阶段才会执行系列的后置处理,这些后置处理有可能会产生代理对象,如果要给B注入这个代理对象,那么只能提前去执行这些后置处理,然后给B返回初始化后的A的引用,否则Spring无法保证B在此时拿到的是就是正确的引用。
如果我们在这里直接使用二级(甚至是一级)缓存的话,那么意味着所有的Bean在这一步都要完成AOP代理。这违背了Spring在结合AOP跟Bean的生命周期的设计:让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

(100000=7)

为什么需要三级缓存呢?

现在来回答这个问题就变得简单了。三种缓存分别代表着一个Bean对象的不同状态:

  • 在一级缓存,说明这个单例对象已经完全创建完成,可以直接使用了。
  • 在二级缓存,这时对象处于早期暴露状态,这能确保最终它的引用是这个,但是不能保证它属性完全填充完毕、初始化完成。
  • 在三级缓存,这时对象属于实例化后的阶段,但是还没有被早期暴露,之后也可能不会被暴露,此时不能保证它属性完全填充完毕、初始化完成。

需要注意的一点是任何对象实例化后都会被添加到三级缓存中,即使没有循环依赖,因为到目前为止Spring也不能确定这个Bean和其他Bean是否是循环依赖的关系。但只有真正发生循环依赖的时候,才会用三级缓存的ObjectFactory提前生成对象以确保其他对象能够正确的引用到自己,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象,并且在创建完毕之后移除掉三级缓存。
如果有三级缓存执行的流程会是这样的:
在这里插入图片描述
如果没有三级缓存,那么执行流程会是这样的:
在这里插入图片描述
而提前去执行后置处理器进行初始化是妥协的做法,对于大多数bean来说没有必要,因此使用了三级缓存。同时如果只用一级缓存还有一个坏处就是在多线程环境下还会带来线程安全问题,一个还没有创建完的Bean被放到缓存里,其他线程可能拿到的就是一个不完全的Bean从而导致程序出现我们无法预期的结果。

当然,spring对于普通bean和工厂bean会通过前缀来区分,从而只使用一个容器就存储两种bean。按照这个思路,我们在用前缀来区分三种缓存其实也可以,这样就只用一个缓存就足够了!但是三级缓存实际上是逻辑上的概念,而不是容器上的概念。事实上Spring可能需要频繁的访问缓存,将三种缓存合并在一个容器也可能会影响系统的性能。

循环赖下A的初始化会是怎么样的?

首先要注意的一点是虽然在getEarlyBeanReference方法里执行了一系列后置处理器,但这不等同于初始化完成。后置处理器的种类非常多,这里只执行了smartInstantiationAware类型的后置处理器。

static class BeanPostProcessorCache {
    
    
	final List<InstantiationAwareBeanPostProcessor> instantiationAware = new ArrayList<>();
	final List<SmartInstantiationAwareBeanPostProcessor> smartInstantiationAware = new ArrayList<>();
	final List<DestructionAwareBeanPostProcessor> destructionAware = new ArrayList<>();
	final List<MergedBeanDefinitionPostProcessor> mergedDefinition = new ArrayList<>();
}

后置处理器的类型也不止上面所列的几种。因此A即使体现执行了一些后置处理器,但是这并不代表初始化完成了,其初始化阶段还是不可以忽略的。

A在getEarlyBeanReference已经创建过一次代理对象了,B创建完毕之后A也会进行初始化,此时是不是会再创建一次代理对象呢?当然不可能会再创建一次。
AOP创建代理对象会通过wrapIfNecessary方法,而在初始化时,AOP会执行一下逻辑:

Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    
     // 如果之前已经创建了一次代理了,那么此时一定不相等,则不再进行代理,直接返回bean
	return wrapIfNecessary(bean, beanName, cacheKey); // 通过此方法创建代理对象
}

如果之前创建过代理了,那么就会有一个标记,有标记就不再执行wrapIfNecessary方法了。直接返回原来那个Bean。
但是问题又来了A去执行初始化后还是一个未代理的对象,但最后返回给上层函数的对象会被存到一级缓存

// .....
exposedObject = initializeBean(beanName, exposedObject, mbd);
// .....
return exposedObject;

那存到一级缓存里的岂不是一个未被代理的对象?但是存的应该是一个代理对象才对啊。初始化完Bean之后,返回exposedObject之前(也就是上面代码的第二个省略好之间),有这么一段逻辑判断

if (earlySingletonExposure) {
    
    
	// 尝试去拿缓存,但是禁用早期引用,也就说,三级缓存是拿不到的,但此时上面创建的实例也还没有放进一级缓存
	// 循环依赖时A被B从三级放到二级,这时可以拿到,也就是说这里拿到的都是那些被提前引用的Bean。
	Object earlySingletonReference = getSingleton(beanName, false);
	if (earlySingletonReference != null) {
    
    
		// bean 是实例化完时的引用
		// earlySingletonReference 是 二级缓存中的对象,如果被AOP代理了,这个就是代理对象,因为提前执行了AOP,B此时持有的也是这个对象
		// exposedObject 是初始化方法后的对象引用,但是他和bean一般还都是一样的,否则就是初始化的时候改变了引用,此时就会进入下面else if的逻辑当中
		if (exposedObject == bean) {
    
     // 看是否初始化之后引用发生了变化
			exposedObject = earlySingletonReference;
			// 这里的逻辑就是要返回一个对象,如果在getEarlyBeanReference前后bean对象没有被替换(代理),那么就这两个是同一个引用,直接暴露即可,否则返回的是代理对象
		}// 否则就要判断一下是否允许直接注入一个不由容器实例的对象
		else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
    
    
			// .......
		}
	}
}

可以看到,我们会获取之前执行了代理的对象然后通过exposedObject = earlySingletonReference;去更新返回值,使得一级缓存里面放到是正确的引用。可能你会想问,那A执行的初始化方法岂不是都失效了,毕竟整个A都被替换成之前的那个代理对象了。别忘了,代理对象内部持有了A的引用,因此A的初始化时有效的。

总结

至此终于写完了大部分关于Spring循环依赖的问题了,我这么一写我对与Spring这部分的原理就变得很清晰了,但是不敢保证读者看完也能有一个很清晰的理解,归根结底这东西还是需要亲自去阅读代码去体会的,文章只能提供一些思路和见解。而且由于载体受限,实际上片段代码不住于对程序理解,一定会觉得有些晕头转向的,对此只能多去跟踪几遍才会变得清晰一些。对于跟踪代码,虽然IDEA有调用栈,但是不断的弹栈压栈很快的也会迷失在庞大的代码之中,所以除了去反复跟踪,我觉得还可以使用思维导图去做一个类似火焰图的东西,这相当于描绘了一个地图,有助于理清程序执行过程。对于循环依赖部分的代码,我做了一个这样的地图,大家可以参考一下:
在这里插入图片描述
以及AOP的
在这里插入图片描述
在这里插入图片描述
以上导图省略了很多逻辑判断和步骤,仅供参考。

猜你喜欢

转载自blog.csdn.net/weixin_45654405/article/details/127579128