Spring AOP 代理与拦截器链全过程
Spring AOP 通过代理拦截“从代理对象进入的方法调用”,把事务、日志、权限、指标等横切能力组织成拦截器链。它不会自动修改所有 Java 调用:this 自调用、构造器、字段访问、目标对象外部引用和某些不可覆盖方法都可能绕过代理。真正掌握 AOP,要能从 Advisor 注册讲到 AutoProxyCreator 何时替换 Bean,再讲到 MethodInvocation.proceed() 怎样逐层执行。
学习目标
- 区分 Join Point、Pointcut、Advice、Advisor、Aspect、Target 和 Proxy;
- 理解
@EnableAspectJAutoProxy怎样注册自动代理创建器; - 讲清 AspectJ 注解方法怎样转换成 Spring Advisor/Interceptor;
- 理解 BeanPostProcessor 在何时决定并创建代理;
- 比较 JDK 动态代理与 CGLIB 的结构、类型和限制;
- 用
ReflectiveMethodInvocation.proceed()解释拦截器链顺序; - 理解 Around、Before、AfterReturning、AfterThrowing、After 的成功/异常语义;
- 解释自调用、private/final/static、手工 new、过早实例化、异步和错误切点为何失效;
- 能排查代理类型、Advisor 匹配、切面顺序、重复代理、性能和 ThreadLocal 泄漏。
一、AOP 解决什么,不解决什么
没有 AOP:
public Order createOrder(Command command) {
long start = System.nanoTime();
checkPermission();
beginTransaction();
try {
Order order = doCreate(command);
commit();
recordMetric(start, true);
return order;
} catch (RuntimeException e) {
rollback();
recordMetric(start, false);
throw e;
}
}横切逻辑重复、顺序容易不一致,并会遮挡业务规则。AOP 将“哪些方法需要什么增强”集中描述。但它不负责自动确定合理事务边界、认证策略和指标口径;切面中放复杂业务只是把耦合移动到更隐蔽的位置。
二、核心概念与真实对象关系
| 概念 | Spring AOP 中的含义 |
|---|---|
| Join Point | 可被增强的连接点,Spring AOP 主要是方法执行 |
| Pointcut | 判断哪些类/方法匹配 |
| Advice | 增强行为,例如 Before/Around |
| Advisor | Pointcut + Advice,Spring 内部常用执行单元 |
| Aspect | 一组切点和通知的模块化声明 |
| TargetSource | 提供目标对象的抽象 |
| Target | 原始业务对象 |
| Proxy | 对外暴露并持有 Advisor 链的代理对象 |
flowchart TD
A["调用方持有Proxy"] --> B["Proxy查匹配拦截器链"]
B --> C["Interceptor 1"]
C --> D["Interceptor 2"]
D --> E["TargetSource取得Target"]
E --> F["反射或方法代理调用Target方法"]Spring AOP 是代理式方法执行 AOP,不拦截任意字段读写、对象构造和所有内部调用。AspectJ 编译期/加载期编织能覆盖更广 Join Point,但部署和调试复杂度更高。
三、@Aspect 怎样进入容器
典型配置:
@Configuration
@EnableAspectJAutoProxy
public class AopConfig { }@EnableAspectJAutoProxy 通过 Import 注册基础设施,最终在 BeanFactory 中放入 AnnotationAwareAspectJAutoProxyCreator(具体类和注册细节随版本演进)。它既是 BeanPostProcessor,也是自动代理创建器。
flowchart TD
A["解析EnableAspectJAutoProxy"] --> B["注册AutoProxyCreator定义"]
B --> C["refresh阶段注册为BeanPostProcessor"]
C --> D["发现Aspect Bean"]
D --> E["解析Pointcut与Advice方法"]
E --> F["构建Advisor列表"]
F --> G["普通Bean初始化时判断是否匹配"]
G --> H["匹配则创建Proxy"]Spring Boot 在检测到 AOP 能力时可自动配置代理创建器,不一定需要手写注解;是否使用 CGLIB 还受 spring.aop.proxy-target-class 和版本默认值影响。
3.1 @Aspect 自身也是 Bean
切面类必须被容器发现,例如 @Component、@Bean 或配置导入。只写 @Aspect 而没有注册为 Bean,Spring 不会自动执行它。
@Aspect
@Component
public class MetricAspect {
@Around("execution(* com.example.order..*(..))")
public Object measure(ProceedingJoinPoint point) throws Throwable {
return point.proceed();
}
}四、Pointcut 怎样匹配
Advisor 通常包含 ClassFilter、MethodMatcher 和 Advice:
flowchart TD
A["候选Advisor"] --> B{"ClassFilter匹配目标类吗"}
B -- "否" --> C["跳过"]
B -- "是" --> D{"MethodMatcher静态匹配吗"}
D -- "否" --> C
D -- "是" --> E{"需要运行时参数匹配吗"}
E -- "否" --> F["加入拦截器链"]
E -- "是" --> G["调用时按实参再次判断"]
G --> F类和方法静态匹配可在代理创建/链缓存阶段完成;args 等运行时切点需要每次调用检查参数,成本更高。
4.1 常见表达式
execution(public * com.example.order..*Service.*(..))
within(com.example.order..*)
@annotation(com.example.Audited)
@within(org.springframework.stereotype.Service)
bean(*Service)
args(java.lang.String,..)Pointcut 应尽量窄而稳定。execution(* *(..)) 可能代理大量基础设施 Bean,增加启动、调用开销和递归风险。注解切点要确认注解放在接口还是实现方法,以及 JDK 代理方法解析对元数据查找的影响。
4.2 桥接方法和最具体方法
泛型会生成 bridge method,接口方法和实现方法也可能注解位置不同。Spring 常解析最具体目标方法和桥接方法,但复杂代理链仍应测试实际匹配,不要只看 IDE 中的方法声明。
五、Advice 怎样变成拦截器
Spring 内部执行模型趋向统一为 MethodInterceptor。不同 Advice 通过适配器或 AspectJ Advice 实现进入链:
| 声明 | 典型执行位置 |
|---|---|
| Before | proceed 前 |
| AfterReturning | 下游正常返回后 |
| AfterThrowing | 下游抛出匹配异常后 |
| After/finally | 无论成功异常,在退出路径执行 |
| Around | 包住下游,可决定是否、何时、几次 proceed |
Around 权力最大也最危险。忘记 proceed() 会短路目标;调用两次会让业务执行两次;吞异常会改变事务回滚和调用方语义;返回错误类型会导致 ClassCastException。
六、自动代理创建发生在 Bean 生命周期哪里
普通路径中,AutoProxyCreator 在 BeanPostProcessor 后置处理阶段判断目标 Bean 是否有匹配 Advisor,匹配则创建代理并把代理作为最终 Bean 暴露:
flowchart TD
A["实例化Target"] --> B["属性填充"]
B --> C["初始化回调"]
C --> D["BeanPostProcessor后置处理"]
D --> E{"有匹配Advisor吗"}
E -- "否" --> F["暴露原Target"]
E -- "是" --> G["构建ProxyFactory"]
G --> H["选择JDK或CGLIB"]
H --> I["暴露Proxy"]循环依赖场景可能通过 getEarlyBeanReference 提前创建代理,保证其他 Bean 注入的早期引用与最终对象尽量一致。若普通 Bean 在 AutoProxyCreator 注册前被 BFPP 过早创建,它可能永远以原对象暴露,AOP 不生效。
七、JDK 动态代理全过程
JDK Proxy 基于接口生成代理类。代理对象实现目标接口,并把调用交给 InvocationHandler(Spring 常见 JdkDynamicAopProxy):
flowchart TD
A["调用PaymentService接口方法"] --> B["JDK Proxy实现类"]
B --> C["InvocationHandler.invoke"]
C --> D["获取Target与Interceptor链"]
D --> E["MethodInvocation.proceed"]
E --> F["调用真实Target"]7.1 类型边界
代理可赋值给接口,但不是目标实现类实例:
PaymentService service = context.getBean(PaymentService.class); // 可用
PaymentServiceImpl impl = context.getBean(PaymentServiceImpl.class); // 可能失败业务应面向接口注入。JDK 代理只代理接口暴露的方法;实现类新增但接口没有的方法不能通过该代理类型调用。
7.2 Object 方法
equals/hashCode/toString 在代理中有专门处理语义,不能假定它们一定进入所有业务 Advisor。不要把代理对象序列化、作为跨进程身份或依赖具体代理类名。
八、CGLIB 代理全过程
Spring 内置/重新打包 CGLIB 生成目标类子类并重写可覆盖方法,方法进入 DynamicAdvisedInterceptor,再走 Advisor 链。Spring 常仍持有独立 TargetSource/target,不应简单理解成“代理子类里直接执行所有 super 方法”。
flowchart TD
A["调用目标类方法"] --> B["CGLIB生成子类Proxy"]
B --> C["重写方法进入Callback"]
C --> D["DynamicAdvisedInterceptor"]
D --> E["MethodInvocation拦截器链"]
E --> F["调用Target具体方法"]8.1 限制
- final class 不能被子类代理;
- final method 不能重写,因此不能增强;
- private method 不会被子类重写;
- static method 属于类,不通过实例虚调用拦截;
- 构造器执行发生在完整代理拦截能力建立之前;
- 模块系统、可见性和 JVM 参数也可能影响代理类生成。
Kotlin 类默认 final,Spring/Kotlin 项目通常使用 all-open 插件或接口;不能看到 proxy-target-class=true 就认为所有方法都可增强。
九、Spring 怎样选择代理方式
经典 Spring AOP 中,有用户接口且未强制类代理时可使用 JDK Proxy;没有合适接口或设置 proxyTargetClass=true 时使用 CGLIB。Spring Boot 常默认倾向 CGLIB,但应以项目版本和配置为准。
| 维度 | JDK Proxy | CGLIB |
|---|---|---|
| 基础 | 接口实现 | 目标类子类 |
| 注入类型 | 接口最稳 | 可按类,但仍建议抽象 |
| final class/method | 不涉及类继承,但只能接口方法 | 无法增强 final |
| 类新增非接口方法 | 代理不可见 | 可覆盖时可能增强 |
| 常见配置 | proxyTargetClass=false | proxyTargetClass=true |
代理方式不是“有接口永远 JDK”的绝对规则,Boot 默认、ProxyFactory 配置、optimize、接口集合和目标类都会影响最终选择。排查时看实际 bean.getClass() 和 AopUtils.isJdkDynamicProxy/isCglibProxy。
十、拦截器链 proceed() 原理
Spring 常构造 ReflectiveMethodInvocation,保存 proxy、target、method、arguments、interceptors 和当前下标。
伪代码:
public Object proceed() throws Throwable {
if (currentInterceptorIndex == interceptors.size() - 1) {
return invokeJoinpoint();
}
Object interceptor = interceptors.get(++currentInterceptorIndex);
if (interceptor instanceof InterceptorAndDynamicMethodMatcher) {
if (matcher.matches(method, targetClass, arguments)) {
return ((MethodInterceptor) interceptor).invoke(this);
}
return proceed();
}
return ((MethodInterceptor) interceptor).invoke(this);
}每个 Around/MethodInterceptor 通常:
before();
try {
Object result = invocation.proceed();
afterReturning(result);
return result;
} catch (Throwable ex) {
afterThrowing(ex);
throw ex;
} finally {
afterFinally();
}这是责任链与递归调用栈结合:proceed 不是直接调用目标,而是推进到下一个拦截器;只有下标到末尾才调用 Target。
十一、多个切面顺序怎样理解
假设外层 A、内层 B:
A around-before
B around-before
Target
B around-after
A around-after顺序由 Advisor 排序、@Order/Ordered、通知类型和注册顺序共同影响。数值越小通常优先级越高,表现为进入时更外层、退出时更晚;但同一 @Aspect 内多个同类型 Advice 的顺序不要依赖方法名或反射顺序,应合并逻辑或明确拆 Aspect。
事务与审计顺序会改变语义:审计切面在事务外层时可看到提交异常,事务内层的 after-returning 发生时数据库未必已真正提交。需要“提交后”动作应使用 TransactionSynchronization/Outbox,而不是仅靠 AOP after-returning。
十二、通知在成功和异常时的语义
| 通知 | 正常返回 | 抛异常 |
|---|---|---|
| Before | 执行 | 执行后下游可能抛错 |
| AfterReturning | 执行 | 不执行 |
| AfterThrowing | 不执行 | 匹配异常时执行 |
| After/finally | 执行 | 执行 |
| Around | 由代码决定 | 由代码决定 |
AfterReturning 能观察/有限修改返回对象内部状态,但不能像 Around 那样替换调用者收到的返回引用。AfterThrowing 只有异常继续抛出时外层能感知;Around catch 后返回默认值会把失败伪装成成功,也可能让事务代理提交。
十三、自调用为什么失效
外部调用:
调用方 → Proxy.outer/inner → Interceptor链 → Target方法Target 内部:
public void outer() {
this.inner();
}此时已经进入 Target 对象方法,this 是目标对象,普通 Java 虚调用不会回到外层 Proxy:
flowchart TD
A["外部调用Proxy.outer"] --> B["outer拦截器链"]
B --> C["Target.outer"]
C --> D["this.inner直接调用Target.inner"]
D --> E["inner对应Advisor没有新代理入口"]外层 outer 已有的事务/日志仍然存在;失效的是希望 inner 再建立的新传播、缓存、异步或重试拦截。
13.1 解决方案优先级
- 把 inner 拆到职责独立的另一个 Bean;
- 把切面边界放在真正外部入口;
- 注入自身代理,需处理循环依赖和设计可读性;
AopContext.currentProxy(),需要 exposeProxy,耦合 Spring 且基于 ThreadLocal;- 使用 AspectJ 编织,复杂度更高。
不要为了让注解生效随意把私有方法改 public;先确定它是否真是独立业务边界。
十四、exposeProxy 与 AopContext
启用 exposeProxy=true 后,代理调用期间把当前代理放入 ThreadLocal,目标方法可:
((OrderService) AopContext.currentProxy()).inner();边界:必须从代理入口进入;非代理线程调用会抛异常;异步新线程没有该 ThreadLocal;强制类型要匹配 JDK/CGLIB;业务代码耦合 Spring;忘记清理会串上下文(框架会在 finally 恢复)。通常拆 Bean 更清晰。
十五、常见无法增强的方法
| 场景 | 为什么 |
|---|---|
| private | 子类不能重写,接口也不暴露 |
| static | 调用绑定类,不通过实例代理 |
| final method + CGLIB | 不能重写 |
| final class + CGLIB | 不能继承 |
| JDK代理中的实现类专有方法 | 不在代理接口上 |
| 构造器 | 对象/代理尚未完成 |
| 手工new对象 | 不经过Spring BeanPostProcessor |
Spring 事务对非 public 方法的支持还受 Spring 版本、代理方式和注解来源规则影响。面试和项目必须标明版本,最可移植的 Service 事务边界仍是从代理外部调用的 public 方法。
十六、完整可运行 ProxyFactory Demo
import java.util.ArrayList;
import java.util.List;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.Ordered;
public class AopChainDemo {
interface PaymentService { String pay(String orderNo); }
static class PaymentServiceImpl implements PaymentService {
public String pay(String orderNo) { return "PAID:" + orderNo; }
}
static class TraceInterceptor implements MethodInterceptor, Ordered {
private final String name;
private final int order;
private final List<String> events;
TraceInterceptor(String name, int order, List<String> events) {
this.name = name; this.order = order; this.events = events;
}
public int getOrder() { return order; }
public Object invoke(MethodInvocation invocation) throws Throwable {
events.add(name + "-before");
try {
Object result = invocation.proceed();
events.add(name + "-after");
return result;
} catch (Throwable e) {
events.add(name + "-error");
throw e;
}
}
}
public static void main(String[] args) {
List<String> events = new ArrayList<String>();
ProxyFactory factory = new ProxyFactory(new PaymentServiceImpl());
factory.setInterfaces(PaymentService.class);
factory.addAdvice(new TraceInterceptor("outer", 1, events));
factory.addAdvice(new TraceInterceptor("inner", 2, events));
PaymentService proxy = (PaymentService) factory.getProxy();
System.out.println(proxy.pay("ORDER-1"));
System.out.println(events);
System.out.println(proxy.getClass().getName());
}
}程序化 ProxyFactory 直接展示 Target、Advice、代理方式和拦截器链,不依赖 @Aspect 解析。注意 addAdvice 的添加顺序决定该 Demo 顺序;自动代理场景会先对 Advisor 使用排序规则。
十七、商业监控切面设计
一个接口耗时切面应考虑:
- 使用
System.nanoTime计算时长; - 标签必须低基数,不能把订单号/用户ID作为时序标签;
- 正常、业务异常、系统异常分类;
- 异步返回
CompletableFuture时,方法返回不代表任务完成; - Reactor/协程上下文不能只靠 ThreadLocal;
- 不能记录完整参数、密码、token、医疗数据;
- 切面异常不能掩盖业务主异常;
- 监控后端故障不能阻塞核心交易。
AOP 适合单 JVM 方法边界监控,不自动等于分布式链路追踪。跨 HTTP/MQ 还需注入和传播 trace context。
十八、事务、缓存、异步为何都依赖代理
| 能力 | 典型拦截器行为 |
|---|---|
@Transactional | 获取事务属性、开启/加入、提交/回滚 |
@Cacheable | 算 key、查缓存、命中短路、未命中回填 |
@Async | 把方法调用提交执行器,立即返回Future/void |
@Retryable | 捕获匹配异常并按策略重试 |
| Method Security | 调用前认证授权,必要时拒绝 |
同一方法多个能力的 Advisor 顺序会改变事务是否包住重试、缓存是否在事务提交前可见、异步线程中是否还能加入原事务。必须明确设计,而不是只叠注解。
十九、AOP 失效排查矩阵
| 现象 | 优先证据 |
|---|---|
| Bean 不是代理 | bean.getClass、AopUtils、是否手工new |
| 是代理但通知不进 | Advisor/Pointcut是否匹配类和最具体方法 |
| 外调生效自调用失效 | 调用是否经过Proxy |
| JDK代理按实现类注入失败 | 代理接口集合和注入类型 |
| CGLIB某方法不生效 | final/private/static/可见性 |
| 切面未发现 | @Aspect 是否同时注册为Bean |
| 部分Bean无增强 | 是否在AutoProxyCreator前过早创建 |
| 异步无上下文 | ThreadLocal/MDC/事务是否跨线程 |
二十、生产故障 Runbook
20.1 确认拿到的对象
记录 Bean 名、实际 class、目标 class、代理类型、Advisor 列表。Spring 可用 AopUtils/AopProxyUtils 等诊断 API;不要在业务日志长期输出动态代理类全名作为稳定标识。
20.2 确认调用入口
从调用栈判断调用方持有 Proxy 还是 Target;检查 this 调用、构造器、@PostConstruct 内调用、事件中保存的原对象、静态工具和手工 new。
20.3 确认切点
核对目标类包名、方法可见性、接口/实现注解、桥接方法、参数运行时类型。先在测试中用 AspectJExpressionPointcut/代理工厂验证具体 Method,而不是盲目扩大为全包切点。
20.4 确认链顺序和异常
记录每个切面进入/退出,查看 @Order、事务/缓存/重试 Advisor 顺序。检查 Around 是否未 proceed、重复 proceed、吞异常或替换返回值。
20.5 AOP 导致延迟升高
用 JFR/async-profiler 看代理、反射、切面 I/O 和日志序列化占比;检查切点是否代理过多 Bean、动态参数匹配、每调用远程上报、日志锁和高基数标签。通常问题在切面工作量而不是代理方法调用本身。
20.6 重复增强
检查同一 Aspect 是否被父子 Context 重复扫描、Bean 同时手工 ProxyFactory 和自动代理、多个 AutoProxyCreator 是否升级合并、第三方 Agent/AspectJ 是否也编织。通过 Advisor 列表而不是日志次数猜测。
二十一、常见错误与后果
| 错误 | 后果 | 正确方向 |
|---|---|---|
@Aspect 未注册Bean | 完全不执行 | Component/Bean/Import |
| Around不proceed | 目标被短路 | 明确短路语义或调用一次 |
| Around调用两次 | 重复扣款/写库 | 每次业务调用一次proceed |
| 吞异常返回null | 事务可能提交、错误伪成功 | 保持异常契约 |
| this调用依赖新事务 | 内层传播不生效 | 拆Bean/调整边界 |
| 强转JDK代理为实现类 | ClassCastException/取Bean失败 | 面向接口或CGLIB |
| final方法期待CGLIB增强 | 通知不执行 | 改设计/接口代理 |
| 切点过宽 | 启动慢、递归、性能下降 | 缩小包/注解 |
| 切面记录敏感参数 | 安全与合规事故 | 白名单和脱敏 |
| ThreadLocal不finally清理 | 线程池串上下文 | 保存旧值并恢复/删除 |
二十二、Spring 与 AspectJ 版本边界
Spring 5.3 常用于 JDK 8;Spring 6 基线 Java 17。Spring AOP 使用 AspectJ pointcut 解析/注解风格不等于使用 AspectJ 编织:默认仍是运行时代理。若启用 load-time weaving/ajc,Join Point 范围和自调用行为可能不同,排查时先确认项目到底是 proxy-based 还是 woven。
Spring Boot 常默认 CGLIB,但不同 Boot 版本和 spring.aop.proxy-target-class 可改变。不能把某个 Boot 项目观察到的代理方式当成所有 Spring Framework 项目的固定规则。
二十三、面试标准回答
Spring AOP 完整执行链
容器把Aspect通知解析为Advisor;AutoProxyCreator作为BeanPostProcessor在Bean初始化时查找匹配Advisor,匹配则通过ProxyFactory创建JDK或CGLIB代理。外部调用进入代理,代理取得目标和MethodInterceptor链,ReflectiveMethodInvocation.proceed逐层推进,最后才调用目标方法,再按调用栈反向退出。
JDK代理和CGLIB区别
JDK Proxy生成接口实现并由InvocationHandler处理,代理可按接口赋值;CGLIB生成目标类子类并拦截可覆盖方法,final类/方法、private/static不能按子类方式增强。最终选择还受proxyTargetClass、Boot默认和接口集合影响。
自调用为什么失效
Spring增强发生在调用进入Proxy时。外部调用Proxy.outer后进入Target.outer,里面的this.inner()是目标对象内部普通调用,不会重新回到代理,因此inner独有的事务、缓存或异步Advisor没有新执行机会。
Around Advice 有什么风险
它可以不调用、调用一次或多次proceed,也能替换返回值和异常。忘记proceed会短路目标,调用两次会重复业务,吞异常可能让事务提交,返回类型错误会导致调用方异常,因此只在需要完整控制时使用。
多个切面顺序怎样判断
先看Advisor的Ordered/@Order,数值小通常在进入时更外层、退出时更晚;再看具体通知适配和同一Aspect内声明。事务、缓存、重试顺序会改变业务语义,应通过拦截器链和测试验证,不能只背注解顺序。
二十四、关联知识与验收
掌握验收:
- 能从
@EnableAspectJAutoProxy画到 AutoProxyCreator、Advisor 和 Proxy; - 能解释 Advisor 为什么是 Pointcut + Advice;
- 能用
proceed下标推进画出两个 Around 的嵌套顺序; - 能根据接口、proxyTargetClass、final/private 判断代理方式和增强边界;
- 能解释外调有效、自调用无效,但外层已有增强仍存在;
- 能说明 Around 不/多次 proceed 的业务后果;
- 能诊断 Bean 不是代理、切点不匹配、过早实例化和异步上下文丢失;
- 能设计不泄露敏感数据、不产生高基数和不阻塞业务的监控切面。
