AOP 代理对象的诞生时刻:Bean 生命周期中的“夺舍”瞬间

news2026/3/28 4:46:03
各位大佬欢迎来到 Spring 容器最神秘、最惊心动魄的现场很多人以为 AOP 是“天生”的 Bean 一出生就带着光环。大错特错不过是前人在负重前行Spring 先造出一个“纯净的肉身”原始对象在它即将“成年”初始化完成的前一秒通过BeanPostProcessor施展“夺舍”大法强行把它的灵魂替换成一个“披着马甲的替身”代理对象。从此以后容器里注册的、你注入的、别人拿到的统统都是这个替身。而那个“纯净的肉身”除非你通过特殊手段如AopTargetUtils否则再也见不到了。是不是很巧妙如此让我们深入Bean 生命周期的核心腹地复盘这场精密的“夺舍”行动。第一幕 “夺舍”与“替身”想象一下 Spring 容器是一个“明星经纪公司”造肉身 (Instantiation)公司先招募了一个有潜力的新人调用构造函数new UserService()。这时候他还是个素人没有任何光环也不会演戏没有事务、日志逻辑。他的名字刻在花名册上但他本人还在化妆间。填属性 (Populate Bean)给他配助理、配服装依赖注入Autowired。此时他依然是素人。初始化前 (Before Initialization)做一些简单的培训BeanPostProcessor.postProcessBeforeInitialization。关键点此时他还是素人初始化 (Initialization)执行PostConstruct或InitializingBean.afterPropertiesSet。陷阱预警如果这时候他给自己打电话this.save()因为还没“夺舍”电话直接打给了素人自己没有任何明星特效事务不生效。夺舍时刻 (After Initialization - AOP)关键角色登场AbstractAutoProxyCreator金牌经纪人。经纪人拿着合同冲进来“等等这个新人需要加特效”经纪人瞬间变出一个“超级替身”代理对象。替身长得和素人一模一样。替身穿着“事务马甲”和“日志披风”。夺舍完成经纪人把花名册上的名字指向了替身。原来的素人被藏在替身的肚子里Target只有替身主动呼唤时才会出来。放入单例池 (Register Singleton)从此以后任何人从公司容器要这个人得到的都是替身。AOP 不是“胎里带”的而是“后天整容”。在整容完成前初始化结束前你面对的永远是那个没开特效的素人。第二幕关键节点定位 —— 谁在何时动了手脚Spring AOP 的“夺舍”操作严格发生在 Bean 生命周期的postProcessAfterInitialization阶段。2.1 核心执行者AbstractAutoProxyCreator这是一个实现了BeanPostProcessor接口的类。它的核心逻辑如下简化版// org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator public Object postProcessAfterInitialization(Nullable Object bean, String beanName) { if (bean ! null) { // 1. 获取当前 Bean 的唯一标识防止循环处理 Object cacheKey getCacheKey(bean.getClass(), beanName); // 2. 检查是否已经处理过避免重复代理 if (this.earlyProxyReferences.remove(cacheKey)) { return wrapIfNecessary(bean, beanName, cacheKey); } // 3. 【核心】如果需要代理则创建代理对象 return wrapIfNecessary(bean, beanName, cacheKey); } return bean; } protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // ... 省略判断逻辑 ... // 4. 获取切面配置 Object[] specificInterceptors getAdvicesAndAdvisorsForBean(...); if (specificInterceptors ! DO_NOT_PROXY) { // 5. 【夺舍发生地】创建代理 // 这里会调用 ProxyFactory (JDK 或 CGLIB) 生成新对象 Object proxy createProxy( bean.getClass(), beanName, specificInterceptors, new TargetSource(bean) ); // 6. 缓存代理类型 this.proxyTypes.put(cacheKey, proxy.getClass()); // 7. 返回替身原始 bean 被丢弃除了被 proxy 持有 return proxy; } return bean; }这个流程还是很清晰的下面咱们简单说一下Spring 容器调用完所有的init-method和PostConstruct。轮到AbstractAutoProxyCreator.postProcessAfterInitialization。它判断“这个 Bean 需要切面吗”如果需要 -createProxy- 生成新对象 -返回新对象。容器接收到返回值将其注册到单例池singletonObjects。原始对象从此“隐居”只有通过代理对象的TargetSource才能访问。第三幕流程图解 —— 完整的“夺舍”时间线让我们把时间轴拉直看看 AOP 到底插在哪一步关键结论构造函数、属性填充、PostConstruct执行时代理对象尚未存在。此时this指向的是原始对象。任何在这些阶段发生的自调用this.method()都是直接调用原始对象的方法完全绕过 AOP 拦截器链。第四幕构造函数陷阱 —— 为什么事务会失效很经典的坑很多生产事故的根源4.1 场景复现Service public class OrderService { Autowired private OrderRepository repo; // ❌ 陷阱 1: 构造函数中调用 public OrderService() { this.initData(); // 失效此时代理还没生成 } // ❌ 陷阱 2: PostConstruct 中调用 PostConstruct public void init() { this.createDefaultOrder(); // 失效此时代理还没生成 } // ✅ 正常方法 Transactional public void createDefaultOrder() { repo.save(new Order()); // 如果这里报错事务应该回滚 // 但在 init() 中调用时这里没有事务上下文 } Transactional public void initData() { // ... } }4.2 深度推导阶段一实例化Spring 调用new OrderService()。内存中产生对象RawObj(地址 0x123)。执行构造函数代码this.initData()。现状thisRawObj。容器中还没有ProxyObj。结果直接调用RawObj.initData()。无事务拦截器介入。阶段二初始化执行PostConstruct方法init()。代码执行this.createDefaultOrder()。现状this依然是RawObj。postProcessAfterInitialization还没跑呢结果直接调用RawObj.createDefaultOrder()。无事务拦截器介入。阶段三夺舍构造函数和PostConstruct都跑完了。Spring 调用AbstractAutoProxyCreator.postProcessAfterInitialization。生成ProxyObj(地址 0x456)内部持有RawObj。容器注册0x456。阶段四外部调用其他 Bean 注入OrderService拿到的是0x456(ProxyObj)。调用proxy.createDefaultOrder()-事务生效。“出生”阶段的自调用都是在对原始对象说话代理根本听不见。”因为那时候代理连影子都还没有4.3 解决方案重构不要在Constructor或PostConstruct中调用本类的Transactional方法。将逻辑提取到另一个 Service 中通过注入调用。内心我是不推荐的方式自注入黑科技Service public class OrderService { Autowired Lazy // 必须加 Lazy防止循环依赖 private OrderService self; PostConstruct public void init() { self.createDefaultOrder(); // 此时 self 是代理对象 } Transactional public void createDefaultOrder() { ... } }原理Lazy使得注入的是一个代理的引用等到PostConstruct执行时代理已经生成完毕通过self调用会走代理逻辑。第五幕早期暴露与循环依赖 —— 三级缓存的奥秘如果 AOP 是在最后才生成的那循环依赖怎么办A 依赖 BB 依赖 A。如果 A 要等生成代理后才能暴露给 B那 B 初始化时拿到的 A 就是原始对象等 A 最终生成代理后B 手里的 A 还是原始的这就不一致了Spring 的三级缓存就是为了解决这个问题提前把“未来的代理”暴露出去。5.1 三级缓存机制简述之前写了本篇略写一级缓存 (singletonObjects)成品 Bean已完成初始化 AOP。二级缓存 (earlySingletonObjects)早期暴露的 Bean已实例化未初始化可能是原始对象也可能是早期代理。三级缓存 (singletonFactories)工厂对象Lambda 表达式用于按需创建早期代理。5.2 AOP 在循环依赖中的特殊处理当 A 实例化后属性填充前发现需要 AOP且存在循环依赖风险放入三级缓存Spring 不会立刻创建代理因为此时属性还没填充可能不完整。它放入一个ObjectFactory(Lambda)addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean));B 需要 AB 初始化时依赖 A。Spring 去一级缓存没找到去二级也没找到。于是从三级缓存拿出工厂执行getObject()。提前“夺舍” (getEarlyBeanReference)protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { // 遍历所有 InstantiationAwareBeanPostProcessor // AbstractAutoProxyCreator 在这里介入 for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp (SmartInstantiationAwareBeanPostProcessor) bp; // 关键点提前生成代理 exposedObject ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }AbstractAutoProxyCreator实现了getEarlyBeanReference。它会提前调用wrapIfNecessary在属性填充之前就生成代理对象放入二级缓存生成的早期代理放入二级缓存返回给 B。这样 B 拿到的 A 已经是代理对象了。后续流程等 A 继续执行属性填充、初始化最后到达postProcessAfterInitialization时Spring 发现这个 Bean 已经被早期代理过了通过earlyProxyReferences记录。它不再重复创建代理直接返回之前的早期代理。保证整个容器中A 只有一个代理实例。循环依赖的存在迫使 Spring 将 AOP 的“夺舍”时刻提前到了属性填充阶段通过三级缓存工厂。但这只是特例。对于绝大多数没有循环依赖的 Bean夺舍依然发生在初始化之后。结语看透生命周期避开 AOP 深坑理解 AOP 代理的诞生时刻是掌握 Spring 精髓的关键一步。时机通常在postProcessAfterInitialization初始化后除非涉及循环依赖提前到属性填充期。本质是替换不是修饰。容器里最终存的是代理原始对象被隐藏。铁律构造函数和PostConstruct中的自调用永远无法触发 AOP。因为那时“替身”还没上场。最后送上金句“AOP 代理是在 Bean 初始化完成后才生成的‘替身’。任何在‘出生’阶段构造函数、PostConstruct的自调用都是在对原始对象说话代理根本听不见。若想听见请等它‘成年’外部调用或自注入。”

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2456790.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…