
1. 项目概述为什么我们要深挖三级缓存如果你在面试中被问到“Spring是如何解决循环依赖的”回答“三级缓存”大概率能过关。但如果你被追问“为什么是三级缓存两级不行吗一级不行吗第二级缓存具体解决了什么问题”还能从容应对的才是真正吃透了Spring容器核心设计的人。这个机制远不止是面试八股文它是理解Spring Bean生命周期、AOP代理创建乃至框架设计哲学的一把钥匙。我最初接触Spring源码时对三级缓存也是一知半解直到在线上环境遇到一个诡异的Bean创建失败问题日志指向AbstractAutowireCapableBeanFactory的doCreateBean方法才被迫一头扎进去。那次排查让我意识到仅仅知道“三级缓存”这个名词是远远不够的。它背后是Spring在灵活性支持AOP、性能避免重复创建和正确性解决循环依赖之间做出的精妙权衡。今天我们就抛开那些笼统的概念从源码行间出发结合实际的调试案例把三级缓存里每一级的作用、交互时机以及设计者的取舍逻辑彻底掰开揉碎讲清楚。无论你是想提升排查问题的能力还是为深入理解Spring框架打下坚实基础这次探究都会让你有实实在在的收获。2. 循环依赖的本质与Spring的解决思路拆解2.1 什么是循环依赖它真的无解吗循环依赖简单说就是“你中有我我中有你”。比如两个BeanAService依赖BServiceBService反过来也依赖AService。在传统的、严格的“构造-设置”流程中这似乎是个死结创建A需要先有B创建B又需要先有A。但从逻辑上看循环依赖并非无解。关键在于我们需要的并不是一个“完全初始化好的、完美的”Bean而是一个“引用”。只要我能先拿到一个对象的引用即使它内部的属性还没填完我就可以先把引用给你让你继续你的初始化流程等我自己初始化完成后再把属性补上。这就像盖房子两个房间需要共用一面墙我们不必等两个房间都完全装修好再砌墙而是先把墙的框架对象引用立起来让两个房间都能基于这个框架继续施工最后再统一粉刷墙面属性注入。Spring解决循环依赖的核心思想正是“提前暴露引用”。但问题来了暴露一个什么样的引用是原始对象还是经过AOP包装后的代理对象暴露的时机在哪里如何保证在并发环境下所有线程拿到的是同一个、正确的引用三级缓存机制就是为了系统性地回答这些问题而诞生的。2.2 三级缓存全景图每一级都是精心的设计在深入代码前我们先建立全局认知。Spring的三级缓存定义在DefaultSingletonBeanRegistry类中是三个Map/** 一级缓存存放完整的单例Bean */ private final MapString, Object singletonObjects new ConcurrentHashMap(256); /** 二级缓存存放早期的Bean尚未填充属性用于解决循环依赖 */ private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); /** 三级缓存存放ObjectFactory用于生成早期引用可能被AOP增强 */ private final MapString, ObjectFactory? singletonFactories new HashMap(16);一级缓存singletonObjects俗称“成品库”。这里存放的是已经完全初始化好的Bean经历了实例化、属性填充、初始化InitializingBean、init-method等所有生命周期步骤。从这取走的Bean是立即可用的。二级缓存earlySingletonObjects俗称“半成品库”。这里存放的是已经实例化但尚未进行属性填充和初始化的“早期Bean”对象。它的核心作用是避免重复执行ObjectFactory。当有循环依赖发生时其他Bean需要依赖当前Bean的引用时会尝试从二级缓存获取。三级缓存singletonFactories这是最精妙的一级。它存放的不是Bean对象本身而是一个ObjectFactory对象工厂。这个工厂的职责是当被调用时能够返回当前Bean的“早期引用”。这个引用可能是原始对象但如果该Bean需要被AOP代理那么这个工厂就会返回代理对象。这是支持AOP的关键。关键理解很多人会疑惑有了三级缓存工厂能生成早期引用为什么还需要二级缓存直接让所有需要早期引用的地方都调用三级缓存里的工厂不就行了这里涉及一个至关重要的点性能与一致性。ObjectFactory的执行特别是生成代理可能涉及复杂的逻辑如匹配切面、创建代理。如果每次依赖注入都调用一次工厂在复杂的循环依赖链中会导致同一个Bean的代理被创建多次这不仅浪费性能更严重的是可能破坏单例语义导致最终拿到的是不同的代理对象。二级缓存的存在就是为了缓存第一次从三级缓存工厂获取到的结果无论是原始对象还是代理对象确保后续所有依赖注入获取到的是同一个实例。3. 核心流程源码级解析Bean是如何“诞生”的让我们跟随一个普通Bean的创建流程看在循环依赖的“压力测试”下三级缓存是如何协同工作的。核心入口在AbstractBeanFactory.doGetBean而创建单例Bean的主战场在DefaultSingletonBeanRegistry.getSingleton(String, ObjectFactory)方法。3.1 第一幕尝试获取与三级缓存的登场当一个Bean例如AService被请求时Spring首先调用getSingleton(beanName)。protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步从一级缓存成品库查找 Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { // 如果一级缓存没有且当前Bean正在创建中说明出现了循环依赖... synchronized (this.singletonObjects) { // 第二步从二级缓存半成品库查找 singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { // 如果二级缓存也没有且允许早期引用默认true... // 第三步从三级缓存获取ObjectFactory ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 调用工厂的getObject()这是可能生成代理的地方。 singletonObject singletonFactory.getObject(); // 将结果放入二级缓存并清空三级缓存对应的工厂 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }流程解读先查一级缓存有则直接返回完美Bean。再查二级缓存没有完美的看看有没有“半成品”。有则返回避免重复创建。最后动用三级缓存如果连半成品都没有但发现这个Bean正在创建中isSingletonCurrentlyInCreation为true说明我们撞上了循环依赖。此时就会取出三级缓存中的ObjectFactory调用它来生成一个早期引用。这个调用是触发AOP代理创建的关键时机之一。生成后将其放入二级缓存并从三级缓存移除该工厂。3.2 第二幕Bean的创建与三级缓存的填充如果三级缓存都没找到说明这个Bean是第一次被创建。流程会走到createBean进而到doCreateBean。在doCreateBean方法中有一个决定性的操作protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException { // 1. 实例化通过反射调用构造函数创建原始对象 instanceWrapper BeanWrapper instanceWrapper createBeanInstance(beanName, mbd, args); Object bean instanceWrapper.getWrappedInstance(); // 2. 【关键步骤】判断是否允许早期暴露 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 允许早期暴露向三级缓存添加一个ObjectFactory addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); } // 3. 属性填充Populate Bean这里会解析Autowired、Resource等递归触发依赖Bean的获取 populateBean(beanName, mbd, instanceWrapper); // 4. 初始化Initialize Bean调用InitializingBean.afterPropertiesSet和init-method exposedObject initializeBean(beanName, exposedObject, mbd); // ... 后续处理 return exposedObject; }核心在于addSingletonFactory这一行。在Bean刚刚实例化完成还是一个“空壳”属性全是默认值即将进行属性填充之前Spring将一个ObjectFactory丢进了三级缓存。这个工厂的getObject()方法实际调用的是getEarlyBeanReference(beanName, mbd, bean)。我们看看getEarlyBeanReference做了什么protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp (SmartInstantiationAwareBeanPostProcessor) bp; // 调用后处理器的getEarlyBeanReference方法 exposedObject ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }这里是AOP登场的舞台。对于Spring AOP其核心后处理器AbstractAutoProxyCreator就是一个SmartInstantiationAwareBeanPostProcessor。它的getEarlyBeanReference方法会判断当前Bean是否需要被代理根据切面定义如果需要它不会立即创建代理而是先将原始Bean包装在一个“早期代理引用”的持有器中或者在一些策略下直接返回代理对象。这就保证了当循环依赖发生时其他Bean注入的将是一个最终会被增强的代理对象的引用而不是原始对象。这是Spring能无缝支持循环依赖AOP的基石。实操心得调试时可以在addSingletonFactory和getEarlyBeanReference方法打断点。你会清晰地看到在AService属性填充需要BService之前AService的工厂就已经进了三级缓存。当后续流程去创建BService而BService又需要注入AService时就会触发上面getSingleton中的流程从三级缓存拿到这个工厂从而获得AService的早期引用可能是代理。3.3 第三幕循环依赖的解决与升级到一级缓存我们模拟AService和BService循环依赖的经典场景开始创建AService- 实例化AService对象 - 向三级缓存添加AService的ObjectFactory。开始为AService填充属性 - 发现需要BService- 触发getBean(“bService”)。开始创建BService- 实例化BService对象 - 向三级缓存添加BService的ObjectFactory。开始为BService填充属性 - 发现需要AService- 触发getBean(“aService”)。此时getSingleton(“aService”)发现AService正在创建中isSingletonCurrentlyInCreation为true且一级缓存没有。于是它从三级缓存拿到AService的ObjectFactory并调用获得了AService的早期引用假设是代理对象。将这个早期引用放入二级缓存并从三级缓存移除AService的工厂。BService成功获得AService的引用完成属性填充和初始化最终成为一个完整的Bean被放入一级缓存。流程回溯到AService的属性填充步骤此时它需要的BService已经在一级缓存了直接注入。AService继续完成自己的属性填充和初始化。最后在AService初始化完成后Spring会调用addSingleton(beanName, singletonObject)方法将AService放入一级缓存并清理二级和三级缓存中关于AService的所有记录。至此循环依赖完美解决两个Bean都是完整的代理对象如果需要且所有缓存状态被正确清理。4. 深度追问设计抉择与边界情况4.1 为什么不能只有两级缓存假设我们去掉二级缓存只有一级成品和三级工厂。在循环依赖场景下AService创建工厂入三级缓存。BService创建需要AService从三级缓存调用工厂得到代理对象proxyA注入给BService。之后如果又有另一个BeanCService也依赖AService且此时AService还未完成初始化未进入一级缓存。那么CService同样会去三级缓存调用工厂。问题来了工厂被调用了两次。如果getEarlyBeanReference逻辑每次都生成一个新的代理对象那么BService和CService注入的将是两个不同的AService代理严重破坏了单例模式。即使AbstractAutoProxyCreator做了缓存重复执行工厂方法也可能带来不必要的性能开销和状态不一致的风险。二级缓存充当了“早期引用缓存”的角色确保在Bean完全初始化前所有需要它早期引用的地方拿到的是同一个对象。4.2 为什么不能只有一级缓存如果只有一级缓存根本无法解决循环依赖。因为只有完全初始化好的Bean才能放入一级缓存。在循环依赖中两个Bean都无法完成初始化因为都在等对方先成为“成品”从而陷入死锁。4.3 构造器循环依赖为何无法解决Spring官方文档明确说明构造器注入的循环依赖无法解决。原因很简单三级缓存发挥作用的前提是对象已经实例化。构造器注入发生在实例化阶段即调用new AService(bService)时此时AService对象本身都还没创建出来更谈不上放入三级缓存就需要BService作为构造参数。而为了创建BService又需要AService作为构造参数这就成了一个“先有鸡还是先有蛋”的真正死结。Spring会通过BeanCurrentlyInCreationException提前发现并抛出异常而不是让你陷入运行时死循环。避坑指南这是实际开发中最常见的循环依赖问题来源。建议优先使用Setter注入或字段注入Autowired。如果非要用构造器注入并且确实存在循环依赖就需要考虑重构设计打破循环例如引入第三个Bean或者使用Lazy注解进行延迟注入。Lazy注解的原理是它不会在注入点立即去获取目标Bean而是注入一个代理对象当第一次调用该代理对象的方法时才会触发真实Bean的创建。这相当于将依赖的获取时机从Bean创建阶段推迟到了方法调用阶段从而绕开了构造器注入的死锁。4.4 原型Prototype作用域的Bean为何不支持循环依赖对于scope”prototype”的BeanSpring容器不负责其完整生命周期的管理每次请求都会创建一个新的实例。因此Spring根本没有为原型Bean维护任何缓存一级、二级、三级都没有。当原型Bean A依赖原型Bean B而B又依赖A时在创建A的过程中需要B会触发创建B创建B的过程中又需要A这会再次触发创建A的新实例……如此递归下去直到栈溢出。Spring无法也不应该去解决这种场景它会直接抛出BeanCurrentlyInCreationException。5. 实战调试与常见问题排查理解了原理我们来看看如何运用这些知识解决实际问题。5.1 调试技巧观察缓存状态的变化最直观的学习方式就是调试。在IDEA中对DefaultSingletonBeanRegistry类中的三个Map设置条件断点singletonObjects一级缓存earlySingletonObjects二级缓存singletonFactories三级缓存在doCreateBean方法的addSingletonFactory和getSingleton方法的allowEarlyReference逻辑处打上断点。然后启动一个包含循环依赖的简单Spring应用。通过观察栈帧和这三个Map内容的变化你可以像看电影一样清晰看到Bean的引用是如何在三级缓存中“流动”的。5.2 常见异常与排查思路1. BeanCurrentlyInCreationException这是最常见的与循环依赖相关的异常。现象应用启动失败报错信息明确提示BeanCurrentlyInCreationException。可能原因1构造器循环依赖。检查报错Bean的依赖关系看是否使用了构造器注入并形成了环。解决方案改为Setter/字段注入或使用Lazy。可能原因2原型Bean的循环依赖。检查Bean的作用域。解决方案重构设计避免原型Bean间的循环依赖或考虑改为单例。2. 注入的Bean不是代理对象AOP失效现象明明配置了Transactional或自定义切面但方法调用时切面逻辑不生效调试发现注入的对象是原始类型而非代理类型。排查这种情况通常不是三级缓存本身的问题。首先检查切面配置是否正确如EnableAspectJAutoProxy。其次注意同类方法调用在同一个Bean内部方法A调用方法B即使方法B有Transactional由于调用走的是this引用原始对象而非经过Spring代理的引用切面也会失效。这是AOP的经典问题需要通过AopContext.currentProxy()或重构代码将方法B放到另一个Bean来解决。与三级缓存的关系确保你的Bean是通过Spring容器获取的并且循环依赖能正常走通三级缓存流程。如果循环依赖因故未能解决可能导致Bean创建失败或者注入了一个状态不正确的对象。3. 在PostConstruct方法中调用依赖Bean的方法报空指针或状态不对现象在AService的PostConstruct方法中调用了BService的某个方法但BService中的某些依赖比如它依赖的AService似乎还没注入完成。分析这是由Bean初始化顺序导致的。PostConstruct在属性填充之后、初始化回调之前执行。在循环依赖场景下当AService执行PostConstruct时BService可能已经创建完成因为它先拿到了AService的早期引用并完成了初始化但BService内部持有的AService引用可能还是一个早期对象尚未执行PostConstruct的AService。因此如果BService的方法依赖于AService在PostConstruct中初始化的状态就可能出错。建议避免在PostConstruct中进行复杂的、涉及循环依赖Bean状态逻辑的调用。可以考虑将初始化逻辑移到更靠后的阶段或者使用事件监听、SmartInitializingSingleton等机制。5.3 性能考量与最佳实践三级缓存机制引入了额外的Map操作和可能的代理创建逻辑在极端复杂的Bean依赖图中会带来微小的开销。但Spring团队经过权衡认为这对于支持强大的特性循环依赖、AOP是值得的。最佳实践建议避免循环依赖尽管Spring提供了解决方案但循环依赖本质上是一种紧耦合的设计。在项目设计中应尽量通过重构提取公共父类、引入第三方服务、使用事件驱动等来避免循环依赖使架构更清晰。优先使用Setter/字段注入如果确实存在循环依赖使用Autowired进行字段注入或Setter注入避免构造器注入带来的无法解决的问题。谨慎使用LazyLazy是打破循环依赖的利器但它会掩盖设计问题并可能将启动期的问题推迟到运行时。只在确实需要时使用并清楚其影响。理解缓存作用域明确你的Bean是单例默认还是原型。原型Bean的循环依赖会直接失败。通过对Spring三级缓存机制的深度解析我们看到的不仅仅是一个解决循环依赖的技巧更是一个优秀框架在面临复杂问题时的设计哲学通过分层、缓存和延迟决策如通过ObjectFactory延迟代理创建来平衡功能、性能和一致性。下次当你使用Autowired时或许会对背后这套精密的协作机制多一份敬意也能在遇到相关问题时更快地直击要害。