AOP:代理选择、调用入口与拦截链
本篇回答“同一个方法为什么有时增强、有时没有”。固定实现为 Framework v6.1.12,Boot 配置语境为 v3.3.3;不要把字节码编织、代理、注解存在三者混为一谈。HTTP/TLS 已在网络权威正文,事务规则只回链现有事务正文。
代理入口与 Advisor 的职责
Pointcut 决定匹配范围,Advice 定义附加行为,Advisor 组合二者。调用者持有 proxy,proxy 为本次方法选择拦截器链,再调用 target。Spring AOP 通常不改写目标对象中每一次 this 调用;AspectJ 编织是另一执行模型,其自调用行为不能直接用于解释代理。@Aspect 注解切面的声明层——通知类型顺序、参数绑定、跨切面 @Order、引介与两种模型的对照——由 AOP 切面页维护,本篇只维护代理类型选择与 proceed 链语义。
常见注解创建入口是 @EnableAspectJAutoProxy → AnnotationAwareAspectJAutoProxyCreator → wrapIfNecessary → findEligibleAdvisors → ProxyFactory。基础设施类、被跳过的 Bean、没有适用 Advisor 的对象可以原样返回;特殊 TargetSource 路径可能在实例化前短路。类过滤器先约束类,再匹配方法。事务 Advisor 可基于事务属性源工作,不要求业务中有一个 Aspect 类。
AbstractAdvisorAutoProxyCreator负责候选发现、适用筛选与排序。早期引用与最终身份只在循环依赖篇维护:自动代理创建器不是在每个阶段无条件创建新代理。
一个代理上的多 Advisor 和多个代理嵌套不是同一种结构。前者是该代理的一条链,后者还要沿 target 追到另一层代理。默认代理通常可经 Advised.getAdvisors 诊断,但 opaque 等配置可隐藏此接口,不应强转猜测。取证至少包括 proxy 类、target 类、Advisor 顺序、实际方法和调用方的引用来源;仅 isAopProxy=true 不足以证明某个方法经过某项增强。
代理类型:看配置分支而非只看接口
原码:DefaultAopProxyFactory.createAopProxy 与相邻接口判定,v6.1.12,60–86 行。固定来源。
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
/**
* Determine whether the supplied {@link AdvisedSupport} has only the
* {@link org.springframework.aop.SpringProxy} interface specified
* (or no proxy interfaces specified at all).
*/
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] ifcs = config.getProxiedInterfaces();
return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
}按输入分支推导,而不是背口号:
- optimize、proxyTargetClass、无用户配置代理接口都为 false,走外层 else,选择 JDK。
- 任一个为 true,先要求能确定 targetClass;否则抛 AopConfigException。
- 进入该分支也不等于必选 CGLIB:targetClass 为接口、JDK Proxy 类或 lambda 类时仍选择 JDK;普通具体类才返回 ObjenesisCglibAopProxy。
“没有用户接口”检查的是配置的 proxiedInterfaces,不是简单数 target 类声明了几个接口。有业务接口且 proxyTargetClass=true 的普通实现类仍会用 CGLIB;反过来强制类代理遇到上列例外也可用 JDK。本段选的是 AopProxy 实现,不证明 Boot 的默认属性,也不证明两者性能孰优;实际应用要查有效配置和实际代理类型。
外部入口、自调用与 final 的边界
JDK 代理暴露所配置接口,不能强转为目标实现类。CGLIB 类代理创建子类,final 类不能这样继承,final/private 方法不能被覆盖增强,包可见方法还受继承与访问边界影响。通过接口的 JDK 代理调用不依赖覆盖实现方法,因此不能笼统说“任何 final 实现方法都无法被任何代理拦截”。
关键仍是入口:proxy.outer 进入链再调用 target.outer;其中 this.inner 是 target 上的普通调用,不重新进入 proxy。改为 CGLIB 并不改变这个委托事实。初始化中的调用又叠加了最终包装可能尚未完成的条件;容器外 new Service 和直接反射调用 target 也绕过已配置的代理。
类代理通常借助 Objenesis 避免代理子类构造造成的重复 target 构造,但这不表示目标构造器从不执行,也不保证 final 方法在代理壳对象上访问字段会产生某个可靠的副作用。本实验不拿 final 方法的计数或字段值做证据,这里只依据可覆盖性界定类代理增强能力。
需要独立拦截的行为优先拆成独立服务并注入其代理;自注入或 AopContext.currentProxy 需要额外配置及线程上下文,增加耦合,也不能自然跨线程传递。生命周期顺序回查上一主线。
proceed:嵌套调用而非广播通知
JdkDynamicAopProxy.invoke 处理 equals/hashCode、Advised 等特殊分支,取得 target 与本次方法的拦截链;无拦截器可直接反射调用,有链则进入 ReflectiveMethodInvocation。CGLIB 的普通增强路径也使用 MethodInvocation 语义,不是“没有 target 和链”。
DefaultAdvisorChainFactory 做 class filter、静态方法匹配、Advice 到 MethodInterceptor 的适配。运行时 MethodMatcher 还需根据本次参数判断,静态链可缓存不表示动态结果可跨参数复用。固定链工厂。
保留原文连续原码:ReflectiveMethodInvocation.proceed,v6.1.12,160–180 行,截至动态匹配分支,不假装展示整个方法。来源。
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.matcher().matches(this.method, targetClass, this.arguments)) {
return dm.interceptor().invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}游标从 -1 开始,先前进再取当前拦截器;链尾才调用 joinpoint。动态不匹配只是跳过这一项,仍继续下游,不是整个目标被跳过。普通非动态拦截器的调用在本摘录后续分支。
把 A/B 都写成 try/finally 并各调用一次 proceed,调用栈如下(教学时序,不是额外源码):
A+ → B+ → target → B- → A-
抛错 ↗ finally 仍按 B、A 退栈
A+ → short-return → A- (未进入 B 与 target)同一个 MethodInvocation 重复 proceed 不会自动把游标重置为 -1。手工重试需明确独立调用状态,Spring 的可克隆 invocation 支持这类需求,但还必须检查操作幂等性。不能以“再调一次 proceed”宣称整条链都重新执行。
短路与异常:谁真正执行了目标
环绕通知可改参数、返回固定值、抛错、吞错或不调用 proceed。返回正常不证明 target 执行;外层缓存命中就可能绕开内层权限。基本类型返回方法却由通知返回 null,JDK 路径可在代理返回边界抛 AopInvocationException,不能误报为 target 数据库失败。
目标抛异常时,已经进入的 finally 通知反向退栈;“正常返回之后”的语句则不会执行。内层通知若把异常转换为另一类型,外层看到的是转换后的异常;若吞掉异常返回默认值,外层看到正常返回。删除或移动一个 catch/finally 会改变观测与资源释放,不只是日志顺序。
事务拦截器如何依据异常决定完成、重试应放哪一层、rollback-only 的作用,统一见事务传播与失效,不在本篇维护第二套规则。排障要画出“谁看见哪个异常”,不能只搜方法注解。
此外,JDK/CGLIB 实际实现还有 TargetSource 释放、代理上下文恢复、返回 this 替换及协程适配等路径;发生资源泄漏应继续看完整 JDK 实现与CGLIB 实现,不能把这段 proceed 当作全部调用清理代码。
实验:同一方法,改变入口和控制流
重试与事务的顺序还应单独画出两条链:Retry → Transaction → Target 中,每次尝试重新穿过事务拦截器;前次事务完成回滚与清理后,下一次才有机会开启新事务。Transaction → Retry → Target 中,重试发生在同一外层事务内,数据库错误或参与者标记可能已让该事务不可提交,第二次业务返回成功也不证明最终 commit 成功。前者若仍参与更外层 REQUIRED 事务,也不能无条件声称每次都是新物理事务;传播与资源边界回查事务正文。
验证变式:第一次调用触发会使事务不可继续的失败,第二次返回成功;记录每次进入事务的次数、连接及最终提交结果,而不是只记录 target 返回值。H2/MySQL 对具体错误的事务状态可能不同,不能假定任意异常都必然污染事务。本页现有短路/异常实验尚未执行这组重试组合;它是需补测的明确推演,不冒充实跑结论。评分点:区分通知嵌套与物理事务、说明失败状态、核对最终持久结果。
运行 JAVA_HOME=$(/usr/libexec/java_home -v17) bash labs/spring-senior/run.sh proxy。ProxyFactory 显式配置类型与 A/B 两个 MethodInterceptor,不混入扫描或 AspectJ 表达式条件;唯一源码如下。
package study.spring;
import java.util.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
public class ProxyLab {
public interface Service { void outer(); void inner(); }
public static class Target implements Service {
boolean fail;
int innerCalls;
public void outer() { events.add("outer"); inner(); }
public void inner() {
innerCalls++; events.add("inner");
if (fail) throw new IllegalStateException("synthetic target failure");
}
}
static final List<String> events = new ArrayList<>();
public static void main(String[] args) {
events.clear();
String mode = args.length == 0 ? "jdk" : args[0];
if (!List.of("jdk", "cglib", "jdk-short", "cglib-short", "jdk-throw", "cglib-throw").contains(mode))
throw new IllegalArgumentException(mode);
var target = new Target();
var factory = new ProxyFactory(target);
boolean cglib = mode.startsWith("cglib");
factory.setProxyTargetClass(cglib);
for (String name : List.of("A", "B")) factory.addAdvice((MethodInterceptor) inv -> {
events.add(name + "+");
try { return inv.proceed(); }
finally { events.add(name + "-"); }
});
Service proxy = (Service) factory.getProxy();
if (AopUtils.isCglibProxy(proxy) != cglib || AopUtils.isJdkDynamicProxy(proxy) == cglib)
throw new AssertionError("wrong proxy type: " + proxy.getClass());
proxy.outer();
if (!events.equals(List.of("A+", "B+", "outer", "inner", "B-", "A-"))) throw new AssertionError(events);
events.clear(); target.inner();
if (!events.equals(List.of("inner"))) throw new AssertionError(events);
events.clear(); proxy.inner();
if (!events.equals(List.of("A+", "B+", "inner", "B-", "A-"))) throw new AssertionError(events);
int before = target.innerCalls;
if (mode.endsWith("short")) {
// Insert between A and B. This advice deliberately does not call proceed.
factory.addAdvice(1, (MethodInterceptor) inv -> { events.add("short"); return null; });
events.clear(); proxy.inner();
if (!events.equals(List.of("A+", "short", "A-")) || target.innerCalls != before)
throw new AssertionError("short circuit: " + events);
}
if (mode.endsWith("throw")) {
target.fail = true; events.clear();
try { proxy.inner(); throw new AssertionError("expected target failure"); }
catch (IllegalStateException expected) {
if (!"synthetic target failure".equals(expected.getMessage())) throw expected;
if (!events.equals(List.of("A+", "B+", "inner", "B-", "A-")) || target.innerCalls != before + 1)
throw new AssertionError("exception unwind: " + events);
}
}
System.out.println("PASS proxy " + mode + " type=" + (cglib ? "CGLIB" : "JDK") + " events=" + events);
}
}- jdk/cglib:用 AopUtils 断言真实代理类型。proxy.outer 内 self-call 只有一套 A/B;target.inner 只有 inner;proxy.inner 有完整 A/B 链。
- jdk-short/cglib-short:在 A 与 B 之间插入不 proceed 的通知,实际事件为 A+、short、A-,target 调用计数不增。
- jdk-throw/cglib-throw:目标真实抛 IllegalStateException,调用方必须接到指定异常,事件仍为 A+、B+、inner、B-、A-,计数只增一。
本批六模式已通过,实测与上述断言一致,实验说明记录输入和边界。@Aspect 声明层、事务叠加错位与编织对照的八模式实验见 AOP 切面页实验(同一 run.sh 入口的 aop 组)。final 副作用、动态参数切点、基本类型 null、原生镜像和代理性能未实测,相关说明是源码/契约推导,不冒充日志结果。
架构师推演:增强顺序也是契约
假设授权、缓存、限流和审计叠加。方案一全部交给注解与 order,接入快,但链组合越多越难证明短路和异常路径;方案二显式编排关键业务边界,只保留少量通用增强,可读性更高但接口与代码更多。同一个 Aspect 中通知类型和同优先级也有规则,不能以一个 order 数字解释所有先后。
核心不变量是未经授权不能读缓存,缓存 key 必须隔离租户,日志不能暴露 password/token,成功审计的含义不能被某层吞错改变。若需要可靠审计,单次 after-returning 发远程消息不提供可靠交付,必须有持久化及恢复协议;通知负责接入,不替代资源协议。
负载变化时,远程授权每次调用会增加延迟和耦合;权限缓存降低成本但带来撤销窗口。要求立即撤销应采用权威检查或版本协议,允许窗口才定义 TTL 和传播时延。给出跨租户、缓存命中、权限撤销、远端超时和异常转换的组合验证,才比“调一下 order”更有说服力。诊断应采样与脱敏,不在每次调用输出完整参数和链。
可口述主线:“先选适用 Advisor 与代理,再为本次方法匹配链;proceed 按嵌套栈推进,入口、短路和异常决定实际效果。”连续追问依次改变配置、调用者引用、参数匹配和通知顺序,便能区分背名词与解释行为。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。