当前位置: 首页 > news >正文

别再死记硬背三级缓存了!反射与字节码插桩下的注入真相

别再死记硬背三级缓存了!反射与字节码插桩下的注入真相

前言

凌晨三点,办公室的灯光惨白。

我盯着屏幕上的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 Map<String, Object> singletonObjects = new HashMap<>(); // 二级缓存:早期引用(半成品) private Map<String, Object> earlySingletonObjects = new HashMap<>(); // 三级缓存:工厂对象(图纸) private Map<String, Supplier<Object>> 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. 从三级缓存找,触发工厂方法 Supplier<Object> 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 都要多层查找。

对于高并发场景,建议关注以下几点:

  1. 避免不必要的循环依赖:能重构就重构,别依赖容器的容错。
  2. 监控 Bean 创建时间:如果某个 Bean 创建特别慢,可能是循环依赖导致的递归查找。
  3. 异常处理:确保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 Map<String, Object> singletonObjects = new HashMap<>(); private Map<String, Object> earlySingletonObjects = new HashMap<>(); private Map<String, Supplier<Object>> singletonFactories = new HashMap<>(); public Object getBean(String beanName, Supplier<Object> creationCallback) { // 1. 查一级 Object bean = singletonObjects.get(beanName); if (bean != null) return bean; // 2. 查二级 bean = earlySingletonObjects.get(beanName); if (bean != null) return bean; // 3. 查三级 Supplier<Object> 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 依赖 BeanB,BeanB 依赖 BeanA demo.getBean("beanA", () -> new ProxyService("BeanA")); System.out.println("\n=== 最终结果 ==="); System.out.println("一级缓存大小: " + demo.singletonObjects.size()); } }

运行这段代码,你会看到清晰的日志输出。

三级缓存的“生成 - 暴露 - 回收”过程一览无余。

七、总结

Spring 的三级缓存,本质上是用“空间换时间”和“延迟加载”的智慧。

它通过ObjectFactory把对象生成的控制权,推迟到了注入的那一刻。

这才解决了“循环依赖”和"AOP 代理”这两个看似矛盾的需求。

记住,技术是为业务服务的。

理解了原理,才能在遇到 Bug 时,知道该往哪个方向修。

别再死记硬背了,去源码里跑一遍,你会有更深的体会。

http://www.jsqmd.com/news/932340/

相关文章:

  • 3步解锁MacBook Touch Bar完整Windows功能:免费驱动终极教程
  • 从零构建Discord机器人:Python事件驱动编程与API交互实战
  • AI提示词极限赛技术
  • 2026年6月正规的宜宾小型车载泵品牌哪家靠谱厂家推荐榜,HBTS80.13.90型、HBC80.16.110型、HBT60.13.90型车载泵厂家选择指南 - 海棠依旧大
  • 终极解决方案:3步解锁MPC Video Renderer专业级HDR体验深度解析
  • 智能语音助手技术全景:从语音识别到自然语言理解的七步流程
  • 从ShuffleNet到SA-Net:轻量级注意力演进史,你的模型该升级了
  • 【Sora 2口型同步核心技术白皮书】:首次公开37ms级唇动延迟压缩算法与神经时序对齐框架
  • 避坑!用SX1276和NS_Radio库做LoRa通信,为什么你的数据会乱码或溢出?
  • Trelby:免费开源的剧本写作软件,如何让创作者专注故事本身?
  • 隐形无头浏览器:camofox-browser 使用详解(解决行为机器人检测问题)
  • 2026 广州增城区高空吊装公司实测 高效服务推荐 - 从来都是英雄出少年
  • 手机投屏电视全攻略:从无线镜像到USB-C直连,原理与实战解析
  • 基于CircuitPython与蓝牙的智能遥控船DIY:从硬件选型到代码实战
  • 深夜两点,ThreadLocal 把我们的生产环境搞崩了,复盘这 3 个救命思路
  • 解决Keil uVision许可证管理中Unknown Product错误
  • 5个PowerToys Awake实用技巧:告别电脑意外休眠,提升工作效率
  • 通过cr3读写进程内存
  • Spring Boot 2.5.4项目里,如何给Swagger 3.0和Knife4j一键加上全局Header参数(附完整代码)
  • IDEA 2023.3 创建 Spring Boot 项目,如何让 Java 8 和 Spring Boot 3.x 共存?保姆级配置指南
  • 天价域名AI.com背后:数字入口的战略价值与AGI生态未来
  • 告别裸奔:用STM32CubeMX给STM32F407ZGT6快速移植FreeRTOS内核(含串口打印任务状态)
  • KAIST 把文本、SQL、知识图谱、属性图全打通:一句话提问,跨四种知识源一起检索
  • STM32掉电检测PVD的5个常见坑与优化技巧:从电压迟滞到中断优先级设置
  • Lab 3-1
  • Arduino蓝牙控制LED:从硬件连接到APP开发的物联网入门实践
  • LaTeX子图排版避坑指南:为什么你的图总对不齐?从原理到实战
  • 三维立体重构智慧矿产透明化安防监测预警及AI预案
  • 如何快速修复Garry‘s Mod游戏问题:面向玩家的完整解决方案
  • 保姆级教程:在ROS Gazebo中为Livox Mid-360激光雷达更换真实3D模型(附Blender缩放技巧)