4.1 横切关注点与代理的诞生


4.1 横切关注点与代理的诞生

本节摘要:日志、监控、事务这类需求散布在每个业务方法周围,手工写就是复制粘贴的灾难。AOP 的解法是:容器给目标对象生成一个代理,调用方拿到的是代理,切面逻辑挂在代理上。本节讲代理的两种生成方式、各自的限制,以及由此产生的"自调用失效"问题的机理与解法。

从复制粘贴说起

假设每个业务方法都要计时,没有 AOP 时你会这样写三十遍:

public Order createOrder(OrderCmd cmd) { long t = System.nanoTime(); try { // 真正的业务逻辑 return save(cmd); } finally { log.info("耗时 {}", System.nanoTime() - t); } }

计时是"横切关注点":它不属于任何一项业务,却要出现在所有业务旁边。复制三十遍的代价不只是字数——改一次计时的格式要动三十处,漏一处就有盲区。AOP 把这段逻辑抽成"切面",在容器装配时织到目标对象外面。

代理:包裹目标的外壳

Spring AOP 的织入发生在运行期,手段是动态代理。请求旅程中,调用方持有的 OrderService 引用其实指向代理对象:代理先执行切面逻辑,再转调真正的目标方法。代理有两种生成路线:

路线 条件 原理 限制
接口代理 目标类实现了接口 运行期生成实现同一接口的代理类 只能拦接口上声明的方法
子类代理 无接口也可 运行期生成目标类的子类 目标类与方法不能是 final;构造器不能私有

有个简单办法确认你面对的是代理还是裸对象——打印类名:

log.info("实际类型:{}", orderService.getClass().getName()); // 输出类似 ...$$SpringCGLIB$$0 即代理;输出原始类名即裸对象

图 4-1 调用方眼中的对象与真实结构

图 4-1 调用方眼中的对象与真实结构

自调用:代理的盲区

代理只能拦截"从外面打来的电话"。类内部方法互调走的是 this,绕过了代理:

@Service public class OrderService { public void batchImport(List<OrderCmd> cmds) { for (OrderCmd c : cmds) { this.createOrder(c); // this 直达目标,切面全部失效 } } @Transactional public void createOrder(OrderCmd cmd) { ... } }

batchImport 里对 createOrder 的调用不会经过代理,事务注解形同虚设,批量导入一旦中途失败,已写入的数据不会回滚——这是生产事故里的常客。三种修法按推荐顺序:拆类(把 createOrder 移到另一个 Bean,天然经代理);改结构(让外部循环调用带事务的方法);自注入自身代理(可行但可读性差,留作了解)。

⚠️ 判断口诀:注解生效的前提是调用穿过代理,this 调用不算。 遇到"注解有时灵有时不灵",先查调用链里有没有 this 前缀。

实验:亲手剥开一个代理

背景:说"容器注册的是代理"终究是断言,用两个小实验把它变成亲眼看的事实。实验一:验证对象身份。在任意注入了 OrderService 的类里打印类名(上文那行日志),输出里若出现代理标记字样,说明容器递给你的确实是代理;再对比在目标类自己的构造器里打印的类名(那里是裸对象)——同一个类,两个视角,两种身份。结果解读:注入的是代理、方法内部 this 是裸对象,这"一体两面"正是自调用失效的机理实证。

实验二:验证拦截能力。给接口代理路线做一个反例——目标类实现了接口,但被调用的方法只定义在实现类上、接口上没有。操作:在切面里拦截该方法并打日志,再发请求。结果:日志不出现,代理对接口上不存在的方法无能为力。变式:给接口补上该方法声明(或让配置走子类代理路线),日志立刻出现。解读:代理路线不是实现细节,它直接决定了切面的覆盖范围;遇到"拦不到"的疑问,先确认代理路线与方法的可见性,再看表达式。

这两个实验十分钟即可完成,但回报极高:此后所有"注解不生效"类问题,你都有了系统性的排查起点——先问"这次调用到底打在谁身上"。

关于代理的三个追问

其一,代理会不会让调试变难?会一些——调用栈里多出代理与拦截器的帧,方法断点也可能在代理层先停一下;应对办法是给断点加条件,或直接在目标方法内打日志。其二,一个对象能被几个切面包?任意多层,洋葱式嵌套,顺序由切面优先级决定;层级多了难以推理时,用打印类名与切面日志逐层确认。其三,切面本身能被切吗?理论上可以,工程上别这么做——基础设施互相缠绕后的执行顺序几乎不可推理。把"代理是调用方与目标之间的中间人"这条心智模型立稳,以上三问的答案都能自行推出。

本节要点回顾

  • 横切关注点抽成切面,消灭围绕业务的重复代码
  • 容器注册的是代理,打印类名可验证
  • 接口代理与子类代理各有前提,final 与私有构造是硬边界
  • 自调用绕过代理是注解失效的第一嫌疑

读完自测

三道回收题:注入的 OrderService 与目标类里的 this 是不是同一个对象,为什么;接口上没有声明的方法为什么可能拦不到;"注解有时生效有时不生效"你的第一排查动作是什么。第三题的答案应当是"画出调用链,找 this 前缀"。

补一个团队协作视角的提醒:切面是全局性的,动一处影响所有被拦方法,评审权重应当高于普通业务代码。规范做法是切面集中放独立的包、配统一前缀命名、每个切面写清楚拦谁与顺序,并在团队文档登记现有切面清单。见过太多项目里切面散落各包、彼此不知道对方存在,出问题时翻遍代码找不到是哪层包装纸在作怪——清单化是最低成本的解药。
再强调一次验证优先:本章三个实验(打印类名、对照拦截、顺序日志)加起来不超过二十分钟,却能把"代理"从名词变成可触摸的运行时事实。学习框架内部机制,这一小段时间的投入几乎没有例外地物超所值,值得养成习惯。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U