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,也就是三级缓存:
-
一级缓存 : 存放已经完全创建好,并且已经执行完所有后处理器(包括 AOP 代理生成)的单例 Bean 实例。只有当 Bean 从头到尾完全初始化并且应用了所有增强(通知)之后,才会被放到这里。
-
二级缓存 :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 随时取用。
AOP和循环依赖
AOP属于代理模式,对于存在循环依赖情况的对象,需要提前暴露的对象,代理的动作也需要提前跟进,所以AOP跟getEarlyBeanReference 也有很大的关系,具体的原理可以看我AOP的文章 Spring之我见-从IOC谈到AOP实现原理 . 我这里直接说一下结论,有两个地方:
-
一个是BeanPostProcessors 的 postProcessAfterInitialization逻辑里
-
还有一个就在 getEarlyBeanReference 方法中
这两个方法同属于 AbstractAutoProxyCreator 抽象类(继承BeanPostProcessor),调用主逻辑完全一样(都是调用wrapIfNecessary),在创建 bean 的过程中会先经过 getEarlyBeanReference 代码,再经过 postProcessAfterInitialization 方法,两个方法都是创建代理对象的方法,但是会通过一个cache map避免重复执行,具体执行哪一个要视具体情况来定。
@Overridepublic Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);this.earlyBeanReferences.put(cacheKey, bean);return wrapIfNecessary(bean, beanName, cacheKey);}
.......@Overridepublic 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;}
所以,为什么是三层缓存
在前面说了那么多的情况下,可能大家还是没理解为什么要用三层缓存,甚至觉得 Spring是过度设计。
不知道大家有没有理解 getEarlyBeanReference,按照注释,它是为了循环依赖导致对象提前暴露设计的,那么对于Spring IOC 的主体流程中,getEarlyBeanReference 被设计成需要的时候才执行,什么时候是需要的?那当然是存在相互依赖的时候!并且当Bean存在循环依赖且其中一个Bean需要AOP代理时,如何确保注入的是最终的代理对象,并且整个容器中只有一个实例?
关键矛盾在于AOP代理的生成时机:
-
正常AOP流程(无循环依赖):
-
Bean创建的主要步骤:实例化 -> 属性填充 -> 初始化 -> AOP代理生成 (在
postProcessAfterInitialization
阶段) -> 放入一级缓存 (singletonObjects
)。 -
AOP代理是在Bean完全初始化之后才生成的。 这是最理想、最标准的时机。
-
-
循环依赖带来的问题:
-
假设A依赖B,B也依赖A。
-
Spring创建A:实例化A(得到一个原始对象)-> 把A的
ObjectFactory
放入三级缓存 -> 准备给A填充属性B。 -
发现需要B,于是去创建B。
-
创建B:实例化B -> 把B的
ObjectFactory
放入三级缓存 -> 准备给B填充属性A。 -
此时B需要注入A!但A还没初始化完(更没生成AOP代理),它还在创建过程中。
-
此时,Spring面临两难选择:
-
选择1(只给B注入原始A,明显会出现问题):
-
从三级缓存拿到A的
ObjectFactory
,调用getEarlyBeanReference()
,如果此时不生成代理,则返回原始A对象注入给B。 -
B创建完成,注入给A。A完成初始化。
-
问题来了: 在A的初始化后阶段,如果需要AOP,会生成A的代理对象
A$Proxy
,并放入一级缓存。结果就是:-
一级缓存里是代理对象
A$Proxy
。 -
但B里面持有的是原始A对象。
-
这不是同一个对象! B调用的A方法没有代理逻辑(如事务、日志),破坏了AOP的预期,也破坏了单例(容器里有两个“A”:原始A和代理A)。
-
-
-
选择2(提前生成AOP代理,最佳选择):
-
在B需要注入A的那一刻,就生成A的代理对象。这样B拿到的直接就是
A$Proxy
。 -
后续A初始化完成时,发现代理已经生成,直接复用这个代理对象放入一级缓存。
-
结果:B持有
A$Proxy
,一级缓存也是A$Proxy
,完美一致。
-
三级缓存如何实现“选择2”(按需提前生成代理):
-
三级缓存 (
singletonFactories
) 存的是工厂 (ObjectFactory
): 这个工厂的核心是getEarlyBeanReference()
方法。 -
“按需触发”机制:
-
无循环依赖时: 这个工厂不会被调用。Bean走正常流程,在初始化后生成代理。
-
发生循环依赖时(如B需要A):
-
当B去查找A,最终会找到三级缓存中A的
ObjectFactory
。 -
调用
ObjectFactory.getObject()
-> 触发getEarlyBeanReference()
! -
在
getEarlyBeanReference()
内部,Spring会检查这个Bean是否需要AOP代理。如果需要,它就在这里提前执行生成代理的逻辑(调用相关的SmartInstantiationAwareBeanPostProcessor
)。 -
生成好的代理对象
A$Proxy
会被返回给B进行注入。 -
同时,这个
A$Proxy
会被放入二级缓存 (earlySingletonObjects
),标记为“早期暴露的引用(可能是代理)”,供后续可能依赖它的其他Bean使用(避免重复生成代理)。
-
-
-
后续流程:
-
A继续完成属性填充(此时注入的是已经创建好的B)和初始化。
-
在A的初始化后阶段 (
postProcessAfterInitialization
),Spring会检查:如果A的代理已经在循环依赖时提前生成(通过二级缓存可以知道),则直接复用这个已有的代理对象,不会再生成一个新的代理。 -
最终,这个代理对象
A$Proxy
放入一级缓存 (singletonObjects
),成为正式的单例Bean。二级缓存中对应的条目会被移除。
-
为什么是三级?二级缓存不行吗?
-
如果只有二级缓存(直接存半成品对象):
-
在B需要A时,只能把原始A(半成品)放入二级缓存并给B注入。
-
后续A初始化完成后生成代理
A$Proxy
放入一级缓存。 -
导致B持有原始A,一级缓存是
A$Proxy
,对象不一致。
-
-
三级缓存的精髓在于
ObjectFactory
的延迟执行能力:-
它不直接存对象,而是存一个能生成对象的“配方”(工厂)。
-
这个工厂被调用的时刻(即循环依赖发生的时刻),就是判断是否需要提前生成代理的关键时机。
-
它确保了:只有在循环依赖迫使一个Bean需要被提前引用时,才按需提前生成它的代理对象。 对于没有循环依赖的Bean,代理仍然在标准时机(初始化后)生成,保持了流程的一致性。
-
// 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));}
循环依赖解决方案并非万能!
在了解了解决循环依赖的基本原理后,我们要知道这种情况只能解决最理想的循环依赖情况(没有经历拓展点的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.");}}}}
总结
总结三层缓存的话
-
第一层缓存:存放的是成品bean,项目启动完成后获取bean实例时,就会从这里取。
-
第二级缓存:创建bean过程中用于处理循环依赖的临时缓存,搭配第三层缓存,保证第三层缓存的ObjectFactory只执行一次
-
第三层缓存:创建bean过程中用于处理循环依赖的临时缓存,只有在存在循环依赖的情况下才会触发ObjectFactory的getObject方法(延迟触发),且获取对象会作为当前bean的最终对象
修订
2024.06.12 依据 【超级干货】为什么spring一定要弄个三级缓存?重新修订了本文章,并引用重新组织了部分内容,感谢原作者!!
2025.04.19 重新编排文章,更加通俗易懂!
2025.07.11 重写“所以,为什么是三层缓存”章节。