Spring三级缓存原理与循环依赖源码深度剖析

Spring三级缓存原理与循环依赖源码深度剖析 聊到Spring源码三级缓存基本是绕不过去的一道坎。网上讲这个的文章很多但大部分停留在“背三个Map名字”的阶段singletonObjects、earlySingletonObjects、singletonFactories背得滚瓜烂熟面试一问“为什么要三级二级不够吗”当场卡壳。我最初读Spring源码时也在这个问题上卡了很久。看了很多遍DefaultSingletonBeanRegistry的代码又自己动手写了个简化版容器才真正想明白这一层设计的精妙之处。这篇文章不打算重复那些“八股解读”而是从源码触发点入手把三级缓存的定义、为什么要这样设计、代理对象如何参与、实际项目中会踩的坑一次讲透。无论你是准备面试还是读源码卡在循环依赖这里这篇应该能给你省下不少时间。1. 三级缓存到底是什么1.1 三个Map的定义与定位三级缓存在源码里其实就是DefaultSingletonBeanRegistry这个类中的三个成员变量位置在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry。代码非常简单就是三个Map。/** 一级缓存存放完整的单例对象 */ private final MapString, Object singletonObjects new ConcurrentHashMap(256); /** 二级缓存存放提前暴露的早期单例对象 */ private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); /** 三级缓存存放单例对象的ObjectFactory */ private final MapString, ObjectFactory? singletonFactories new HashMap(16);这三个Map虽然都叫“缓存”但职责差异非常大**一级缓存singletonObjects**是最终存放成品的地方。Spring容器创建完一个bean完成属性填充、初始化、代理增强等所有步骤之后才会把对象放进这里。它专门服务getBean的常规调用保证容器里同一个名字只有一个单例对象也就是我们常说的“单例池”。**二级缓存earlySingletonObjects**放的是“半成品”对象准确说是已经new出来、但可能还没有填充属性或完成初始化的早期引用。为什么要单独放一层而不是直接放一级缓存因为Spring不允许把不完整的对象直接暴露给外部正常调用但循环依赖场景下又需要把“未完成”的对象提前给别人用所以设计了一个专门的暂存区。**三级缓存singletonFactories**和前两个不一样它不存对象存的是ObjectFactory也就是一个可以产出对象的工厂。这个工厂是延迟执行的只有发生循环依赖、真的需要提前拿到引用时才会调用factory.getObject()生成对象然后放入二级缓存。对比一下就很清楚了一级是成品仓库二级是临时半成品暂存区三级是预约生产单——下单了才现场造。1.2 为什么设计成三层而不是一层很多第一次接触的人会问既然二级缓存就够了为什么要多搞一个三级还存ObjectFactory而不是直接存对象这个问题问到点子上了也是理解整个机制的关键。用一个生活化的例子来说。假设你在餐厅点了一份菜后厨做菜需要用到隔壁店的半成品调料。正常流程是备好菜new出来→ 加调料填充属性→ 出锅上桌放入一级缓存。如果B菜品需要用到A菜品还没出锅时的半成品厨房会把A菜的半成品先拿给B用这就是二级缓存做的事。但这里有个问题有些菜出锅前需要做“最后加工”比如给A菜加个装饰对应Spring里的AOP代理而这个装饰必须在上桌前才做做早了可能不对。三级缓存放ObjectFactory本质上是把“要不要做最后加工、做什么加工”这个决定推迟到最后一刻。没发生循环依赖这个工厂永远不会被调用对象走正常流程完成加工再放入一级缓存发生循环依赖了工厂被触发在拿“半成品”的那一刻才去判断是否需要生成代理对象。这个延迟是二级缓存替代不了的。用代码层面的语言说如果直接往二级缓存放对象那么这个对象在“提前曝光”的瞬间就已经定型了后续无法再执行getEarlyBeanReference逻辑尤其是AOP代理的介入会被错过。换句话说三级缓存的设计让Spring有机会在“循环依赖发生并且必须提前取出引用”这个精确的时间点去做代理增强。1.3 三级缓存处理流程速览先看一下整体流程后面再逐段拆。假设有两个beanA依赖BB也依赖A典型循环依赖。开始创建AA在doCreateBean时把自己包装成ObjectFactory放入三级缓存。A填充属性时发现需要B于是调用getSingleton(b)触发B的创建。B在填充属性时发现需要A于是调用getSingleton(a)。此时A还没有创建完但A在三级缓存里有工厂于是工厂被触发生成A的早期引用放入二级缓存。B拿到A的早期引用完成属性填充和初始化最终放入一级缓存。A从B那里拿到引用继续完成自己的属性填充和初始化也放入一级缓存。这个流程里三级缓存、二级缓存各自在哪个时机介入看一遍就能记住。2. 循环依赖与三级缓存的核心思路2.1 如果没有三级缓存循环依赖会怎样循环依赖本身是个“鸡生蛋、蛋生鸡”的问题创建A需要B创建B又需要A两边互相等谁都创建不完。如果Spring没有设计任何“提前曝光”机制依赖注入时拿不到对方的引用唯一的结果就是抛BeanCurrentlyInCreationException或者无限递归直接栈溢出。实际上JVM里的对象引用没有这么死板一个对象只要new出来即使它的某个属性是null也已经占据了一块内存。别人拿到这个引用后即使看到的是null也没关系因为Java传的是引用后续属性被赋值了拿到引用的人同样能看到最新值。这正是解决循环依赖的理论基础——我们可以把“没完全组装好的对象”先给出去等对方组装完自己也再把属性补上最终所有人都能拿到正确的对象。当然提前曝光有一个前提必须是单例、允许循环引用、且当前正在创建中。Spring用singletonsCurrentlyInCreation这个集合记录正在创建中的beanName只有在这个集合里的bean才允许被提前引用。原型Prototypebean不存在缓存无法提前曝光所以遇到循环依赖直接报错。2.2 getSingleton方法源码拆解三级缓存的入口方法就是DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)。这个方法是循环依赖解决的核心我建议直接贴进IDE里一行行看。protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步从一级缓存拿拿到了说明bean已经创建完成 Object singletonObject this.singletonObjects.get(beanName); // 一级缓存没有并且这个bean正在创建中 if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { // 第二步从二级缓存拿提前暴露的早期引用 singletonObject this.earlySingletonObjects.get(beanName); // 二级缓存也没有并且允许提前引用 if (singletonObject null allowEarlyReference) { synchronized (this.singletonObjects) { // 双重检查避免并发问题 singletonObject this.singletonObjects.get(beanName); if (singletonObject null) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null) { // 第三步从三级缓存找到ObjectFactory执行工厂方法 ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); // 把生成的对象放入二级缓存并从三级缓存移除 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }这段代码逻辑非常清晰就是一层层向上找一级有就直接返回没有就找二级二级没有再找三级并触发工厂。这里注意两个细节第一查找有前置条件。只有isSingletonCurrentlyInCreation(beanName)为true时才会允许从二级和三级缓存中取数据。这个判断非常关键它保证了一个bean在正常创建流程中不会被外部提前拿走半成品。你可以把它理解为“只有当前这锅饭正在煮才允许别人先盛半碗走”。第二三级缓存移到二级缓存是自动完成的。一旦触发singletonFactory.getObject()生成的对象立即放入earlySingletonObjects同时从singletonFactories移除。这意味着整个生命周期内工厂只会被调用一次避免重复执行代理逻辑或生成多个不一致的对象。2.3 锁机制和并发安全需要注意的是getSingleton方法里加了synchronized (this.singletonObjects)锁。Spring单例bean的创建本身就是线程安全的依赖这一点三级缓存操作时不需要额外的并发容器。二级缓存用了ConcurrentHashMap三级缓存用的则是普通的HashMap因为它们都在synchronized块保护下不会出现并发写入问题。这个细节很多人会忽略但面试时如果能主动提出来会给人留下“真的读过源码”的印象。3. 核心细节解析与实操要点3.1 addSingletonFactory三级缓存的注册时机三级缓存里那层ObjectFactory是什么时候放进去的这要回到AbstractAutowireCapableBeanFactory.doCreateBean方法。整个bean创建流程大致是createBeanInstance实例化→populateBean填充属性→initializeBean初始化。在实例化完成后、填充属性之前有一段代码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)); }earlySingletonExposure翻译过来就是“是否允许提前暴露”三个条件缺一不可单例bean、全局允许循环引用allowCircularReferences默认true、当前bean正在创建中。addSingletonFactory方法也很简单就是在三级缓存放一个lambda。重点在于这个lambda只是被注册并没有立即执行。也就是我在前面说的“预约生产单”——只有后续真正发生提前引用时才会执行。protected void addSingletonFactory(String beanName, ObjectFactory? singletonFactory) { synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); } } }顺便说一句这里能看到一级缓存判断如果一级缓存已经有这个bean说明创建流程都走完了没有必要再注册工厂。3.2 getEarlyBeanReference代理对象在这里产生三级缓存里那个ObjectFactory的生产逻辑最终会调到getEarlyBeanReference方法。变量名可能有点误导它返回的不是引用而是“允许提前暴露的bean实例”。核心代码如下protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject bp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }这个方法会遍历所有SmartInstantiationAwareBeanPostProcessor。我们平时用的AOP自动代理创建器AbstractAutoProxyCreator就实现了这个接口它的getEarlyBeanReference很关键Override public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey getCacheKey(bean.getClass(), beanName); // 标记这个bean已经被提前代理了 this.earlyProxyReferences.put(cacheKey, Boolean.TRUE); // 如果需要直接在这里生成代理对象 return wrapIfNecessary(bean, beanName, cacheKey); }注意这个earlyProxyReferences。它在AOP代理逻辑里扮演了“防重复代理”的角色。正常流程下AOP代理是在initializeBean最后的postProcessAfterInitialization方法里创建的。但如果循环依赖发生了代理就可能提前到getEarlyBeanReference这一步。被标记过TRUE之后后面再走到postProcessAfterInitialization时AbstractAutoProxyCreator就会发现这个bean已经代理过了跳过包装逻辑。这一步是三级缓存设计里最容易被忽略但最重要的点。3.3 代理模式下的两条路径详解把AOP和三级缓存放一起看你会发现一个非常优雅的设计同一个bean根据是否发生循环依赖有两条完全不同的代理创建路径但最终容器里都只有一份代理对象。路径一没有循环依赖。A在创建时注册三级缓存工厂但一直没人触发。A走完属性填充、初始化在applyBeanPostProcessorsAfterInitialization里被AbstractAutoProxyCreator.postProcessAfterInitialization包装成代理对象然后放入一级缓存。路径二有循环依赖。A在填充属性时被B反向引用getSingleton(a)触发三级缓存工厂getEarlyBeanReference里直接生成代理放入二级缓存B拿到的是A的代理对象。等A自己回到初始化阶段postProcessAfterInitialization检查到earlyProxyReferences已有标记不再重复代理最终这个代理对象被放入一级缓存。两条路径最终一级缓存里存的都是同一个代理对象。如果三级缓存放的是原始对象而不是ObjectFactory那么路径二中B拿到的A就没经过代理注入给B的是一个“残缺版本”等A真正完成代理后B仍然持有旧的原始对象整个Bean的代理语义就崩了。所以三级缓存存ObjectFactory的本质就是把“是否生成代理”这个决策从populateBean之前推迟到真正发生提前引用的那一刻。这样既保证了没有循环依赖时高性能地走正常流程又保证了有循环依赖时能及时生成正确的代理对象。4. 常见问题与排查技巧实录4.1 构造器循环依赖三级缓存为什么救不了三级缓存能解决循环依赖有一个隐藏前提bean必须已经完成实例化也就是new出来了。但构造器注入循环依赖发生时A的构造器需要BB的构造器需要A此时两边都还没有实例化没有任何“半成品”可以提前曝光三级缓存自然无从下手。遇到这种情况Spring会抛出BeanCurrentlyInCreationException。日志里通常会有类似这样的提示org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name a: Requested bean is currently in creation: Is there an unresolvable circular reference?解决方案一般有三种一是改用setter注入或字段注入让对象先实例化再填充依赖二是用Lazy对其中一个依赖做延迟注入注入进去的是一个代理占位符真正调用时才去创建目标bean三是重构业务逻辑拆开循环依赖结构。最后一种才是根本解法前两种只是绕过问题。4.2 原型Bean的循环依赖原型bean不缓存、不提前曝光每次getBean都重新创建一个新实例。即使创建了AA也不能被提前拿去做B的依赖因为下次getBean(a)又是一个新对象提前曝光完全没有意义。所以原型bean一旦发生循环依赖直接抛异常没有什么可商量的余地。4.3 Spring Boot 2.6及之后默认禁止循环依赖从Spring Boot 2.6开始官方把循环依赖默认行为改成了禁止spring.main.allow-circular-references默认值为false。这意味着即使你用字段注入启动时一旦检测到循环依赖容器也一样启动失败。很多人升级Spring Boot版本后突然遇到一堆循环依赖报错原因就在这。如果确实要临时恢复可以在application.yml里加一行spring: main: allow-circular-references: true但我建议不要图省事直接打开这个开关。循环依赖本身是设计上的一种坏味道能用Lazy、中间层或者事件驱动拆掉就尽量拆掉。把开关打开只是掩盖问题后面维护成本会越来越高。4.4 Async、事务等代理机制叠加循环依赖的坑这里的坑比较隐蔽。三级缓存的代理逻辑是通过SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference实现的但并不是所有代理处理器都实现了这个接口。像Async对应的AsyncAnnotationBeanPostProcessor早期版本就没有实现getEarlyBeanReference而是只实现了postProcessAfterInitialization。这就导致一个现象A和B循环依赖A还需要Async代理。循环依赖触发getEarlyBeanReference时因为AsyncAnnotationBeanPostProcessor不参与提前代理B拿到的A是原始对象等A自己走到postProcessAfterInitialization时Async代理才生成。最终结果是容器里的A是代理但B持有的A引用没有代理异步调用直接失效甚至可能出现类型转换异常。这类问题非常难排查因为表面看起来注入成功了但运行期行为不符合预期。我的经验是凡是涉及Async、Transactional等需要额外代理的bean如果同时卷入循环依赖尽早重构掉循环依赖。用Lazy打破循环是最快的临时方案但长期看还是要消除循环依赖。4.5 循环依赖问题排查速查表现象可能原因解决建议启动报BeanCurrentlyInCreationException日志指向构造器构造器注入形成循环依赖改为setter/字段注入或使用Lazy延迟其中一个依赖启动报错但日志没有明显堆栈Spring Boot 2.6默认禁止循环引用配置spring.main.allow-circular-referencestrue或重构依赖链bean注入成功但Async方法不生效Async代理未参与提前曝光B拿到原始对象消除循环依赖用Lazy或者事件解耦出现BeanNotOfRequiredTypeException循环依赖中代理对象和原始对象不一致优先检查AOP切面、代理处理器参与情况消除循环依赖原型bean循环依赖原型bean不缓存用单例bean或重组调用链如果你负责的项目已经能正常运行只是想在升级Spring Boot前排查隐患可以在启动时加-Dspring.main.allow-circular-referencesfalse跑一遍所有循环依赖会在启动阶段全部暴露出来比上线后遇到问题再排查要快得多。5. 手写一个简化版三级缓存5.1 核心类结构为了彻底搞明白三级缓存我建议你也自己动手写一个精简版。不需要复刻Spring只实现“实例化bean → 提前曝光 → 解决循环依赖”这一小段核心逻辑就够了。我写过一个MiniContainer核心就是模仿DefaultSingletonBeanRegistry的三个Map和getSingleton流程。5.2 简化版实现代码import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; public class MiniContainer { // 一级缓存完整对象 private final MapString, Object singletonObjects new HashMap(); // 二级缓存提前暴露的早期引用 private final MapString, Object earlySingletonObjects new HashMap(); // 三级缓存对象工厂 private final MapString, Supplier? singletonFactories new HashMap(); // 正在创建中的bean集合 private final MapString, Boolean singletonCurrentlyInCreation new HashMap(); public Object getBean(String beanName, SupplierObject creator) { // 先尝试从缓存获取 Object bean getSingleton(beanName, true); if (bean ! null) { return bean; } // 模拟单例创建过程 singletonCurrentlyInCreation.put(beanName, Boolean.TRUE); try { Object instance creator.get(); // 模拟属性填充阶段检查是否有循环依赖需要处理 if (singletonFactories.containsKey(beanName)) { // 正常情况下会在这里填充属性这里简化处理 } addSingleton(beanName, instance); singletonCurrentlyInCreation.remove(beanName); return instance; } catch (RuntimeException e) { singletonCurrentlyInCreation.remove(beanName); throw e; } } public void addSingletonFactory(String beanName, Supplier? factory) { synchronized (singletonObjects) { if (!singletonObjects.containsKey(beanName)) { singletonFactories.put(beanName, factory); earlySingletonObjects.remove(beanName); } } } public void addSingleton(String beanName, Object instance) { synchronized (singletonObjects) { singletonObjects.put(beanName, instance); singletonFactories.remove(beanName); earlySingletonObjects.remove(beanName); } } protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject singletonObjects.get(beanName); if (singletonObject null singletonCurrentlyInCreation.containsKey(beanName)) { singletonObject earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { synchronized (singletonObjects) { singletonObject singletonObjects.get(beanName); if (singletonObject null) { singletonObject earlySingletonObjects.get(beanName); if (singletonObject null) { Supplier? factory singletonFactories.get(beanName); if (factory ! null) { singletonObject factory.get(); earlySingletonObjects.put(beanName, singletonObject); singletonFactories.remove(beanName); } } } } } } return singletonObject; } }这里getBean的第二个参数Creator模拟的是完整创建过程实际使用时创建过程里如果遇到依赖项就调用getBean(另一个bean, ...)循环依赖就会触发三级缓存工厂提前返回早期引用。5.3 用MiniContainer模拟A和B的循环依赖假设A的创建过程需要BB的创建过程需要A。用上面这个容器跑一遍大概流程是getBean(a, creatorA)进入创建注册A的工厂到三级缓存。creatorA执行到属性填充需要B调用getBean(b, creatorB)。B进入创建注册B的工厂执行到属性填充需要A。此时getBean(a)从三级缓存找到A的工厂生成早期A放到二级缓存返回给B。B拿到A的早期引用填充完成注册到一级缓存。A拿到B的引用填充完成注册到一级缓存。跑完这个Demo后再回头看Spring源码你会发现核心逻辑几乎是逐行对应上面这些步骤的。Spring只是多了实例化策略、BeanPostProcessor、属性解析这些外围能力缓存和循环依赖的核心就这么点东西。我在实际读源码时还有个体会就是不要只盯着getSingleton方法看一定要配合doCreateBean里的addSingletonFactory时机一起看。很多人代码看了好几遍不知道为什么三级缓存里有工厂却没被调用就是因为没关注注册时机和触发时机是完全分离的。注册在实例化后、填充属性前触发在另一个bean反向依赖时只有把这两个点都找到整个链路才真正通了。6. 为什么三级缓存不能简化为二级缓存6.1 去掉三级缓存会引发什么问题有个经典的面试追问是如果把三级缓存去掉直接在一开始就把原始对象放进二级缓存循环依赖不同样也能解决吗表面上确实能解决“引用找不到”的问题B拿到A的原始对象后续A再完成代理但B始终持有的是A的原始对象。如果A最终需要AOP代理而B注入进来的是原始对象那B里面对A的调用就没有任何增强逻辑事务、权限、日志切面全部失效。这种情况下循环依赖“看似解决”实际是埋了一个运行期才会爆发的坑。换句话说二级缓存方案解决的是“有和没有”的问题三级缓存解决的是“完整不完整”的问题。AOP代理是Spring最核心的能力之一如果循环依赖以牺牲代理为代价来换取启动成功那这个框架的基本盘就崩了。6.2 为什么不能直接放代理对象进二级缓存看到这里你可能会想那把代理也提前做了在注册工厂时就生成代理放进二级缓存不就行了比如在addSingletonFactory时直接创建代理。理论上可行但Spring没有这么做原因我认为有两点一是成本问题。代理创建涉及CGLIB字节码生成或JDK动态代理开销不小。如果在注册三级缓存时就生成代理那些从来没有发生循环依赖的bean也会白做一次代理逻辑纯属浪费。Spring把工厂设计成延迟执行就是为了让代价只发生在真正需要的路径上。二是职责问题。实例化阶段bean的属性还是空切面表达式里如果依赖了某些属性值来做增强判断提前生成的代理可能不准确。把决策推迟到“即将被提前引用”的时刻能最大程度保证代理基于正确状态生成。从这两点看三级缓存并不是为了复杂而复杂它的每一层都有明确的职责和权衡逻辑。背下来很容易真正理解这里的取舍才算是把Spring的bean生命周期吃透了。6.3 这段设计对日常编码的启示三级缓存这个设计放到日常开发里其实也有借鉴意义。核心就是“延迟决策”在一个对象还没有完全准备好之前先不要把它交给其他人如果必须提前交出也要保证交出的是一个有正确能力的引用而不是一个残缺品。类比到日常编码比如你写一个初始化流程较重的组件中间状态可以被外部感知但又不希望外部依赖最终形态时就可以考虑用工厂或Provider来延迟产出。Spring三级缓存就是这样典型的“注册工厂、按需生产”模式而不是“一次性把对象准备好再共享”。很多框架源码读起来晦涩是因为我们没有带着“为什么”去读。三级缓存这个例子一旦读透再看Spring的其他组件比如事务、AOP、自动配置都会有一种豁然开朗的感觉。