spring之我见- spring循环依赖为啥是三级缓存?

单例在spring里的获取方式

今天讲一下spring中针对单例bean的循环依赖问题,本着追本溯源的学习理念,我们要先知道单例在spring中怎么管理的。spring获取实例都通过beanFactory的getBean方法获取实例,顺着代码而下,在doGetBean方法(AbstractBeanFactory)中,单例总是通过getSingleton()方法获取实例。

protected <T> T doGetBean(
			final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
			throws BeansException {

......
						// Create bean instance.
				if (mbd.isSingleton()) {
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							destroySingleton(beanName);
							throw ex;
						}
					});
					beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}
......
}

getSingleton和createBean方法大致流程(单例):

  • 先从缓存拿(从第一层到第三层缓存中依次获取)
  • 创建对象准备工作(beforeSingletonCreation):把beanName标记为正在创建中(singletonsCurrentlyInCreation.add)
  • 对象实例化(createBeanInstance):,通过其定义里的class找到构造器方法反射创建实例
  • 提前暴露对象解决循环引用问题(Eagerly cache singletons to be able to resolve circular references):如果此beanName为正在创建中,则把其对象工厂放入第三层缓存(addSingletonFactory)
    • 有一个bean拓展点,getEarlyBeanReference 是一个延迟动作,等其它类依赖获取这个对象的时候会触发(SmartInstantiationAwareBeanPostProcessor,本质也是BeanPostProcessor)。
  • 对象初始化
    • populateBean 装配对象变量:获取此bean中有@Autowired等注解的成员变量,从所有bean定义中找出此类型的beanName,又通过BeanFactory#getBean方法获取实例,然后反射设值成员变量.
    • initializeBean:我们熟知的 applyBeanPostProcessorsBeforeInitialization (BeanPostProcessor) 和 InitializingBean 和 applyBeanPostProcessorsAfterInitialization (BeanPostProcessor)等拓展点.
  • 循环依赖检查
  • 创建对象结束工作(afterSingletonCreation):移除正在创建中的标记(singletonsCurrentlyInCreation.remove),把实例放入第一层缓存,移除第二、三层中的缓存(addSingleton),最后返回实例

不熟悉源码的同学可能看着云里雾里,其实这里得分成两块来看:标黑的是创建bean的主流程,其他的是发生循环依赖(bean相互引用)时的处理。

三级缓存

Spring给了三个map,也就是三级缓存:

  • 一级缓存 : singletonObjects 初始化完后的单例都会存在这个map中,一般ioc容器初始完后,你再用getBean取实例都可以从这里获取到。
  • 二级缓存 :earlySingletonObjects 提前曝光的实例,这时候bean还处于创建中(bean的创建分为 实例化 和 初始化),没有完全创建好。
  • 三级缓存 :singletonFactories 跟二级缓存要放在一起看,这里存的只是一个工厂类,需要通过getObject获取实例,实例获取到以后会放到二级缓存,只会执行一遍。

	/** 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 ConcurrentHashMap<>(16);

再来看看Spring对三级缓存的获取逻辑,这段代码的含义就是 先从一级缓存中获取实例,取不到,再去取二级缓存,再没有,那就三级中取。 注意: 当从三级中取到实例时,会删除三级缓存中的值,然后放入二级缓存,二/三级缓存是一体的,三级缓存的工厂类是为了延迟执行(怎么延迟执行后面说),然后二级缓存是为了存放三级缓存getObject的结果,所以每个对象的三级缓存只会执行一遍,所以其实我们就把三级缓存当成两级缓存(一级缓存和二/三级缓存),这样对整体机制的理解有帮助,减少理解的成本

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return (singletonObject != NULL_OBJECT ? singletonObject : null);
	}

我们还要知道Spring对一个bean的拓展点有哪些,这对我们理解Spring循环依赖处理有帮助。

从上文我们知道一个bean在创建过程中因为拓展点的存在,可能会产生两个对象(狸猫换太子):

  • 一个是循环依赖时需要设值依赖的对象(getEarlyBeanReference)
  • 一个是初始化后的对象(initializeBean)

如果现在要对bean做增强,比如实现切面,则需要生成代理类,所以spring在上述两个方法中通过BeanPostProcessor类提供了拓展点。

当我们铺垫完上面的基础源码流程,我们开始引出本文的核心:什么是循环依赖和Spring循环依赖解决方案。

循环依赖的问题和Spring的解决方案

上一章我们讲了单例如何从三个缓存中去取,可是为什么要设计这个缓存我们听的云里雾里,这里我们就抛出开发中存在的问题:循环依赖。

我们知道,当A 有一个 属性 B , B 有一个属性A时,就形成了循环依赖,按照单例bean的创建逻辑,A没创建完,就去拿B,B还没创建完,又去拿A,请问A在哪?A还没创建完呢 ! 所以解决问题的关键在哪?spring给设计了一个多级缓存,当A完成实例化(createBeanInstance)时,这个实例已经有了自己的内存地址,只是对象不够完善,spring先把这种对象放进一个缓存。然后装配属性的时候需要B时 ,B开始自己的创建之旅,途中B需要A了,它就可以从缓存中顺利拿到实例A,即使这时候的A并不完善。 然后B把自己创建完成后,又轮到A继续完善自己,直到 A和B 都创建完毕,这样就没有了循环依赖的问题。

下面用图反映spring bean的大致创建流程 与 多级缓存之间的配合:

  • 当实例化完一个初始bean时,会先放入三级缓存,记住,这里的A还只是一个 初生的婴儿,没有属性装配,没有经历初始化,是个ObjectFactory。
  • 然后经历装配bean属性的过程。期间属性涉及到 循环依赖的时候,就可以通过第三缓存拿到对象。拿到后删除三级缓存,放入二级缓存。
  • 实例创建完毕后会删除二,三级缓存,放入一级缓存。一级缓存就是 spring单例对象的完全体,后面程序可以通过beanFactory 随时取用。

在这里插入图片描述

在经历拓展点情况下的循环依赖检查

在了解了解决循环依赖的基本原理后,我们要知道这种情况只能解决最理想的循环依赖情况(没有经历拓展点的Bean),但是Spring预留了两处拓展点,拓展点意味着Bean的返回值有可能跟原始Bean不再一样,比如原始Bean是A,那么A跟其它对象的依赖的A必须要内存地址一致才行,这时候看看Spring怎么检查循环依赖的。

我们重点关注一下下面的代码,从doCreateBean方法中看到如下示例代码,这段代码在装配完bean(populateBean)后开始执行,会看到其中调用了getSingleton(beanName, false),这里其实是 这个bean是否存在被其它bean依赖的一次检查

这里有三个对象我们需要留意,一个是earlySingletonReference ,一个是exposedObject,一个是 bean

bean是最原始的对象,不会被任何流程修改。

earlySingletonReference,代表这个bean是否存在被其它bean依赖的情况。因为第二个参数为false,看里面的实现可以知道,false的话是不会执行工厂类的方法的,换句话说,一定是哪里依赖了这个对象,才会执行了工厂类的getObject方法,earlySingletonReference的值才不为空。

当发现这个bean是存在被其它bean依赖的情况(earlySingletonReference!=null),Spring就要开始提高警惕了,Spring会再判断exposedObject == bean,exposedObject 是可能被initializeBean这个拓展点修改的,如果相等,那么一切都好说,Spring通过把exposedObject 的引用指向了二级缓存中的对象,从而保证不管是A本身也好,还是其它对象依赖的A也好,都能保证一致。

如果不相等,那就麻烦了!这代表着其它对象依赖的A与exposedObject 是不一样的对象, 代码会开始找哪些对象会依赖B(根据beanName查找),最终发现还真不少(actualDependentBeans不为空),然后就会开始报错,项目启动失败。

		if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
								StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
								"] in its raw version as part of a circular reference, but has eventually been " +
								"wrapped. This means that said other beans do not use the final version of the " +
								"bean. This is often the result of over-eager type matching - consider using " +
								"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

所以,为什么是三层缓存

在前面说了那么多的情况下,可能大家还是没理解为什么要用三层缓存,甚至觉得Spring是过度设计。

不知道大家有没有理解getEarlyBeanReference,按照注释,它是为了解决循环依赖的,那么对于Spring IOC的主体流程中,getEarlyBeanReference被设计成需要的时候才执行,什么时候是需要的?那当然是存在相互依赖的时候!所以Spring设计了一个可以延迟执行的ObjectFactory,所以Spring的三层缓存才被设计出来

		// Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		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");
			}
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

最后谈谈AOP跟拓展点

aop具体的原理可以看我aop的文章 spring之我见 - Spring AOP实现原理(上), spring之我见 - Spring AOP实现原理(下)
. 我这里直接说一下结论,有两个地方: 一个是BeanPostProcessors 的 postProcessAfterInitialization逻辑里,还有一个就在 getEarlyBeanReference 方法中, 这两个方法同属于 AbstractAutoProxyCreator 抽象类(继承BeanPostProcessor),在创建bean的过程中会先执行 getEarlyBeanReference ,再执行postProcessAfterInitialization,两个方法都是创建代理对象的方法,但是会通过一个cache map避免重复执行,具体执行哪一个要视具体情况来定。

	@Override
	public Object getEarlyBeanReference(Object bean, String beanName) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		this.earlyBeanReferences.put(cacheKey, bean);
		return wrapIfNecessary(bean, beanName, cacheKey);
	}
.......
	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyBeanReferences.remove(cacheKey) != bean) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

总结

总结三层缓存的话

第一层缓存:存放的是成品bean,项目启动完成后获取bean实例时,就会从这里取。
第二级缓存:创建bean过程中用于处理循环依赖的临时缓存,搭配第三层缓存,保证第三层缓存的ObjectFactory只执行一次
第三层缓存:创建bean过程中用于处理循环依赖的临时缓存,只有在存在循环依赖的情况下才会触发ObjectFactory的getObject方法(延迟触发),且获取对象会作为当前bean的最终对象

修订

2024.06.12 依据 【超级干货】为什么spring一定要弄个三级缓存?重新修订了本文章,并引用重新组织了部分内容,感谢原作者!!

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值