资讯动态

Spring三级缓存源码剖析:从循环依赖到AOP代理的延迟决策

发布时间:2026/9/9 16:56:48 来源:尧图企业网站定制
我在面试候选人和带新人的时候几乎每次都会遇到同一个问题Spring 三级缓存到底是干什么的很多人能背出三个 Map 的名字但一追问“为什么非得三级两级不行吗”就支支吾吾了。还有不少人把“三级缓存”等同于“解决循环依赖”这个理解不能说错但太片面了。实际上三级缓存真正的精妙之处在于它把“对象什么时候被代理”这个决策延迟到了最后一刻。这篇文章我从源码出发把三级缓存从设计动机、建立时机、访问时序到各种实战场景全部复盘一遍。无论你是准备面试、在写手写 Spring 练手项目还是在线上排查 BeanCurrentlyInCreationException都能从中找到能直接落地的答案。1. 三级缓存的整体设计与核心思路很多人一上来就扎进 DefaultSingletonBeanRegistry 的代码里结果看半天越看越晕。我建议换个顺序先搞明白 Spring 要解决什么问题再回头看代码每个 Map 的作用就非常清晰了。1.1 缓存要解决的核心问题循环依赖中的“半成品”循环依赖说白了就是 A 依赖 BB 又依赖 A。Spring 创建 Bean 的时候按照正常流程走实例化 - 属性填充 - 初始化。A 实例化之后需要填充属性发现需要 B于是去创建 BB 实例化之后需要填充属性发现需要 A于是又回头找 A。但这时候 A 还没创建完连属性都还没注入它就是一个“半成品”。如果没有缓存机制这种互相引用就会无限递归最终栈溢出。所以 Spring 的解决思路很直接A 在实例化完成之后先把“半成品”暴露到一个公共区域。B 在创建时发现自己依赖 A直接去这个公共区域拿那个半成品先顶着注入进去然后 B 继续走完自己的创建流程。等 B 创建完了A 再回来继续填充剩余属性最终把完整的 A 补全。这个“公共区域”就是缓存。换句话说循环依赖的关键不是“缓存三个 Map”而是让 Bean 的创建过程允许“提前暴露”自己。1.2 为什么两级缓存不够代理对象时机问题你可能马上会想到既然要提前暴露半成品那一级缓存放完整 Bean二级缓存放半成品不就行了吗Spring 为什么还要搞第三级带着这个疑问先假设只有两级缓存。A 创建时实例化完成把原始对象放入二级缓存。B 创建时从二级缓存拿到 A 的原始对象完成注入。A 继续走初始化流程最后把成品放入一级缓存。这个流程在没有 AOP 的场景下完全没问题。但一旦 A 上有切面Spring 最终放进一级缓存的必须是一个代理对象而不是原始对象。问题就来了B 在二级缓存里拿到的 A 是原始对象而最终 A 的成品是代理对象。这就导致 B 持有的 A 和容器里的 A 不是同一个对象AOP 增强逻辑在 B 内部完全失效。你可能会说那我在 A 实例化完成之后直接把代理对象放到二级缓存不就行了这样 B 拿到的就是代理对象最终进一级缓存的也是代理对象两边一致了。这个方案理论可行但过早创建代理对象会带来新的麻烦A 可能根本不会被循环依赖引用创建代理就是白白的开销而且如果 A 在属性填充阶段还要经历其他 BeanPostProcessor 的包装提前生成代理很容易造成重复代理或错误包装。所以 Spring 引入了三级缓存核心是将“是否生成代理、生成什么样的代理”这个决策延迟到真正发生提前引用的那一刻。三级缓存里放的并不是对象而是一个 ObjectFactory 工厂只有在这个工厂真正被调用时才执行生成“早期引用”可能是代理的逻辑。1.3 三级缓存的语义划分与数据流向搞清楚了设计动机三个 Map 的语义就非常清楚了一级缓存 singletonObjects存放创建完成的成品 Bean这是 Spring 对外提供服务的最终对象。二级缓存 earlySingletonObjects存放提前暴露的早期引用。这个对象可能还没有完成属性填充和初始化但它已经是“半成品”中的 MVP。三级缓存 singletonFactories存放 ObjectFactory 工厂工厂的 getObject() 方法里通常会调用 getEarlyBeanReference 来生成早期引用。数据流向也很有意思正常情况下 Bean 创建完成后直接进入一级缓存三级缓存走完即删。一旦发生循环依赖Bean 在实例化后先放入三级缓存被引用时通过三级缓存的工厂生成早期引用放入二级缓存同时删除三级缓存。最终创建完成后再放入一级缓存清理二级缓存。整个过程同一个 Bean 在任意时刻最多只在一个缓存里存在有效引用。2. 源码级拆解三级缓存的建立与访问有了整体思路现在可以进入源码了。我用的示例代码是 Spring 5.3.x 版本的 DefaultSingletonBeanRegistry这也是目前大多数项目实际使用的版本。Spring 6 和 Spring Boot 3 里的核心逻辑没有变化可以放心对照。2.1 三个 Map 的字段定义与角色定位打开 DefaultSingletonBeanRegistry最先看到的就是这几个字段/** 一级缓存Cache of singleton objects: bean name -- bean instance */ private final MapString, Object singletonObjects new ConcurrentHashMap(256); /** 三级缓存Cache of singleton factories: bean name -- ObjectFactory */ private final MapString, ObjectFactory? singletonFactories new HashMap(16); /** 二级缓存Cache of early singleton objects: bean name -- bean instance */ private final MapString, Object earlySingletonObjects new HashMap(16); /** 已注册的单例 Bean 名称集合 */ private final SetString registeredSingletons new LinkedHashSet(256); /** 正在创建中的 Bean 名称集合 */ private final SetString singletonsCurrentlyInCreation new ConcurrentHashMap(16).newKeySet();注意一个细节一级缓存用的是 ConcurrentHashMap二级和三级缓存用的是普通 HashMap但所有读写操作都在 synchronized(this.singletonObjects) 同步块内完成。这个设计谈不上谁更优但至少保证了缓存操作的原子性。读代码的时候如果看到 HashMap 就担心线程安全那是多虑了。另外 singletonsCurrentlyInCreation 这个集合虽然不是缓存但在循环依赖判断中起着关键作用。getSingleton 的流程里只有判断“当前 Bean 正在创建中”才会去走三级缓存的工厂逻辑。2.2 三级缓存的建立addSingletonFactory 的调用时机三级缓存的建立发生在 AbstractAutowireCapableBeanFactory.doCreateBean 方法中。核心代码大约是这样protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) { // 1. 实例化 Bean BeanWrapper instanceWrapper null; if (mbd.isSingleton()) { instanceWrapper this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper null) { instanceWrapper createBeanInstance(beanName, mbd, args); } Object bean instanceWrapper.getWrappedInstance(); Class? beanType instanceWrapper.getWrappedClass(); // 2. 允许提前暴露注册三级缓存 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); } // 3. 属性填充 Object exposedObject bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { // ... } // 4. 处理和提前引用不一致的情况 if (earlySingletonExposure) { Object earlySingletonReference getSingleton(beanName, false); if (earlySingletonReference ! null) { if (exposedObject bean) { exposedObject earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping !hasDependentBean(beanName)) { // 抛出 BeanCurrentlyInCreationException } } } return exposedObject; }这里有几个关键点值得展开。第一earlySingletonExposure的判断条件有三个必须是单例、容器允许循环引用、当前 Bean 正在创建中。注意“允许循环引用”的开关是allowCircularReferences默认值是 true。你可以在自定义 BeanFactoryPostProcessor 里把它设为 false或者在 Spring Boot 2.6 中通过配置默认关闭。第二addSingletonFactory注册的是一个 Lambda 表达式里面的getEarlyBeanReference方法才是真正生成早期引用的地方。Lambda 表达式不会立刻执行只有后续发生循环依赖时getSingleton(String, boolean)方法内部调用了这个工厂代理决策才会真正发生。第三我看到有些分析文章纠结于“Lambda 里引用的 bean 变量是不是 null”这个问题。实际上执行到addSingletonFactory这一行时bean变量已经通过instanceWrapper.getWrappedInstance()赋值了它指向的就是当前刚创建出来的早期实例。Lambda 捕获的是这个变量调用时它已经指向有效的实例对象不会出现 null 的问题。2.3 getEarlyBeanReference代理决策的真正执行点三级缓存的工厂里getObject() 调用的是 AbstractAutowireCapableBeanFactory.getEarlyBeanReference这个方法会调用 SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference 对 Bean 进行提前包装。AbstractAutoProxyCreator 是 Spring AOP 的核心实现类它的 getEarlyBeanReference 方法大致是这样的Override public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey); }这段代码的含义是当 A 被 B 提前引用时Spring 在这个时点检查 A 是否需要 AOP 代理。如果需要就生成代理对象如果不需要就原样返回原始对象。关键变量earlyProxyReferences记录了哪些 Bean 已经被提前处理过。如果 A 在循环依赖里被引用过后续在 initializeBean 阶段AbstractAutoProxyCreator 的 postProcessAfterInitialization 方法会先检查 earlyProxyReferences。如果发现这个 Bean 已经被提前代理过就直接返回原始 bean不再重复创建代理避免二次包装。这就是 Spring 保证“同一个 Bean 不会出现两个不同代理”的核心逻辑。2.4 getSingleton 双检索与缓存迁移接下来是缓存读取的核心方法protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 先从一级缓存拿成品 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? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 执行三级缓存工厂触发 getEarlyBeanReference singletonObject singletonFactory.getObject(); // 放入二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); // 删除三级缓存 this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }这段代码虽然不长但每个分支都有存在的理由。一级缓存没有而且 Bean 不在创建中说明真的没有这个 Bean返回 null 让上层继续创建。一级缓存没有但 Bean 正在创建中说明循环依赖发生了于是继续向下找。二级缓存有值说明之前已经有别的 Bean 先引用过当前 Bean三级缓存工厂已经执行过一次此时直接复用早期引用不再重复执行工厂。二级没有三级有说明当前 Bean 还没有被任何 Bean 引用过于是执行三级缓存工厂生成早期引用。执行完成后立即将工厂从三级缓存删除防止后续重复执行。同步块和双重检查的组合很典型。即使多处并发调用 getSingleton最终执行三级工厂的只会有一个线程其他线程会阻塞等待锁释放后直接拿到二级缓存里已经生成好的早期引用。这里我额外提醒一句三级缓存里的 ObjectFactory 只会执行一次。执行完就会迁移到二级缓存三级缓存中的 entry 被删除。这个“一次且仅一次”是设计上的硬约束不能打破。2.5 addSingleton成品入一级缓存Bean 完成属性填充和初始化之后Spring 调用 addSingleton 方法将成品放入一级缓存protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } }这段代码的逻辑非常清晰成品入一级缓存同时清理二级和三级缓存中同一个 Bean 的残留。从这一刻起容器里对外提供的就是这个成品对象。你会发现整个过程里二级缓存只存放“当且仅当有 Bean 提前引用它”时的早期引用。如果没有发生循环依赖Bean 会直接从三级缓存跳到一级缓存二级缓存完全不会被用到。这也就解释了为什么“两级缓存够用”的讨论总是差那么一口气两级缓存是为“提前暴露”设计的但它无法解决“暴露原始对象还是暴露代理对象”的决策问题。3. 全场景复盘从最简单到最复杂搞清楚源码之后我们用场景来验证这套机制在不同情况下的表现。这部分内容对应的就是“全场景复盘”的核心。3.1 场景一无 AOP 的 setter 循环依赖这是最简单的场景。A 和 B 都通过 setter 注入互相依赖且没有 AOP。整个执行时序如下getBean(A) 创建 AA 实例化完成registerSingleton 将 A 标记为“创建中”。addSingletonFactory(A) 注册 A 的三级缓存。populateBean 时发现 A 依赖 B触发 getBean(B)。B 实例化完成addSingletonFactory(B) 注册 B 的三级缓存。populateBean 时发现 B 依赖 A触发 getBean(A)。由于 A 正在创建中getSingleton 走到三级缓存工厂getEarlyBeanReference 里没有 AOP 逻辑直接返回原始 A。这个原始 A 被放入二级缓存删除三级缓存中的 A 工厂。B 拿到了原始 A完成属性填充B 初始化完成addSingleton(B)一级缓存出现 B。回到 A 的创建流程A 拿到 B注入属性A 初始化完成addSingleton(A)一级缓存出现 A。这个场景里A 在二级缓存放过但最终一级缓存里的 A 和二级缓存里那个被 B 持有的 A 是同一个对象所以不存在一致性问题。3.2 场景二带 AOP 的 setter 循环依赖现在给 A 加上事务切面或自定义 AOP 切面。整体流程与场景一基本一样区别发生在第 5 步getEarlyBeanReference 会调用 AbstractAutoProxyCreator此时发现 A 需要被代理于是提前生成了 A 的代理对象。二级缓存里放的是代理对象B 注入的就是这个代理对象。回到 A 的创建流程时A 还会经历 initializeBeanAbstractAutoProxyCreator 的 postProcessAfterInitialization 发现 A 已经被提前代理过earlyProxyReferences 中有记录于是不再重复创建代理。doCreateBean 尾部还有个一致性修正逻辑earlySingletonReference 存在且 exposedObject bean就用 earlySingletonReference代理对象覆盖返回值。最终放进一级缓存的也是同一个代理对象。整个过程下来B 持有的 A 代理、二级缓存里的 A 代理、一级缓存里的 A 代理三者是同一个对象。这个结果正是设计者想要的。如果 A 既有 AOP 代理又被其他 Bean 后续包装成另一个对象导致 exposedObject ! bean同时又有其他 Bean 依赖 ASpring 会抛出 BeanCurrentlyInCreationException 来提示你存在不一致风险。源码里那行allowRawInjectionDespiteWrapping就是从这个角度设计的逃逸开关默认是 false不建议随意开启。3.3 场景三构造器循环依赖构造器循环依赖比较特殊因为 A 在构造器参数解析阶段就需要 B 的实例而这个时候 A 连实例化都没完成三级缓存还没有注册。B 创建时又需要 A去缓存里找不到任何 A 的引用因为 A 的三级缓存还没建立最终抛出 BeanCurrentlyInCreationException。说白了Spring 的三级缓存只能解决“实例化完成之后”的循环依赖也就是属性注入和 setter 注入场景。构造器循环依赖属于无解问题必须通过重构代码解决常见方案有三个用 Lazy 注解构造器参数注入一个代理对象而不是真实依赖。把构造器注入改成 setter 注入或字段注入。引入 ObjectProvider在需要时延迟获取。3.4 场景四原型 Bean 的循环依赖原型 Bean 不在三级缓存的处理范围内。每次 getBean 都会创建新实例根本不会被提前暴露因此原型 Bean 之间一旦循环依赖直接抛异常。单例 Bean 依赖原型 Bean 是可以的。每次 getBean 都会创建新的原型实例不会缓存也不存在循环依赖问题。反过来原型 Bean 如果要注入一个单例 Bean实际上是原型 Bean 在创建时从容器里获取单例 Bean这个没问题。真正要注意的是尽量避免原型 Bean 的构造器循环依赖因为没有任何机制能兜底。3.5 场景五Async 与循环依赖的经典坑这个场景我单独拿出来说因为线上遇到最多。Async 标注的 Bean 本质上是一个通过 AsyncAnnotationBeanPostProcessor 生成的代理。但这个后置处理器没有重写 getEarlyBeanReference 方法。所以在循环依赖场景下A 在三级缓存被提前引用时getEarlyBeanReference 并不会触发 Async 代理的生成B 拿到的是原始 A。A 最终完成初始化后AsyncAnnotationBeanPostProcessor 在 postProcessAfterInitialization 阶段才生成 Async 代理。此时 doCreateBean 尾部的修正逻辑发现 earlySingletonReference 存在但 exposedObjectAsync 代理和 bean原始对象不是同一个对象。如果 A 还有别的依赖 Bean就会抛异常如果没有虽然不会抛异常但 B 持有的 A 是原始对象Async 增强在 B 内部完全失效。这解释了为什么 Spring Boot 2.6 开始默认禁止循环依赖循环依赖虽然能被三级缓存兜住但它和 Async、各种自研 AOP 的兼容性问题实在太多属于“能跑但脆”的架构设计。遇到这种情况最好的处理方式不是去调开关而是把循环依赖拆掉。3.6 场景六Lazy 是怎样“曲线救国”的Lazy 注解解决循环依赖的原理和三级缓存完全不同。假设 A 的 setter 注入 BB 的 setter 注入 A且其中一个注入点标注 Lazy。Spring 在创建 A 时发现 B 的注入点标了 Lazy不会立即创建真正的 B而是先生成一个 B 的代理对象注入到 A 里。A 完成创建。真正调用到这个代理对象的方法时代理内部才会触发 getBean(B) 去获取真实的 B。这个方案的代价是B 的代理对象和最终真实 B 不是同一个对象但代理对象内部持有对真实 B 的引用方法调用会被转发过去所以大多数场景没问题。同时Lazy 可以绕开早期引用的各种代理冲突因为它压根不依赖三级缓存。3.7 场景七手写 Spring 时如何参考三级缓存做手写 Spring 练手项目的时候很多人的目标是“解决 setter 循环依赖”。这时候没必要把源码里那套复杂性全部搬过来可以先写一个简化版本实例化所有 Bean按字段名依赖关系做两轮拓扑填充。这种方案本质是“先实例化、后注入”副作用是必须先知道所有 Bean 的类型动态注册 Bean 的能力会弱一些。如果你想在简化版本里也保留“边创建边暴露”的体验可以照着 Spring 的逻辑简化用 Map 存放工厂 Lambda遇到循环引用时执行工厂获取早期对象。这个方案比 StackOverflow 上常见的“直接放原始对象到二级缓存”要优雅一些也更接近 Spring 的真实语义。后面给出一段可运行的最小示例代码。4. 手写一个最小三级缓存 Demo为了验证三级缓存的机制我曾经写过一个仅 80 行左右的最小容器专门用来演示 setter 循环依赖的解决。这里把核心代码分享出来只要复制到本地就能跑。import java.lang.reflect.Field; import java.util.*; public class MiniSpring { // 一级缓存完整 Bean private final MapString, Object singletonObjects new HashMap(); // 二级缓存早期引用 private final MapString, Object earlySingletonObjects new HashMap(); // 三级缓存ObjectFactory private final MapString, SupplierObject singletonFactories new HashMap(); // 正在创建中的 Bean 名称 private final SetString singletonsCurrentlyInCreation new HashSet(); // beanName - Class 映射 private final MapString, Class? beanDefinitions new HashMap(); public void register(String name, Class? clazz) { beanDefinitions.put(name, clazz); } public T T getBean(String name) { // 1. 一级缓存 Object singletonObject singletonObjects.get(name); if (singletonObject ! null) { return (T) singletonObject; } // 2. 正在创建中说明可能发生循环依赖查二级缓存 if (singletonsCurrentlyInCreation.contains(name)) { singletonObject earlySingletonObjects.get(name); if (singletonObject null) { // 3. 查三级缓存执行工厂并迁移 SupplierObject factory singletonFactories.get(name); if (factory ! null) { singletonObject factory.get(); earlySingletonObjects.put(name, singletonObject); singletonFactories.remove(name); } } if (singletonObject ! null) { return (T) singletonObject; } } // 4. 创建新 Bean return createBean(name); } private T T createBean(String name) { singletonsCurrentlyInCreation.add(name); try { Class? clazz beanDefinitions.get(name); // 实例化使用无参构造器 Object instance clazz.getDeclaredConstructor().newInstance(); // 提前暴露三级缓存注册工厂 final Object currBean instance; singletonFactories.put(name, () - currBean); // 属性填充按字段名从容器获取 for (Field field : clazz.getDeclaredFields()) { String fieldName field.getName(); Object dep getBean(fieldName); field.setAccessible(true); field.set(instance, dep); } // 完成创建放入一级缓存清理二三级 singletonObjects.put(name, instance); singletonFactories.remove(name); earlySingletonObjects.remove(name); return (T) instance; } catch (Exception e) { throw new RuntimeException(创建 Bean 失败: name, e); } finally { singletonsCurrentlyInCreation.remove(name); } } public static void main(String[] args) { MiniSpring container new MiniSpring(); container.register(a, A.class); container.register(b, B.class); A a container.getBean(a); System.out.println(A 中的 B a.getB()); System.out.println(A 中的 B 中的 A a.getB().getA()); } }这个 Demo 的核心逻辑是创建 A 时先实例化 A把工厂放入三级缓存然后填充字段发现依赖 B于是 getBean(B)。B 实例化后同样注册三级缓存填充字段发现依赖 A此时 A 正在创建中一级二级缓存都没有 A但三级缓存有 A 的工厂执行工厂返回原始 A放入二级缓存。B 拿到 A 完成创建。回到 A 的创建流程拿到 B完成创建A 入一级缓存。最终输出会显示A 中持有的 B 和 B 中持有的 A 指向的是同一个对象循环依赖被成功解开。注意 Demo 里没有实现 AOP 代理所以三级缓存工厂直接返回原始对象。如果要在 Demo 中演示代理场景可以在工厂执行时判断是否需要代理并生成代理逻辑上完全一致。5. 常见问题排查与避坑实录代码看再多最后还是要落到“线上出问题怎么办”。我把高频问题整理成一张速查表再展开讲几个坑。现象常见原因排查方向BeanCurrentlyInCreationException构造器循环依赖、Async 组合循环依赖、原型 Bean 循环依赖看异常栈确认卡在哪个阶段用 Lazy 或拆依赖注入的对象缺乏 AOP 增强循环依赖中提前引用了原始对象之后才生成代理检查是否有自研 BeanPostProcessor 没有实现 getEarlyBeanReferenceBean 被创建了两次原型 Bean 注入到单例 Bean或配置错误检查 scope确认不是 getBean 触发新实例关闭循环依赖后老项目直接启动失败Spring Boot 2.6 默认禁止循环依赖逐个找出循环引用链用 Lazy 或设计模式解除两个对象 hashCode 不同但理论上应是同一个 BeanAOP 早期代理和后期包装不一致沿 getSingleton 流程断点观察三级缓存迁移过程unregister 后容器报找不到缓存手动销毁了 Bean 但容器仍持有引用检查是否实现 DisposableBean销毁方法是否误清理缓存第一条异常最常见的坑在于“定位不准”。很多人在循环依赖报错时第一反应是去查缓存配置其实 BeanCurrentlyInCreationException 的异常栈会非常清晰地告诉你卡在哪一步。如果是构造器注入栈会落在 createBeanInstance 里如果是 Async 组合栈会落在 initializeBean 阶段。定位准确才能对症下药。第二个坑非常隐蔽。比如自研了一个 BeanPostProcessor重写了 postProcessAfterInitialization 给某些 Bean 动态生成了代理但没有同步处理 getEarlyBeanReference。在普通场景下没有异常一旦出现循环依赖B 拿到的 A 是原始对象最终容器里的 A 是你的动态代理对象两方完全不一致。这个问题几乎不会报错但结果是业务数据错乱或 AOP 失效。所以自定义 BeanPostProcessor 时务必对照 SmartInstantiationAwareBeanPostProcessor 的接口好好检查一遍。第三个坑是 Spring Boot 2.6 升级后的普遍问题。老项目中循环依赖很多升级直接报错。这时候我的建议是不要急着把 allow-circular-references 设回 true先跑一遍启动日志把循环依赖链全部打出来逐个评估。很多循环依赖本质上是设计问题比如 Service 互相调用中间加一个 Facade 层就能彻底解开。暂时解不开的再用 Lazy 过渡。长期维护的代码能拆就拆别让三级缓存变成结构缺陷的挡箭牌。第四个坑是手写或阅读源码时的常见误解很多人以为三级缓存是“同步地”把 Bean 最终代理对象直接生成好再放入缓存实际上三级缓存只是工厂生成动作是延后的。理解了这一点再看为什么二级缓存里放的是“早期引用”而不是“最终代理”思路就顺了。最后聊聊我自己的习惯。阅读这段源码时我推荐的顺序是先看 getSingleton(String, boolean) 的完整分支再看 doCreateBean 里的 addSingletonFactory 调用然后看 getEarlyBeanReference最后回到 addSingleton 清理缓存。把这条主链理顺之后再去研究 populateBean、initializeBean、applyBeanPostProcessorsAfterInitialization 这些周边逻辑会轻松得多。三级缓存并非一个独立模块它和 AOP、BeanPostProcessor 的执行顺序深度绑定脱离执行链路去看缓存永远都是雾里看花。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价