1. 项目概述:为什么我们要深挖三级缓存?
如果你在面试中被问到“Spring是如何解决循环依赖的?”,回答“三级缓存”大概率能过关。但如果你被追问:“为什么是三级缓存,两级不行吗?一级不行吗?第二级缓存具体解决了什么问题?”,还能从容应对的,才是真正吃透了Spring容器核心设计的人。这个机制,远不止是面试八股文,它是理解Spring Bean生命周期、AOP代理创建乃至框架设计哲学的一把钥匙。
我最初接触Spring源码时,对三级缓存也是一知半解,直到在线上环境遇到一个诡异的Bean创建失败问题,日志指向AbstractAutowireCapableBeanFactory的doCreateBean方法,才被迫一头扎进去。那次排查让我意识到,仅仅知道“三级缓存”这个名词是远远不够的。它背后是Spring在灵活性(支持AOP)、性能(避免重复创建)和正确性(解决循环依赖)之间做出的精妙权衡。今天,我们就抛开那些笼统的概念,从源码行间出发,结合实际的调试案例,把三级缓存里每一级的作用、交互时机以及设计者的取舍逻辑,彻底掰开揉碎讲清楚。无论你是想提升排查问题的能力,还是为深入理解Spring框架打下坚实基础,这次探究都会让你有实实在在的收获。
2. 循环依赖的本质与Spring的解决思路拆解
2.1 什么是循环依赖?它真的无解吗?
循环依赖,简单说就是“你中有我,我中有你”。比如两个Bean:AService依赖BService,BService反过来也依赖AService。在传统的、严格的“构造-设置”流程中,这似乎是个死结:创建A需要先有B,创建B又需要先有A。
但从逻辑上看,循环依赖并非无解。关键在于,我们需要的并不是一个“完全初始化好的、完美的”Bean,而是一个“引用”。只要我能先拿到一个对象的引用(即使它内部的属性还没填完),我就可以先把引用给你,让你继续你的初始化流程,等我自己初始化完成后再把属性补上。这就像盖房子,两个房间需要共用一面墙,我们不必等两个房间都完全装修好再砌墙,而是先把墙的框架(对象引用)立起来,让两个房间都能基于这个框架继续施工,最后再统一粉刷墙面(属性注入)。
Spring解决循环依赖的核心思想正是“提前暴露引用”。但问题来了:暴露一个什么样的引用?是原始对象,还是经过AOP包装后的代理对象?暴露的时机在哪里?如何保证在并发环境下,所有线程拿到的是同一个、正确的引用?三级缓存机制,就是为了系统性地回答这些问题而诞生的。
2.2 三级缓存全景图:每一级都是精心的设计
在深入代码前,我们先建立全局认知。Spring的三级缓存,定义在DefaultSingletonBeanRegistry类中,是三个Map:
/** 一级缓存:存放完整的单例Bean */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** 二级缓存:存放早期的Bean(尚未填充属性),用于解决循环依赖 */ private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); /** 三级缓存:存放ObjectFactory,用于生成早期引用(可能被AOP增强) */ private final Map<String, 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。- 之后,如果又有另一个Bean
CService也依赖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”的Bean,Spring容器不负责其完整生命周期的管理,每次请求都会创建一个新的实例。因此,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注入,避免构造器注入带来的无法解决的问题。 - 谨慎使用
@Lazy:@Lazy是打破循环依赖的利器,但它会掩盖设计问题,并可能将启动期的问题推迟到运行时。只在确实需要时使用,并清楚其影响。 - 理解缓存作用域:明确你的Bean是单例(默认)还是原型。原型Bean的循环依赖会直接失败。
通过对Spring三级缓存机制的深度解析,我们看到的不仅仅是一个解决循环依赖的技巧,更是一个优秀框架在面临复杂问题时的设计哲学:通过分层、缓存和延迟决策(如通过ObjectFactory延迟代理创建)来平衡功能、性能和一致性。下次当你使用@Autowired时,或许会对背后这套精密的协作机制多一份敬意,也能在遇到相关问题时,更快地直击要害。