Spring AOP 代理与拦截器链
Spring AOP 在调用者和业务目标之间放置代理。调用进入代理后,框架按方法选择增强,再执行目标。事务、方法安全、缓存和异步执行都可以利用这层入口,但它们是否生效取决于调用确实经过哪一个对象。
调用者 → 代理 → 选中的拦截器链 → 目标方法
↘ 短路返回或抛异常从一个方法调用建立代理
Advisor、Pointcut 与 Advice
Advice 描述增强动作,Pointcut 描述适用的类和方法,Advisor 将二者组合。TargetSource 提供目标对象;普通 singleton 代理使用固定目标,也可以由特定 TargetSource 在调用时取得目标。代理保存这些配置,业务对象无需自己执行切点判断。AOP 概念
| 增强形式 | 正常调用时机 | 能否决定继续目标 |
|---|---|---|
| Before | 目标之前 | 抛异常可阻止执行,正常返回后继续 |
| After returning | 后续调用正常返回后 | 可读取结果,通常不替换返回引用 |
| After throwing | 后续调用抛出匹配异常后 | 观察或另抛异常 |
| After / finally | 正常或异常结束时 | 执行清理 |
| Around | 包围后续链 | 可以继续、短路、替换返回值或异常 |
Spring AOP 主要处理方法执行连接点。构造器、字段读写和任意代码位置需要不同执行模型,例如 AspectJ 织入;声明 AspectJ 风格注解不等于启用了字节码织入。Spring AOP 能力
完整 ProxyFactory 程序
下载代理实验源码。Linux 普通账号准备 Docker 与 unzip;构建镜像 Maven 3.9.12-eclipse-temurin-25,Spring 7.0.9,AspectJ weaver 1.9.25.1,编译目标 Java 17。程序在本地进程运行,没有网络调用或持久数据。
mkdir spring-aop-lab
unzip spring-framework-aop-proxy-lab.zip -d spring-aop-lab
cd spring-aop-lab
mkdir -p .m2
IMAGE=maven:3.9.12-eclipse-temurin-25
docker pull "$IMAGE"第一个程序直接配置代理,便于观察 Around 的嵌套。完整 src/main/java/example/FirstProxy.java:
package example;
import java.util.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
public class FirstProxy {
public interface Echo { String say(); }
public static class Target implements Echo {
public String say() { return "ok"; }
}
public static void main(String[] args) {
var trace = new ArrayList<String>();
var factory = new ProxyFactory(new Target());
factory.addAdvice((MethodInterceptor) invocation -> {
trace.add("outer:enter");
try { return invocation.proceed(); }
finally { trace.add("outer:exit"); }
});
factory.addAdvice((MethodInterceptor) invocation -> {
trace.add("inner:enter");
try { return invocation.proceed(); }
finally { trace.add("inner:exit"); }
});
var proxy = (Echo) factory.getProxy();
System.out.println("result=" + proxy.say());
System.out.println(trace);
}
}MethodInterceptor 获得本次 invocation,调用 proceed() 把控制权交给后续链。finally 保证异常路径也执行收尾;一次调用的状态保留在局部变量中,不存入可能被并发复用的拦截器字段。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>aop-lab</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>7.0.9</version></dependency>
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.25.1</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><parameters>true</parameters></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version></plugin>
</plugins></build>
</project>docker run --rm --user "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/maven \
-v "$PWD:/work" -v "$PWD/.m2:/cache" -w /work "$IMAGE" \
sh -ec 'mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/cache \
clean package dependency:build-classpath -Dmdep.outputFile=target/classpath.txt
java -cp "target/classes:$(cat target/classpath.txt)" example.FirstProxy'result=ok
[outer:enter, inner:enter, inner:exit, outer:exit]这是调用栈嵌套,与 BeanPostProcessor 的两次正序遍历不同。先进入 outer,outer 的 proceed 进入 inner,目标返回后先退出 inner,再退出 outer。程序直接调用真实 Spring ProxyFactory。编程代理
构建容器显式使用宿主 UID/GID,确保当前目录和 .m2 可写。镜像和依赖在联网准备后可离线运行;企业仓库通过批准的 Maven settings 指定,凭据不进入示例。依赖错误先检查实际版本树,权限错误先检查挂载,不需要更改 AOP 配置。Maven settings
代理类型和调用入口
JDK 代理与类代理
JDK 动态代理实现接口,调用者可按接口持有,但不能把代理强转为目标实现类。类代理生成目标类的子类,适用于无接口组件;它要求类可继承,方法可覆盖。Spring 中两种策略都会委托实际目标,不能据“生成子类”推断 self 调用自动经过增强。代理机制
| 情况 | JDK 代理 | 类代理 |
|---|---|---|
| 接口方法 | 通过接口进入代理 | 可覆盖的实例方法可增强 |
| 实现类是 final | 仍可通过其接口委托调用 | 无法创建继承代理 |
| 实现方法是 final | 对应接口调用仍可拦截 | 不能覆盖该方法 |
| private / static | 不属于接口代理入口 | 不可按覆写路径增强 |
| 按实现类注入 | 通常不可赋值 | 类可代理时可赋值 |
类代理遇到不可拦截的 final 方法还可能直接执行代理实例继承的方法,不能把它当作安全的“无增强调用目标”后备方案。需要增强的方法应放在清晰、可代理的公开服务接口上。
@EnableAspectJAutoProxy 默认可按接口创建代理,proxyTargetClass=true 请求类代理。Boot 可能有不同默认设置,所以定位时查看最终对象,不凭代码中有无接口推断。
自调用跳过再次匹配
外部调用 inner:caller → proxy → advice → target.inner
目标内部调用: caller → proxy → target.outer → this.inner第二条路径已经进入目标对象,this.inner() 不会重新回到代理。把 inner 改为 public 并不能改变调用引用。
扩展源码使用 @Configuration、@EnableAspectJAutoProxy、一个 Work Bean 和以下切面:
@Aspect
public static class Counter {
int advised;
@Before("execution(* example.AopLab.Work.inner(..))")
public void before() { advised++; }
}计数器只用于这个单线程实验。生产增强应避免无同步共享计数,使用适当指标设施。执行:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.AopLab self'selfAdvised=0 externalAdvised=1 targetCalls=2
jdkImplementationCastRejected=true
advisors=2outer 内部调用和外部直接调用都到达业务目标,但只有后者触发 inner 的 Before。实验还验证 JDK 代理不能强转实现类。Advisor 数包含框架所需基础设施,不应把它固定解释为两个业务切面。
需要单独事务或缓存语义时,将 inner 放到协作 Bean,由外部调用它的代理。注入自己或使用 AopContext 会增加代理上下文耦合,应只在明确的遗留兼容场景使用;更换为织入需要重新评估构建、类加载和运行部署。
一次 invocation 怎样走完增强链
创建阶段决定代理,调用阶段决定方法链
自动代理创建器是 BeanPostProcessor 家族成员。普通初始化后,它查找可应用 Advisor,满足条件时返回代理;循环依赖则可能通过 early-reference 协议先建立代理。代理中 AdvisedSupport 保存目标来源、接口和 Advisor 配置,按方法缓存拦截器链。自动代理
类过滤与静态方法匹配可以提前完成,依赖运行时参数的匹配在调用期间判断。AspectJ 表达式常用 execution、within、this、target、args 和注解匹配:this 面向代理类型,target 面向业务目标,args 面向运行参数。完整语法及 Spring 支持子集应按官方参考使用,不把 AspectJ 所有连接点都当作 Spring 代理支持项。切点表达式
ReflectiveMethodInvocation 保存当前方法、参数、目标、拦截器列表及当前索引。proceed 推进索引;遇到动态匹配器先判断,不命中则继续推进;走到链尾才调用目标。7.0.9 invocation 实现
currentInterceptorIndex = -1
→ proceed:推进到 0,调用外层
→ 外层 proceed:推进到 1,调用内层
→ 内层 proceed:到达链尾,调用目标
← 返回值/异常沿现有调用栈返回同一个 MethodInvocation 的索引是可变状态。手写重试拦截器不能假定重复调用同一个 proceed 就会从自己后面完整重放链条;Spring 的 ProxyMethodInvocation 提供可克隆 invocation,AspectJ around 的 ProceedingJoinPoint 适配又有自己的调用实现。通常把重试放到明确的外层调用边界,才能更容易保证每次重新进入完整事务链。
短路和异常需要明确结果
不调用 proceed,后续增强和目标都不会执行。缓存命中、鉴权拒绝可以使用短路,但返回值必须与方法契约兼容。
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.AopLab short-circuit
java -cp "target/classes:$(cat target/classpath.txt)" example.AopLab final'两个程序分别输出 targetCalls=0 与 finalClassProxyRejected=true。前者拦截 void 方法后直接返回,后者强制代理 final 类并捕获指定 AopConfigException。若把第一个方法换成基本类型返回,返回 null 会造成类型契约错误,必须同步修改拦截器。
捕获并包装异常会改变外层增强看到的异常类型。例如事务判断依据穿过事务拦截器的异常,而不是业务代码最早抛出的那一个。保存 cause;若无需改变业务错误,就原样传播。
Advisor 顺序改变业务含义
排序靠 Ordered、Order 或基础设施配置,不能看注解在文件里的上下位置。重试包在事务外,通常每次可以重新建事务;放在事务内可能继续使用已失败的同一物理事务。指标放在缓存外统计请求,放在缓存内更接近真实计算。相同优先级不适合作为跨模块固定顺序协议。增强顺序
异步增强如果把目标提交到另一线程,原调用栈的 finally 只描述任务提交结束。响应式方法返回 Publisher,也不等于订阅和业务完成。计时、事务和上下文应绑定到实际完成、错误或取消信号,不能用同步方法返回代替异步生命周期。
代理故障的定位与修复
查看容器实际暴露对象:
Object bean = context.getBean("work");
System.out.println("proxy=" + AopUtils.isAopProxy(bean));
System.out.println("target=" + AopUtils.getTargetClass(bean).getName());
if (bean instanceof Advised advised) {
for (var advisor : advised.getAdvisors()) {
System.out.println(advisor.getAdvice().getClass().getName());
}
}这段诊断需要导入 org.springframework.aop.support.AopUtils 与 org.springframework.aop.framework.Advised。临时输出对象类型和增强类型即可,不打印方法参数中的身份、凭据或业务秘密。
增强未执行时,顺着实际调用检查:对象是否由启用自动代理的 Context 创建;取得的是代理还是直接 new 的目标;方法能否被当前策略拦截;切点是否匹配;是否属于 self 调用;前置增强是否短路。类转换异常优先检查代理接口;启动代理失败检查 final、方法可见性和字节码依赖。
修复后同时运行命中与不命中用例,确认目标调用次数、返回结果和异常仍正确。扩大切点可能“修好”一次缺失,却把基础设施自身纳入递归增强;窄注解或稳定服务接口更便于控制。实验容器执行后自动移除,不需要停止服务或撤销网络配置。
权威资料与规范地址
可按接口、注解和实现类查阅详细契约。
