Spring 三级缓存解决bean循环依赖,为何用三级缓存而非二级

Spring bean循环依赖即循环引用。是指2个或以上bean 互相持有对方,最终形成闭环。比如A依赖于B,B依赖A。产生循环依赖的方式有两种,一种是通过构造器注入形成的循环依赖,第二种是通过field属性注入形成的循环依赖。Spring通过特殊的bean生成机制解决了第二种方式产生的循环依赖问题,使得循环链的所有bean对象都能正确创建,而构造器注入方式阐释的循环依赖则会抛出异常。两者之间的差异能在bean创建机制中得到解释。

注意此处讨论的bean皆为单例,prototype的bean循环依赖皆会抛出异常,若是单例与prototype混合的情况,若是先创建单例则能成功,反之则会抛出异常。

总的来说,Spring解决循环依赖问题是通过结合bean实例化和bean属性填装分离,singletonObjects、earlySingletonObjects 、singletonFactories 三级缓存机制和引用提前暴露机制实现的。下面将详细介绍单例bean创建与获取过程。

Spring bean三级缓存解决循环依赖

Spring能解决bean循环依赖的核心机制即三级缓存,其源码如下:

/** 一级缓存,保存singletonBean实例: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** 二级缓存,保存早期未完全创建的Singleton实例: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 

/** 三级缓存,保存singletonBean生产工厂: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

详细源码调用链不再赘述,总体逻辑上,当Spring容器试图获得单例bean时,首先会在三级缓存中查找,具体方法如下:

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);
}

查找位置从一级缓存至三级缓存,注意若三级缓存查找成功,其返回的bean对象并不一定是完全体,而可能是仅完成实例化,还未完成属性装填的提前暴露引用。当三级缓存内都未找到目标,getSingleton方法则会返回null,之后Spring将会执行一系列逻辑,最终将调用以下方法新创建bean对象:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
			throws BeanCreationException { 
			
        //此处略过 做某些事
       
		// Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		// 早期缓存单例对象以解决循环引用问题
		// 即使问题是在如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");
			}
			// 将实例化完成但还未填装属性的bean引用暴露出来,方法为将beanName和对应singletonFactory加入第三级缓存Map
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// Initialize the bean instance.
		Object exposedObject = bean;
		
		// 此处省略部分代码
		
		//填装属性,在此方法内尝试获得循环引用的被引用bean,方法与自身bean获得流程一致
		populateBean(beanName, mbd, instanceWrapper);
		exposedObject = initializeBean(beanName, exposedObject, mbd);
		
		// 此处省略部分代码

以上则是解决循环引用问题的关键:在初次创建Bean对象的过程中,当bean实例化完成后并不直接开始属性装配,而是先检查是否允许提前暴露,若允许则通过将对应SingletonFactory加入第三级Bean缓存使得外界能够提前获得该Bean的未完成引用,之后再进行属性装配工作。通过上述执行流程,循环引用问题得以解决:
1、设A、B单例对象相互依赖,当我们试图获得A对象时,将首先根据BeanName在三级缓存中查找,显然目前是查询不到的。
2、之后将执行doCreateBean方法创建新的A对象,在实例化但未完成属性装配时,方法会通过将对应SingletonFactory将A-Bean的引用提前暴露出去,然后执行属性装配。
3、在属性装配方法中,发现A依赖于B,随机尝试获得B-bean,调用getSingleton方法查询三级缓存,结果依然是未找到B-bean,于是和A流程一样调用doCreateBean方法创建一个新的B-bean。
4、此处与doCreateBean方法创建A-bean流程一致,成功实例化B-bean但未填装属性,将早期引用暴露,执行属性填装方法,发现B依赖于A,于是试图获得A-bean。
5、差异在此处出现,由于第三步已经将A-bean的早期引用通过第三级缓存暴露出来,在此处执行getSingleton方法时将会成功在三级缓存内获得A-bean引用,由此递归地返回,先后完成B-bean的属性装填工作,B-bean的创建工作,A-Bean的属性装填工作,最后完成A-Bean的创建工作。

由此通过属性注入的循环依赖问题得以解决,同时上述流程也解释了为何通过构造器注入产生的循环依赖问题会抛出异常,因为在A-bean执行构造方法的时候即需要获得B-bean方法。

另外bean对象在某些场景下会在三级缓存间迁徙,非本文论述重点,不再讨论。

为何使用三级缓存而非二级

学习解决循环依赖机制过程中便有一个疑问:将缓存分为三级是否有必要,依据处理流程,二级缓存似乎也能很好解决循环依赖问题。查证之后发现许多人也有和我一样的疑问,但很难找到一个权威的解答,最终找到一篇博客给出了令我信服的说法:

使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。使用三级而非二级缓存并非出于IOC的考虑,而是出于AOP的考虑,即若使用二级缓存,在AOP情形下,注入到其他bean的,不是最终的代理对象,而是原始对象。

相关链接如下:
https://my.oschina.net/u/4340310/blog/4332450

    原文作者:笑矣乎
    原文地址: https://blog.csdn.net/panda9527z/article/details/107359916
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞