
别再死记硬背三级缓存了反射与字节码插桩下的注入真相前言凌晨三点办公室的灯光惨白。我盯着屏幕上的BeanCurrentlyInCreationException心里一阵发凉。这是生产环境上线前的最后一次回归测试。一个看似简单的循环依赖居然把整个上下文给搞崩了。同事说“加个Lazy不就行了”我加了没用。问题出在哪是因为我们只背了“三级缓存解决循环依赖”的八股文却没搞懂底层反射和字节码增强到底是怎么在这三个缓存里“打架”的。今天咱们不背源码就聊聊这背后的“猫腻”。一、底层原理1.1 核心机制Spring 的三级缓存说白了就是为了解决“我还没生完孩子你就急着抱走”的问题。这里的“孩子”就是 Bean 的实例。这里的“抱走”就是属性注入。咱们把这三个缓存想象成一个“半成品仓库”。一级缓存singletonObjects成品区。完全初始化好的 Bean随时能用。二级缓存earlySingletonObjects半成品区。实例化了但属性还没填AOP 代理可能也没做。三级缓存singletonFactories图纸区。存的是一个工厂对象ObjectFactory只有被调用的时候才知道到底生成啥。为什么非要搞个三级缓存直接二级不行吗这就涉及到了“字节码插桩”和“代理对象”的坑。如果只有二级缓存实例化后直接放进去。一旦这个 Bean 需要被 AOP 代理比如加了Transactional。那么注入到其他 Bean 里的就是原始对象而不是代理对象。这就导致事务失效或者切面不生效。三级缓存的核心在于那个ObjectFactory。它允许 Spring 在真正需要注入的那一刻动态决定是返回原始对象还是返回一个代理对象。咱们画个图看看这个流程是怎么跑通的。sequenceDiagram participant A as BeanA (用户服务) participant B as BeanB (订单服务) participant Cache as 三级缓存仓库 participant Spring as Spring 容器 Note over A, Spring: 1. 实例化 BeanA Spring-Cache: 放入三级缓存 (工厂对象) Note over A, Spring: 2. 属性注入 BeanA Spring-B: 发现依赖 BeanB Note over A, Spring: 3. 实例化 BeanB Spring-Cache: 放入三级缓存 (工厂对象) Note over A, Spring: 4. 属性注入 BeanB Spring-A: 发现依赖 BeanA Spring-Cache: 从三级缓存获取工厂 Cache--Spring: 执行工厂方法 (可能生成代理) Spring-Cache: 移入二级缓存 Spring-B: 注入早期引用 Note over A, Spring: 5. BeanB 初始化完成 Spring-Cache: 移入一级缓存 Spring-A: 注入 BeanB Note over A, Spring: 6. BeanA 初始化完成 Spring-Cache: 移入一级缓存设计优势非常明显。它把“对象实例化”和“对象增强代理”这两个动作解耦了。只有在从三级缓存取出对象时才执行getObject()。这时候BeanPostProcessor有机会介入生成代理对象。这就保证了注入进来的永远是最终的那个“增强版”Bean。1.2 与同类方案的对比为了让大家更直观咱们对比一下几种常见的依赖注入处理方式。方案缓存层级是否支持 AOP 代理适用场景风险点构造器注入无缓存不支持强制依赖循环依赖直接报错无法解环普通设值注入二级缓存部分支持简单循环依赖若涉及代理早期引用可能是裸对象Spring 三级缓存三级缓存完全支持复杂循环依赖 AOP逻辑复杂调试困难性能微损Lazy 注解代理延迟加载支持打破循环依赖本质是延迟初始化非真正解决循环可以看到只有 Spring 的三级缓存机制能同时兼顾“循环依赖”和AOP 代理”。这也是为什么它成为了 Spring 容器的“镇厂之宝”。二、快速上手光说不练假把式。咱们写个最简 Demo复现一下循环依赖看看三级缓存是怎么救场的。这里我们模拟两个服务UserService依赖OrderServiceOrderService又依赖UserService。// 用户服务类 public class UserService { // 订单服务中文变量名方便理解 private OrderService orderService; // 无参构造器Spring 实例化的前提 public UserService() { System.out.println(【UserService】正在实例化...); } // 设值方法模拟属性注入 public void setOrderService(OrderService orderService) { this.orderService orderService; System.out.println(【UserService】注入订单服务完成); } public void getUserInfo() { System.out.println(【UserService】获取用户信息调用订单服务...); orderService.createOrder(); } } // 订单服务类 public class OrderService { // 用户服务 private UserService userService; public OrderService() { System.out.println(【OrderService】正在实例化...); } public void setUserService(UserService userService) { this.userService userService; System.out.println(【OrderService】注入用户服务完成); } public void createOrder() { System.out.println(【OrderService】创建订单调用用户服务...); userService.getUserInfo(); } }现在咱们写个简单的容器模拟器看看三级缓存怎么工作。import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; public class SimpleIocContainer { // 一级缓存成品 private MapString, Object singletonObjects new HashMap(); // 二级缓存早期引用半成品 private MapString, Object earlySingletonObjects new HashMap(); // 三级缓存工厂对象图纸 private MapString, SupplierObject singletonFactories new HashMap(); public Object getBean(String beanName, Class? beanClass) { // 1. 先从一级缓存找成品直接拿 Object bean singletonObjects.get(beanName); if (bean ! null) { return bean; } // 2. 从二级缓存找半成品也能用 bean earlySingletonObjects.get(beanName); if (bean ! null) { System.out.println(【容器】从二级缓存获取早期引用 beanName); return bean; } // 3. 从三级缓存找触发工厂方法 SupplierObject factory singletonFactories.get(beanName); if (factory ! null) { System.out.println(【容器】从三级缓存获取工厂正在生成对象 beanName); // 移除三级缓存防止重复创建 singletonFactories.remove(beanName); // 执行工厂方法获取早期引用 bean factory.get(); // 放入二级缓存 earlySingletonObjects.put(beanName, bean); return bean; } // 4. 缓存都没找到开始创建 System.out.println(【容器】开始创建新 Bean beanName); // 实例化对象 (模拟反射 new 对象) try { bean beanClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(实例化失败, e); } // 提前暴露引用到三级缓存 // 这里的关键是存的是一个 Lambda 表达式延迟执行 singletonFactories.put(beanName, () - { // 这里可以加入 AOP 代理逻辑比如返回 proxyBean return bean; }); // 模拟属性注入过程 try { // 这里简化处理实际会遍历字段进行注入 injectProperties(beanName, bean); } catch (Exception e) { // 创建失败清理缓存 singletonFactories.remove(beanName); throw new RuntimeException(注入失败, e); } // 初始化完成放入一级缓存 singletonObjects.put(beanName, bean); // 清理二级和三级缓存中的残留 earlySingletonObjects.remove(beanName); singletonFactories.remove(beanName); return bean; } private void injectProperties(String beanName, Object bean) throws Exception { // 模拟反射注入逻辑 if (bean instanceof UserService) { // UserService 依赖 OrderService Object orderService getBean(orderService, OrderService.class); // 通过反射设值 bean.getClass().getMethod(setOrderService, OrderService.class).invoke(bean, orderService); } else if (bean instanceof OrderService) { // OrderService 依赖 UserService Object userService getBean(userService, UserService.class); bean.getClass().getMethod(setUserService, UserService.class).invoke(bean, userService); } } public static void main(String[] args) { SimpleIocContainer container new SimpleIocContainer(); // 获取用户服务触发连锁反应 container.getBean(userService, UserService.class); System.out.println( 容器启动完成 ); } }运行结果会显示容器并没有报错而是顺利完成了互相注入。这就是三级缓存的魔力。三、核心 API / 深水区3.1 核心方法速查在 Spring 源码AbstractAutowireCapableBeanFactory中有几个方法是你必须盯着的。方法名作用关键参数getSingleton(String beanName)获取单例 Bean核心入口allowEarlyReference(允许早期引用)addSingletonFactory(String beanName, ObjectFactory? singletonFactory)添加三级缓存传入工厂对象getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean)获取早期引用决定是否生成代理3.2 生产级配置在生产环境中三级缓存虽然强大但也带来了一些性能开销。主要是每次获取 Bean 都要多层查找。对于高并发场景建议关注以下几点避免不必要的循环依赖能重构就重构别依赖容器的容错。监控 Bean 创建时间如果某个 Bean 创建特别慢可能是循环依赖导致的递归查找。异常处理确保ObjectFactory内部不要抛出 unchecked 异常否则会导致容器启动回滚。3.3 高级定制如果你想自定义三级缓存的行为比如想在所有 Bean 创建时都加个日志。可以继承InstantiationAwareBeanPostProcessor。在getEarlyBeanReference方法里做文章。public class CustomBeanPostProcessor implements InstantiationAwareBeanPostProcessor { Override public Object getEarlyBeanReference(Object bean, String beanName) { // 在这里可以对早期引用进行增强 // 比如记录日志或者修改代理逻辑 System.out.println(【拦截器】正在处理早期引用 beanName); return bean; } }四、实战演练咱们来一个真实的业务场景。假设我们有一个PaymentService支付服务和LogService日志服务。PaymentService需要记录日志LogService需要记录支付状态。这就形成了循环依赖。而且PaymentService必须被Transactional代理保证数据一致性。Service public class PaymentService { Autowired private LogService logService; Transactional public void pay() { System.out.println(正在支付...); logService.log(支付成功); } } Service public class LogService { Autowired private PaymentService paymentService; public void log(String msg) { System.out.println(日志记录 msg); // 模拟日志服务也需要调用支付服务查询状态 // paymentService.queryStatus(); } }在这个场景下如果PaymentService没有三级缓存支持。LogService注入到的PaymentService就是原始对象。调用pay()方法时事务注解就失效了。有了三级缓存getEarlyBeanReference会被触发。Spring 发现PaymentService有Transactional。它会在这个阶段就生成代理对象放入二级缓存。LogService拿到的就是带事务的代理对象。完美。五、避坑指南与最佳实践虽然三级缓存很强大但也不是万能药。这里总结几个我踩过的坑。技巧如果实在解不开循环依赖试试Lazy。它会在注入时返回一个代理对象真正调用方法时才去容器里找 Bean。这相当于把“启动时的依赖”变成了“运行时的依赖”。⚠️警告构造器注入无法使用三级缓存。因为构造器注入时对象还没实例化连“图纸”都画不出来。所以循环依赖的 Bean尽量用Autowired设值注入。✅推荐保持 Bean 的单一职责。如果一个 Bean 依赖了半个容器那设计本身就有问题。重构代码提取接口往往比研究缓存机制更治本。六、综合实战演示最后咱们把上面提到的点整合成一个完整的测试类。这个类模拟了带 AOP 代理的循环依赖场景。import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; // 模拟带代理的 Bean class ProxyService { private String name; public ProxyService(String name) { this.name name; } public void doSomething() { System.out.println(name 执行操作); } } public class AdvancedIocDemo { // 缓存区 private MapString, Object singletonObjects new HashMap(); private MapString, Object earlySingletonObjects new HashMap(); private MapString, SupplierObject singletonFactories new HashMap(); public Object getBean(String beanName, SupplierObject creationCallback) { // 1. 查一级 Object bean singletonObjects.get(beanName); if (bean ! null) return bean; // 2. 查二级 bean earlySingletonObjects.get(beanName); if (bean ! null) return bean; // 3. 查三级 SupplierObject factory singletonFactories.get(beanName); if (factory ! null) { singletonFactories.remove(beanName); bean factory.get(); earlySingletonObjects.put(beanName, bean); return bean; } // 4. 创建 System.out.println( 创建 Bean: beanName); // 实例化 Object instance creationCallback.get(); // 5. 暴露三级缓存 (关键步骤) // 注意这里模拟了 AOP 代理的逻辑 // 实际 Spring 会在这里判断是否需要代理 singletonFactories.put(beanName, () - { // 模拟生成代理对象 System.out.println( [AOP] 正在为 beanName 生成代理...); return instance; }); // 6. 属性注入 (模拟) try { // 这里简化实际会反射设值 // 假设 BeanA 依赖 BeanB if (beanA.equals(beanName)) { Object beanB getBean(beanB, () - new ProxyService(BeanB)); // 模拟注入 System.out.println( [注入] BeanA 注入了 BeanB); } } catch (Exception e) { singletonFactories.remove(beanName); throw new RuntimeException(e); } // 7. 放入一级缓存 singletonObjects.put(beanName, instance); earlySingletonObjects.remove(beanName); System.out.println( Bean: beanName 初始化完成); return instance; } public static void main(String[] args) { AdvancedIocDemo demo new AdvancedIocDemo(); // 启动容器获取 BeanA // BeanA 依赖 BeanBBeanB 依赖 BeanA demo.getBean(beanA, () - new ProxyService(BeanA)); System.out.println(\n 最终结果 ); System.out.println(一级缓存大小: demo.singletonObjects.size()); } }运行这段代码你会看到清晰的日志输出。三级缓存的“生成 - 暴露 - 回收”过程一览无余。七、总结Spring 的三级缓存本质上是用“空间换时间”和“延迟加载”的智慧。它通过ObjectFactory把对象生成的控制权推迟到了注入的那一刻。这才解决了“循环依赖”和AOP 代理”这两个看似矛盾的需求。记住技术是为业务服务的。理解了原理才能在遇到 Bug 时知道该往哪个方向修。别再死记硬背了去源码里跑一遍你会有更深的体会。