什么是 Spring 的三级缓存?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 循环依赖理解:面试官不仅仅想知道三个缓存分别叫什么,更想知道你是否理解 Spring 为什么要搞三级缓存,以及它是怎么解决循环依赖这个难题的。

  2. Bean 生命周期掌握:三级缓存的运作贯穿了 Bean 创建的整个流程,能讲清楚它,说明你对 Spring Bean 的实例化、属性注入、初始化这一条线是清楚的。

  3. 源码级深度:能说出 getSingleton()addSingletonFactory() 这些关键方法的调用时机,面试官会认为你真的读过源码,而不是只看过博客。

核心答案

先说结论:Spring 的三级缓存是 DefaultSingletonBeanRegistry 中的三个 Map,用于解决 单例 Bean 的循环依赖 问题。

级别 缓存名 类型 存的是什么 什么时候放进去
一级 singletonObjects ConcurrentHashMap 完全初始化好 的 Bean 实例 Bean 创建完成,走完全部生命周期后
二级 earlySingletonObjects HashMap 提前暴露 的 Bean 引用(可能是代理对象) 发生循环依赖时,从三级缓存拿到后放入
三级 singletonFactories HashMap Bean 的 ObjectFactory(对象工厂) Bean 实例化后、属性注入前

一句话:一级存成品,二级存半成品,三级存的是生产半成品的工厂

深度解析

一、什么是循环依赖?

先搞清楚问题是什么,再看怎么解决。

@Component
public class A {
    @Autowired
    private B b;  // A 依赖 B
}

@Component
public class B {
    @Autowired
    private A a;  // B 又依赖 A
}

A 创建 → 发现需要 B → 去 create B → 发现需要 A → 去 create A → 又需要 B……死循环了。

Spring 怎么破?核心思路就四个字:提前暴露。A 创建好实例但还没注入属性的时候,先把 A 的引用 "提前暴露" 出去,这样 B 拿到 A 的早期引用后就能完成创建,然后 A 再回头把 B 注入进来,闭环了。

二、三级缓存解决循环依赖的完整流程

拿上面 A 和 B 互相依赖的例子,走一遍完整流程:

上面这个流程是三级缓存的核心运作机制,分六个步骤:

  • 步骤 1:Spring 实例化 A(只是 new 出来,属性还没注入),然后立刻把 A 的 ObjectFactory 放进三级缓存。这一步是关键——在属性注入之前就提前暴露了

  • 步骤 2:A 开始属性注入,发现需要 B,于是去获取 B,触发了 B 的创建流程。

  • 步骤 3:B 也走同样的流程——先实例化,再把 B 的 ObjectFactory 放进三级缓存。

  • 步骤 4:B 开始属性注入,发现需要 A。这时候去缓存里找 A,一级没有、二级没有、三级有!执行 ObjectFactory.getObject() 拿到 A 的早期引用,然后把 A 从三级缓存移到二级缓存。这里就是循环依赖被打破的地方——B 拿到了 A 的引用(虽然 A 还没完全初始化),不再死循环了。

  • 步骤 5:B 完成初始化,放入一级缓存,同时清理二级和三级缓存。

  • 步骤 6:回到 A,把 B 注入进来,A 也完成初始化,放入一级缓存。

最终,两个 Bean 都安安稳稳地待在一级缓存里,二级和三级缓存被清空。

三、为什么必须是三级?两级不行吗?

这个问题面试官特别爱追问,很多人卡在这里。

两级缓存在 没有 AOP 的情况下确实够用。但一旦涉及 AOP,就不行了。

原因在于:AOP 代理对象的创建应该在 Bean 初始化完成之后(即在 AbstractAutoProxyCreator 这个 BeanPostProcessor 中完成)。如果没有循环依赖,一切都是正常的——先创建原始对象,初始化阶段再包装成代理对象。

但如果有循环依赖呢?A 还没初始化完,B 就要引用 A 了。这时候 B 拿到的应该是 A 的代理对象还是原始对象?如果 B 拿到的是原始对象,而 A 后续被代理了,那 B 持有的就是一个 "过期" 的引用。

三级缓存存的不是对象本身,而是 ObjectFactory——一个 对象工厂。只有真正发生循环依赖的时候,才会调用工厂方法 getObject() 来决定返回原始对象还是提前生成代理对象。

// AbstractAutowiredCapableBeanFactory 源码(简化)
// 三级缓存放的是 Lambda 表达式
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

getEarlyBeanReference() 内部会检查是否有 AOP 代理需求,如果有,就提前创建代理对象返回。这样 B 拿到的就是 A 的代理引用,和最终 A 放入一级缓存的是同一个对象。

如果只有两级缓存:你必须在 A 实例化后就立刻决定是否创建代理对象——不管有没有循环依赖,都要提前创建。这违反了 Spring 的设计原则:代理应该在初始化阶段创建,而不是实例化阶段。三级缓存通过 ObjectFactory 延迟了这个决策,只在真正需要的时候才提前创建代理。

四、源码验证

核心代码都在 DefaultSingletonBeanRegistry 中:

public class DefaultSingletonBeanRegistry {

    /** 一级缓存:完整的单例 Bean */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

    /** 二级缓存:早期暴露的 Bean 引用 */
    private final Map<String, Object> earlySingletonObjects = new HashMap<>();

    /** 三级缓存:Bean 的 ObjectFactory */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

    /** 正在创建中的 Bean 名称集合 */
    private final Set<String> singletonsCurrentlyInCreation =
            Collections.newSetFromMap(new ConcurrentHashMap<>());

    // 获取单例 Bean(按顺序查三级缓存)
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 1. 先查一级缓存
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // 2. 再查二级缓存
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 3. 最后查三级缓存
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject(); // 调用工厂方法
                    // 从三级升到二级
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
        return singletonObject;
    }
}

查找顺序就是 一级 → 二级 → 三级,代码写得非常直白。

五、哪些循环依赖 Spring 解决不了?

三级缓存不是万能的,有些场景它确实搞不定:

场景 能否解决 原因
setter 注入 / 字段注入的循环依赖 ✅ 能 实例化后提前暴露引用
构造器注入的循环依赖 ❌ 不能 实例都还没创建出来,没法提前暴露
prototype 作用域的循环依赖 ❌ 不能 原型 Bean 不走缓存,每次都新建
@Async 标注的 Bean 循环依赖 ❌ 可能报错 @Async 的代理在初始化后期才创建,和三级缓存的早期代理可能冲突

其中构造器注入的问题,可以通过 @Lazy 来解决:

@Component
public class A {
    private final B b;

    @Autowired
    public A(@Lazy B b) {  // 加上 @Lazy,Spring 注入的是代理对象
        this.b = b;
    }
}

@Lazy 让 Spring 注入一个 B 的懒加载代理,等真正调用 B 的方法时才去创建 B,巧妙绕过了循环依赖。

面试高频追问

  1. 为什么三级缓存用的是 ObjectFactory 而不是直接存对象?

    • 为了延迟决策。只有在真正发生循环依赖时,才调用 getObject() 决定返回原始对象还是代理对象。如果没有循环依赖,这个工厂根本不会被调用,代理对象的创建就留给正常的初始化阶段,保证了生命周期流程的完整性。
  2. 二级缓存的作用是什么?能不能去掉?

    • 二级缓存保证在多次循环依赖的场景下,始终返回同一个早期引用。假设 A 被 B 和 C 同时依赖,B 先触发三级缓存拿到 A 的早期引用并升级到二级,之后 C 再来拿时直接从二级取就行,不用重复调用工厂方法。如果去掉二级,每次都要执行工厂逻辑,可能产生不一致的代理对象。
  3. Spring Boot 2.6 之后默认禁止循环依赖了,你怎么看?

    • 对,spring.main.allow-circular-references 默认改成 false 了。Spring 官方的态度很明确:循环依赖本身就是设计问题,应该从架构层面避免。三级缓存是兜底方案,不应该被当作正常特性来用。

常见面试变体

  • "Spring 是怎么解决循环依赖的?"
  • "为什么要用三级缓存?两级不行吗?"
  • "构造器注入的循环依赖能解决吗?怎么处理?"
  • "@Lazy 是怎么解决循环依赖的?"

记忆口诀

"一成品、二半成品、三工厂;先实例再暴露,三级升二级,最后全升一。"

总结

三级缓存是 Spring 为了解决单例 Bean 的 setter 循环依赖而设计的机制。一级存成品,二级存半成品引用,三级存生产半成品的工厂。核心流程是:Bean 实例化后立即把 ObjectFactory 放入三级缓存,发生循环依赖时调用工厂拿到早期引用并升级到二级,Bean 完全初始化后进入一级缓存。面试中重点讲清楚两件事:一是完整流程(六步走),二是为什么三级缓存不能省(和 AOP 代理有关)。