当前位置: 首页 > news >正文

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代理的生成时机:

  1. 正常AOP流程(无循环依赖):

    • Bean创建的主要步骤:实例化 -> 属性填充 -> 初始化 -> AOP代理生成 (在postProcessAfterInitialization阶段) -> 放入一级缓存 (singletonObjects)。

    • AOP代理是在Bean完全初始化之后才生成的。 这是最理想、最标准的时机。

  2. 循环依赖带来的问题:

    • 假设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”(按需提前生成代理):

  1. 三级缓存 (singletonFactories) 存的是工厂 (ObjectFactory): 这个工厂的核心是getEarlyBeanReference()方法。

  2. “按需触发”机制:

    • 无循环依赖时: 这个工厂不会被调用。Bean走正常流程,在初始化后生成代理。

    • 发生循环依赖时(如B需要A):

      • 当B去查找A,最终会找到三级缓存中A的ObjectFactory

      • 调用 ObjectFactory.getObject() -> 触发 getEarlyBeanReference()

      • getEarlyBeanReference()内部,Spring会检查这个Bean是否需要AOP代理。如果需要,它就在这里提前执行生成代理的逻辑(调用相关的SmartInstantiationAwareBeanPostProcessor)。

      • 生成好的代理对象 A$Proxy 会被返回给B进行注入。

      • 同时,这个A$Proxy会被放入二级缓存 (earlySingletonObjects),标记为“早期暴露的引用(可能是代理)”,供后续可能依赖它的其他Bean使用(避免重复生成代理)。

  3. 后续流程:

    • 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 重写“所以,为什么是三层缓存”章节。

http://www.xdnf.cn/news/1100557.html

相关文章:

  • uniApp实战五:自定义组件实现便捷选择
  • Hadoop 用户入门指南:驾驭大数据的力量
  • 如何将文件从OPPO手机传输到电脑
  • crmeb多门店对接拉卡拉支付小程序聚合收银台集成全流程详解
  • UniApp 生命周期详解:从启动到销毁的完整指南
  • WHQL认证失败怎么办?企业如何高效申请
  • 前端开发—全栈开发
  • Python中类静态方法:@classmethod/@staticmethod详解和实战示例
  • Linux:多线程---同步生产者消费者模型
  • 人事系统选型与应用全攻略:从痛点解决到效率跃升的实战指南
  • 区块链应用场景深度解读:金融领域的革新与突破
  • 资源分享-FPS, 矩阵, 骨骼, 绘制, 自瞄, U3D, UE4逆向辅助实战视频教程
  • 将Blender、Three.js与Cesium集成构建物联网3D可视化系统
  • 【SpringAI】6.向量检索(redis)
  • javaweb之相关jar包和前端包下载。
  • PHY模式,slave master怎么区分
  • 7.11文件和异常
  • 什么是进程、什么是线程(进程、线程的全方面解析)
  • 界面组件DevExpress WPF中文教程:Grid - 如何检查节点?
  • 在 React Three Fiber 中实现 3D 模型点击扩散波效果
  • JavaWeb笔记二
  • 企业级配置:Azure 邮件与 Cloudflare 域名解析的安全验证落地详解
  • CReFT-CAD 笔记 带标注工程图dxf,png数据集
  • JVM 内存结构
  • 每天一个前端小知识 Day 29 - WebGL / WebGPU 数据可视化引擎设计与实践
  • 人工智能-基础篇-29-什么是低代码平台?
  • AI问答之手机相机专业拍照模式的主要几个参数解释
  • 人工智能-基础篇-28-模型上下文协议--MCP请求示例(JSON格式,客户端代码,服务端代码等示例)
  • 大数据学习7:Azkaban调度器
  • 《Effective Python》第十三章 测试与调试——使用 Mock 测试具有复杂依赖的代码