别再死记硬背了!Spring 三级缓存的真相,90% 的面试官都讲不清楚
别再死记硬背了Spring 三级缓存的真相90% 的面试官都讲不清楚前言兄弟们说实话搞技术这条路真是各种坑。咱们做开发的说白了就是要不断踩坑、不断成长这才是技术人的常态。很多兄弟面试必背三级缓存。背得滚瓜烂熟。但真让你写个循环依赖你就慌了。尤其是当 Bean 被 AOP 代理的时候。反射和字节码插桩怎么介入的这才是深水区。很多人以为三级缓存就是为了循环依赖。这话说对了一半。它真正的核心是为了解决“提前暴露引用”时的代理对象不一致问题。今天咱们不背八股。直接扒开 Spring 源码的皮。看看反射和字节码技术在里面到底干了什么。一、 底层原理1.1 核心机制Spring 的三级缓存其实是三个 Map。一级缓存singletonObjects。存的是完全初始化好的成品 Bean。二级缓存earlySingletonObjects。存的是早期的引用。三级缓存singletonFactories。存的是 ObjectFactory 工厂。为什么要有三级如果是普通 Bean二级缓存就够了。但如果这个 Bean 需要被 AOP 代理呢这里就涉及到了字节码插桩。Spring 会在 Bean 实例化后初始化前。通过getEarlyBeanReference方法。判断是否需要生成代理对象。如果需要就把代理工厂放进三级缓存。只有当真正需要注入的时候。才调用工厂方法生成代理对象放入二级缓存。这样保证了注入到 A 里面的 B是代理后的 B。而 AOP 切面生效的 B也是这个代理对象。避免了同一个 Bean 在容器里存在两个实例。graph TD A[Bean 实例化] -- B[放入三级缓存br/ObjectFactory] B -- C{属性填充?} C -- 是 -- D[注入依赖] D -- E{依赖是否循环?} E -- 是 -- F[从三级缓存获取工厂] F -- G[执行 getEarlyBeanReference] G -- H{需要代理?} H -- 是 -- I[生成代理对象] H -- 否 -- J[返回原始对象] I -- K[放入二级缓存] J -- K K -- L[移除三级缓存] L -- M[初始化完成] M -- N[放入一级缓存]1.2 与同类方案的对比如果不使用三级缓存只用二级缓存行不行理论上可以。但 Spring 的设计哲学是“按需代理”。我们对比一下几种方案。方案缓存层级代理对象处理内存开销适用场景Spring 三级缓存三级延迟生成代理低标准 Spring AOP二级缓存方案二级实例化即代理高简单循环依赖构造器注入无无法解决无推荐避免循环依赖二级缓存方案的问题是。只要实例化就立刻生成代理对象。哪怕这个 Bean 最后根本不会被注入给别人。这就浪费了字节码生成的开销。Spring 的三级缓存。把代理的生成时机推迟到了“注入那一刻”。这才是精髓。二、 快速上手咱们写个最简单的循环依赖 Demo。不用 Spring 框架模拟一下这个过程。这样你能看得更清楚。// 模拟 Bean 定义 class UserService { private UserDao userDao; // 设置依赖模拟注入 public void setUserDao(UserDao userDao) { this.userDao userDao; } } class UserDao { private UserService userService; // 设置依赖模拟注入 public void setUserService(UserService userService) { this.userService userService; } } // 模拟容器逻辑 public class SimpleContainer { // 一级缓存成品 private static MapString, Object singletonObjects new ConcurrentHashMap(); // 二级缓存早期引用 private static MapString, Object earlySingletonObjects new ConcurrentHashMap(); // 三级缓存工厂 private static MapString, Object singletonFactories new ConcurrentHashMap(); public static void main(String[] args) { // 1. 实例化 UserService UserService userService new UserService(); // 2. 放入三级缓存暴露工厂 singletonFactories.put(userService, () - getEarlyBeanReference(userService, userService)); // 3. 属性填充发现依赖 UserDao UserDao userDao new UserDao(); // 4. 放入三级缓存 singletonFactories.put(userDao, () - getEarlyBeanReference(userDao, userDao)); // 5. UserDao 需要注入 UserService // 此时从二级或三级缓存获取 Object earlyUserService getSingleton(userService); userDao.setUserService((UserService) earlyUserService); // 6. UserDao 初始化完成放入一级缓存 singletonObjects.put(userDao, userDao); singletonFactories.remove(userDao); // 7. UserService 继续初始化放入一级缓存 userService.setUserDao(userDao); singletonObjects.put(userService, userService); singletonFactories.remove(userService); System.out.println(循环依赖解决成功 ); } // 模拟从缓存获取 private static Object getSingleton(String beanName) { Object bean earlySingletonObjects.get(beanName); if (bean null) { // 从三级缓存拿工厂 Object factory singletonFactories.get(beanName); if (factory ! null) { // 执行工厂方法 bean ((ObjectFactory) factory).getObject(); // 放入二级缓存 earlySingletonObjects.put(beanName, bean); // 移除三级缓存 singletonFactories.remove(beanName); } } return bean; } // 模拟获取早期引用这里可以加入代理逻辑 private static Object getEarlyBeanReference(String beanName, Object bean) { // 如果这里需要 AOP就生成代理 // 目前先返回原对象 return bean; } }三、 核心 API / 深水区3.1 核心方法速查在AbstractAutowireCapableBeanFactory里。有几个方法决定了三级缓存的走向。方法名作用触发时机addSingletonFactory注册三级缓存实例化后属性填充前getSingleton获取单例每次注入前检查缓存getEarlyBeanReference获取早期引用三级缓存工厂被调用时doCreateBean创建 Bean 主流程容器启动时3.2 生产级配置生产环境里循环依赖其实是代码坏味道。但有时候避不开。比如某些遗留系统。你需要关注earlyInit相关的配置。如果 Bean 被Async或者Transactional注解了。三级缓存的工厂方法里。会调用ProxyFactory。这时候字节码生成就会发生。如果内存紧张。要注意代理对象的生成开销。虽然它是延迟的。但如果大量 Bean 都有循环依赖。内存压力还是会很大。3.3 高级定制你可以实现InstantiationAwareBeanPostProcessor。在getEarlyBeanReference阶段介入。比如你想自定义代理逻辑。或者你想记录哪些 Bean 触发了三级缓存。这是一个很好的切入点。很多中间件框架。比如 Dubbo 的Reference。就是在这一层做文章。把远程引用包装成代理对象。提前暴露给容器。四、 实战演练我们来模拟一个真实场景。订单服务OrderService依赖 库存服务InventoryService。库存服务又依赖 订单服务。而且库存服务需要加Transactional事务代理。Service class OrderService { // 注入库存服务 Autowired private InventoryService inventoryService; public void createOrder() { // 调用库存扣减 inventoryService.reduceStock(); } } Service class InventoryService { // 注入订单服务形成循环 Autowired private OrderService orderService; Transactional public void reduceStock() { // 业务逻辑 System.out.println(扣减库存...); // 这里可能会调用订单服务的某些方法 // orderService.createOrder(); } }当 Spring 容器启动。先实例化OrderService。放入三级缓存。开始填充属性。发现需要InventoryService。实例化InventoryService。放入三级缓存。填充InventoryService属性。发现需要OrderService。去三级缓存找。拿到OrderService的工厂。执行getObject。因为InventoryService有事务注解。Spring 会检查OrderService是否也需要代理。如果不需要返回原对象。如果需要生成代理对象。这里有个细节。OrderService本身没有事务注解。所以注入到InventoryService里的。是原始的OrderService。而InventoryService自己。会被包装成事务代理对象。放入一级缓存。这样整个链路就通了。五、 避坑指南与最佳实践5.1 技巧使用Lazy在循环依赖的一方加上Lazy。注入时会返回代理对象。真正调用时才初始化。直接打破循环。重构代码把公共逻辑抽离出来。放到第三个服务里。A 和 B 都依赖 C。这是最彻底的解决方法。5.2 ⚠️ 警告构造器注入必死构造器注入无法使用三级缓存。因为实例化之前对象都不存在。没法提前暴露引用。直接用Autowired字段注入。PostConstruct风险在初始化回调里。如果调用了其他 Bean。那个 Bean 可能还没完全初始化好。尤其是涉及代理对象的时候。状态可能不一致。5.3 ✅ 推荐开启循环依赖检测Spring Boot 默认开启。不要关闭它。关闭了虽然能启动。但运行时可能会报BeanCurrentlyInCreationException。监控缓存命中率通过DefaultSingletonBeanRegistry。可以统计三级缓存的调用次数。如果次数过多。说明你的代码里循环依赖太严重了。六、 综合实战演示下面是一套精简的、闭环的测试代码。你可以直接复制到 IDE 里跑。看看三级缓存到底是怎么把代理对象“偷”出来的。import java.util.concurrent.ConcurrentHashMap; import java.util.Map; import java.util.function.Supplier; // 模拟事务代理工厂 class TransactionProxyFactory { public static Object createProxy(Object target) { // 模拟字节码生成返回一个代理对象 return new Object() { Override public String toString() { return 事务代理对象: target.getClass().getSimpleName(); } }; } } class MainApp { // 模拟 Spring 的三级缓存结构 static MapString, Object singletonObjects new ConcurrentHashMap(); static MapString, Object earlySingletonObjects new ConcurrentHashMap(); static MapString, SupplierObject singletonFactories new ConcurrentHashMap(); public static void main(String[] args) { // 模拟创建 Bean A createBean(beanA, new BeanA()); // 模拟创建 Bean B createBean(beanB, new BeanB()); System.out.println(容器启动完成); System.out.println(BeanA 实例: singletonObjects.get(beanA)); System.out.println(BeanB 实例: singletonObjects.get(beanB)); } static void createBean(String name, Object bean) { // 1. 实例化 // 2. 放入三级缓存暴露工厂 // 工厂的作用是如果需要代理就生成代理否则返回原对象 singletonFactories.put(name, () - { // 模拟 AOP 逻辑这里假设 beanB 需要代理 if (name.equals(beanB)) { return TransactionProxyFactory.createProxy(bean); } return bean; }); // 3. 属性填充 (模拟依赖注入) if (bean instanceof BeanA) { // BeanA 依赖 BeanB Object dep getSingleton(beanB); ((BeanA) bean).setBeanB((BeanB) dep); } else if (bean instanceof BeanB) { // BeanB 依赖 BeanA Object dep getSingleton(beanA); ((BeanB) bean).setBeanA((BeanA) dep); } // 4. 初始化完成放入一级缓存 singletonObjects.put(name, bean); // 5. 清理缓存 singletonFactories.remove(name); earlySingletonObjects.remove(name); } // 核心获取单例处理三级缓存流转 static Object getSingleton(String name) { // 先查一级 Object bean singletonObjects.get(name); if (bean ! null) return bean; // 再查二级