ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring三级缓存机制深度解析:从循环依赖到AOP代理的源码级揭秘

2026/8/13 3:06:18 拓冰建站 浏览量
Spring三级缓存机制深度解析:从循环依赖到AOP代理的源码级揭秘 1. 从一次诡异的Bean创建异常说起那天下午我正在调试一个看起来平平无奇的Spring Boot服务。服务启动时控制台突然抛出了一个异常不是常见的BeanCreationException而是一个更底层的、关于BeanCurrentlyInCreationException的错误错误信息里反复提到了“循环依赖”和“正在创建中”。这让我有点意外因为项目结构并不复杂理论上不应该出现这么明显的循环依赖问题。我检查了Bean的定义发现是两个Service之间互相Autowired了对方这确实是教科书式的循环依赖场景。但奇怪的是这个项目之前一直运行得好好的直到我引入了一个Async注解在其中一个Service的方法上问题才暴露出来。这个经历让我重新审视Spring处理循环依赖的机制。我们都知道Spring通过“三级缓存”解决了单例Bean的循环依赖问题这几乎是面试八股文的必考题。但当你真的在复杂场景下比如结合了AOP、Async、Transactional遇到问题时仅仅背出“一级缓存放成品Bean二级缓存放早期暴露对象三级缓存放ObjectFactory”是远远不够的。你需要真正理解每一级缓存存在的必要性、它们协同工作的时序以及这个精巧设计背后的权衡与边界。这次我们就抛开那些笼统的概念直接深入到DefaultSingletonBeanRegistry的源码里看看这三级缓存到底是如何在Bean的生命周期中“辗转腾挪”化险为夷的。2. 三级缓存的庐山真面目源码中的三个Map要理解三级缓存首先得找到它们藏在哪。在Spring IoC容器的核心——DefaultSingletonBeanRegistry类中定义了三个至关重要的Map这就是我们常说的三级缓存。它们不是任何配置而是Spring框架为解决单例Bean循环依赖而设计的内部数据结构。2.1 一级缓存singletonObjects– 成品的归宿/** Cache of singleton objects: bean name to bean instance. */ private final MapString, Object singletonObjects new ConcurrentHashMap(256);这是大家最熟悉的一级缓存也叫“单例池”。它的角色非常明确存放已经完全初始化好的、可供直接使用的成品Bean。当一个Bean走完了完整的生命周期实例化、属性填充、初始化它就会被放入这个singletonObjects中。之后任何地方通过getBean()方法请求这个Bean时容器会首先来这里查找。如果找到了直接返回这是性能最高、最直接的路径。你可以把它想象成一个餐厅的“出菜口”做好的菜都放在这里服务员直接从这里端给客人。2.2 二级缓存earlySingletonObjects– 半成品的临时驿站/** Cache of early singleton objects: bean name to bean instance. */ private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16);二级缓存的存在是解决循环依赖的关键一环。它存放的是**“早期暴露”的Bean引用**。什么是早期暴露在Bean的生命周期中通常是在“实例化”调用构造方法创建出对象之后但在“属性填充”和“初始化”之前Spring会尝试将这个刚创建出来、还是个“空壳”的对象提前暴露出去。这个对象可能还没有注入依赖也没有执行PostConstruct方法但它已经是一个Java对象了。二级缓存就是这个提前暴露的对象的临时存放点。它的生命周期很短主要服务于解决循环依赖的场景。一旦Bean完全初始化完毕它会被从earlySingletonObjects移动到singletonObjects然后在这里被清理掉。它就像一个餐厅的“备餐区”菜只进行了一半的加工比如肉切好了但还没下锅炒但因为下一道菜急需这个原料所以先把它拿出来应急。2.3 三级缓存singletonFactories– 生产半成品的工厂/** Cache of singleton factories: bean name to ObjectFactory. */ private final MapString, ObjectFactory? singletonFactories new HashMap(16);三级缓存是最特殊、也最容易被误解的一级。它存放的不是Bean对象本身而是一个ObjectFactory?对象工厂。这个工厂的作用是当需要获取某个Bean的早期引用时能够动态地创建或返回一个处理过的对象。为什么需要工厂而不是直接放对象这涉及到Spring AOP以及通过Async、Transactional等注解实现的代理。如果一个Bean需要被代理例如被AOP切面增强那么最终暴露给其他Bean使用的不应该是原始对象而应该是它的代理对象。这个代理对象何时创建是在Bean初始化后由BeanPostProcessor处理的。但在循环依赖的场景下其他Bean在属性注入阶段就需要引用这个Bean此时它的代理可能还没生成。ObjectFactory就是为了解决这个“时机”问题。它封装了一段逻辑当被调用时它可以判断当前情况决定是返回原始对象还是返回一个提前创建好的代理对象通过SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference方法。这给了Spring极大的灵活性。你可以把它理解为一张“菜品加工券”凭此券可以到后厨要求对半成品早期对象进行特定的加工如代理然后得到最终可用的形态。注意三级缓存singletonFactories在Bean的早期引用被获取一次后通常就会被移除。其对应的ObjectFactory是一次性的执行完就失效了。这是为了确保Bean引用的唯一性和一致性。3. 循环依赖的破局三级缓存协同作战全流程理论总是抽象的我们通过一个经典的Setter注入循环依赖场景来还原三级缓存是如何一步步配合打破僵局的。假设有AService和BService互相依赖。Service public class AService { Autowired private BService bService; // ... } Service public class BService { Autowired private AService aService; // ... }Spring容器启动开始创建AService这个Bean。这个过程发生在AbstractAutowireCapableBeanFactory.doCreateBean()方法中。3.1 第一步创建A实例并提前暴露工厂实例化Spring调用AService的构造方法创建出一个原始对象a。此时a里面的bService字段是null。暴露工厂关键操作在属性填充之前Spring会执行一个至关重要的操作——addSingletonFactory。// AbstractAutowireCapableBeanFactory.doCreateBean boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 将BeanName和对应的ObjectFactory放入三级缓存 addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }此时AService对应的ObjectFactory被放入了三级缓存singletonFactories。这个工厂封装了获取AService早期引用的逻辑其中包含了处理AOP代理的可能性。注意此时一级和二级缓存都没有AService。3.2 第二步填充A的属性触发B的创建属性填充Spring开始为AService的实例a填充属性Autowired BService bService。获取B为了得到BService容器调用getBean(“bService”)。创建B实例和A一样Spring开始创建BService。先调用构造方法创建出原始对象b然后将BService的ObjectFactory也放入三级缓存。3.3 第三步填充B的属性向A求助B的属性填充Spring开始为BService的实例b填充属性Autowired AService aService。获取A循环点容器再次调用getBean(“aService”)来获取AService。三级缓存的第一次立功这次getBean不会从头创建A而是会触发缓存查找逻辑。在DefaultSingletonBeanRegistry.getSingleton(String, boolean)方法中查找顺序是一级缓存singletonObjects没有A还没初始化完。二级缓存earlySingletonObjects没有。三级缓存singletonFactories找到了之前存放的AService的ObjectFactory。执行工厂Spring调用这个ObjectFactory.getObject()。这个方法会执行getEarlyBeanReference如果AService需要被代理比如有AOP切面这里就会提前生成代理对象如果不需要则直接返回原始对象a。我们假设这里不需要代理返回了原始对象a。升级缓存从三级缓存获取到对象a后Spring会做两件事将对象a放入二级缓存earlySingletonObjects。将AService对应的ObjectFactory从三级缓存中移除。 现在AService的早期引用a存在于二级缓存中。这个a被成功注入到了BService的实例b中。3.4 第四步B完成初始化回归一级缓存B完成生命周期BService的属性aService已经注入完毕注入了A的早期引用a接着Spring完成BService的后续初始化执行PostConstruct等得到一个完全体b。B入驻一级缓存完全体b被放入一级缓存singletonObjects。同时BService相关的ObjectFactory从三级缓存移除早期引用从二级缓存移除如果之前有的话。3.5 第五步A完成初始化闭环A继续未竟之事此时AService的属性填充步骤之前在等getBean(“bService”)返回终于收到了返回值——刚刚创建好的完全体b。于是b被注入到a的bService字段中。A完成生命周期AService接着完成自己的初始化过程。A入驻一级缓存清理二级缓存完全体的a被放入一级缓存。同时Spring会发现AService在二级缓存中还有一个早期引用于是将其从二级缓存中移除。至此循环依赖完美解决。AService和BService的成品Bean都安静地待在一级缓存里可供随时使用。整个过程中三级缓存提供了获取早期引用的能力二级缓存作为临时存储避免了工厂的重复执行一级缓存则是最终的归宿。实操心得理解这个流程后你就能解释很多现象。比如为什么构造器注入无法解决循环依赖因为构造器注入发生在实例化阶段此时对象还没创建出来更无法提前暴露ObjectFactory到三级缓存死锁必然发生。Setter/字段注入之所以可以是因为注入发生在实例化之后、初始化之前那时已经有对象和工厂可以提前暴露了。4. 为什么必须是三级两级或一级不行吗这是一个经典的面试题也是理解Spring设计精妙之处的关键。我们分别来分析如果只有一级或两级缓存会怎样。4.1 假设只有一级缓存singletonObjects如果只有成品缓存那么流程根本无法进行。当创建A到一半需要B而创建B又需要A时由于A还没有成为“成品”未完成属性填充和初始化它不会被放入一级缓存。B在获取A时直接返回null或抛出异常循环依赖无解。所以必须有一个空间来存放“半成品”。4.2 假设只有两级缓存成品缓存半成品缓存这是我们思考的重点。很多初学者会想既然二级缓存earlySingletonObjects已经能存放早期对象了为什么还要三级缓存singletonFactories这个工厂直接把早期对象放进二级缓存不行吗理论上对于没有AOP的普通Bean是可行的。在A实例化后直接把原始对象a扔进二级缓存。B创建时需要A直接从二级缓存拿到a注入然后B完成初始化A再完成初始化。似乎也能跑通。但一旦引入AOP或任何需要创建代理的BeanPostProcessor问题就来了。核心矛盾在于代理对象创建的时机与循环依赖的需求时机存在冲突。正常的、无循环依赖的Bean创建流程实例化 - 属性填充 - 初始化 - (AOP)创建代理 - 放入一级缓存。代理是在初始化之后才创建的。循环依赖下的需求B在属性填充阶段就需要A的引用。如果此时A还在创建中它应该给B一个什么给原始对象但最终B注入的应该是A的代理对象否则AOP增强会失效例如Transactional注解的方法调用同类其他方法时如果注入的是原始对象事务会失效。给代理对象但A的初始化还没完成代理对象按理说还无法生成。三级缓存中的ObjectFactory正是为了解决这个“时机悖论”而设计的。它不是一个简单的对象存储而是一个延迟决策和处理的机制。当B通过getSingleton(“aService”)请求A时会调用三级缓存中的ObjectFactory.getObject()。这个方法内部会调用SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()。Spring内置的AOP代理创建器AbstractAutoProxyCreator就实现了这个接口。如果A最终需要代理在getEarlyBeanReference()方法中AOP框架会判断是否已经为这个Bean创建过早期代理。如果是第一次被索取早期引用它会提前为A创建代理对象并返回。这个代理对象会被放入二级缓存后续所有需要A早期引用的地方虽然通常只有一个都从这个二级缓存获取保证了引用的一致性。如果A最终不需要代理getEarlyBeanReference()方法直接返回原始对象。所以三级缓存的核心价值在于将“早期引用”的生成逻辑封装成一个可延迟执行的工厂。它把“是否要创建代理”以及“如何创建代理”的决策推迟到真正有其他Bean需要注入它的那一刻从而完美适配了AOP代理的创建流程保证了在循环依赖场景下注入的引用与最终成品Bean无论是原始对象还是代理对象在类型和行为上的一致性。踩坑记录这就是为什么在我的开篇案例中给一个Service方法加上Async会引发循环依赖异常。Async也是通过BeanPostProcessor创建代理来实现的。在某些复杂的代理创建场景或代理处理器顺序问题下三级缓存的协调过程可能出现意外导致早期引用获取失败从而抛出BeanCurrentlyInCreationException。解决方法通常是调整Bean的依赖关系或者使用Lazy注解进行延迟注入打破即时的依赖索取。5. 三级缓存的边界与失效场景三级缓存机制虽然强大但它不是万能的。理解它的边界能帮助我们在设计时避免陷阱。5.1 非单例BeanPrototype三级缓存只针对单例SingletonBean。对于原型Prototype作用域的BeanSpring容器根本不会缓存它们每次getBean()都会创建一个新的实例。因此原型Bean之间的循环依赖Spring会直接抛出BeanCurrentlyInCreationException因为它无法也不应该去解决这种每次请求都产生新对象的依赖闭环。5.2 构造器注入Constructor Injection如前所述这是三级缓存机制无法解决的硬伤。因为构造器调用发生在实例化阶段的第一步此时Bean的实例尚未创建更谈不上将ObjectFactory加入三级缓存。当两个Bean都通过构造器相互依赖时Spring在启动时就会检测到并抛出异常。这是Spring官方明确声明不支持的情况。解决方法是改用Setter或字段注入或者重构设计消除循环依赖。5.3 某些特殊的BeanPostProcessor处理顺序Spring允许我们自定义BeanPostProcessor并且可以通过实现Ordered接口或使用Order注解来指定执行顺序。如果某个BeanPostProcessor在SmartInstantiationAwareBeanPostProcessor负责getEarlyBeanReference之前执行并且它试图去获取一个正在创建中的Bean的最终形态可能会遇到问题。因为此时可能连早期引用都还没生成。5.4allowCircularReferences配置在AbstractApplicationContext中有一个setAllowCircularReferences(boolean)方法默认是true。如果将其设置为falseSpring将完全禁止循环依赖三级缓存的循环依赖解决机制将被关闭。任何形式的循环依赖都会导致启动失败。这个配置在某些对代码质量要求极高、强制要求消除所有循环依赖的项目中可能会被使用。6. 从源码角度验证getSingleton方法逐行解析让我们聚焦到最核心的DefaultSingletonBeanRegistry.getSingleton(String, boolean)方法看看代码是如何实现上述逻辑的。这个方法清晰地展示了三级缓存的查询顺序和升级逻辑。// DefaultSingletonBeanRegistry.java protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 首先尝试从一级缓存成品缓存获取 Object singletonObject this.singletonObjects.get(beanName); // 如果没找到并且该Bean正在创建中说明可能存在循环依赖 if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 2. 尝试从二级缓存早期引用缓存获取 singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { // 3. 尝试从三级缓存工厂缓存获取工厂 ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 4. 执行工厂获取早期对象 singletonObject singletonFactory.getObject(); // 5. 将获取到的对象放入二级缓存并从三级缓存移除工厂 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }这段代码是三级缓存协同工作的核心算法先查一级最快路径直接返回成品。再查二级如果Bean正在创建中说明它可能是个“半成品”去二级缓存找找看有没有提前放进去的早期引用。最后查三级如果二级也没有但允许早期引用allowEarlyReference通常为true就去三级缓存找工厂。工厂生产与缓存升级找到工厂后调用getObject()生产出早期对象可能是原始对象也可能是提前创建的代理。然后将这个对象升级到二级缓存同时删除三级缓存中的工厂。这个“升级”操作确保了同一个Bean的早期引用只会通过工厂创建一次后续的索取都直接走二级缓存保证了效率与一致性。7. 设计启示与最佳实践探究Spring三级缓存机制不仅能帮助我们解决实际问题更能获得一些深刻的设计启示。1. 空间换时间与延迟决策三级缓存本质上是“空间换时间”和“延迟决策”的经典结合。通过引入额外的存储结构二级、三级缓存避免了循环依赖导致的死锁。更重要的是通过ObjectFactory将代理对象的创建决策延迟到真正被需要的那一刻优雅地解决了代理时机问题。这在软件设计中是一个常用思路当面临不确定或成本较高的操作时先提供一个轻量的“承诺”或“工厂”等到必须执行时再兑现。2. 状态迁移的清晰界定从三级缓存工厂- 二级缓存半成品- 一级缓存成品Bean的状态迁移路径非常清晰。每一级缓存都代表了Bean生命周期的不同阶段。这种明确的状态划分使得代码逻辑如getSingleton方法可以有条不紊地处理各种边界情况。在我们的业务代码设计中明确对象的状态并为之设计相应的处理逻辑同样能减少Bug。3. 对Spring使用者的建议优先使用构造器注入虽然它不能解决循环依赖但它是一种更安全的依赖注入方式能明确声明Bean的必需依赖并使Bean在构造完成后就处于完全初始化的状态。很多现代Spring实践如Spring官方指南都推荐构造器注入作为首选。警惕循环依赖尽管Spring提供了三级缓存机制来救场但循环依赖本身是代码结构上的一个“坏味道”Code Smell它通常意味着类的职责边界不清晰耦合度过高。应当将其视为重构的提示考虑使用“引入第三方”、“事件驱动”、“方法参数传递”等方式来解耦。理解Lazy注解Lazy注解是解决某些复杂循环依赖场景的利器。它告诉Spring延迟初始化Bean或者在注入时先注入一个代理等到第一次真正调用时再初始化真实对象。这相当于在依赖链中插入了一个“缓冲”打破了即时的循环。但滥用Lazy可能会掩盖设计问题并带来运行时性能开销和调试复杂性。复杂AOP场景下的测试当你的服务层大量使用Transactional,Async,Cacheable等基于AOP的注解时在涉及循环依赖的Bean上进行集成测试尤为重要以确保代理被正确创建且行为符合预期。回过头看最初那个因Async引发的异常根本原因是在那个特定场景下代理创建链与三级缓存的交互出现了预期之外的情况。最终的解决方案并不是调整缓存而是通过代码重构将AService中一个独立的方法抽离到一个新的TaskService中由这个新Service来承载Async方法从而彻底消除了AService和BService之间的直接循环依赖。这再次印证了那句话框架提供的复杂机制是我们的安全网但清晰简洁的代码设计才是最好的保障。