Skip to content

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:

java
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
AdvisorPointcut + Advice,Spring 内部常用执行单元
Aspect一组切点和通知的模块化声明
TargetSource提供目标对象的抽象
Target原始业务对象
Proxy对外暴露并持有 Advisor 链的代理对象
mermaid
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 怎样进入容器

典型配置:

java
@Configuration
@EnableAspectJAutoProxy
public class AopConfig { }

@EnableAspectJAutoProxy 通过 Import 注册基础设施,最终在 BeanFactory 中放入 AnnotationAwareAspectJAutoProxyCreator(具体类和注册细节随版本演进)。它既是 BeanPostProcessor,也是自动代理创建器。

mermaid
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 不会自动执行它。

java
@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:

mermaid
flowchart TD
    A["候选Advisor"] --> B{"ClassFilter匹配目标类吗"}
    B -- "否" --> C["跳过"]
    B -- "是" --> D{"MethodMatcher静态匹配吗"}
    D -- "否" --> C
    D -- "是" --> E{"需要运行时参数匹配吗"}
    E -- "否" --> F["加入拦截器链"]
    E -- "是" --> G["调用时按实参再次判断"]
    G --> F

类和方法静态匹配可在代理创建/链缓存阶段完成;args 等运行时切点需要每次调用检查参数,成本更高。

4.1 常见表达式

text
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 实现进入链:

声明典型执行位置
Beforeproceed
AfterReturning下游正常返回后
AfterThrowing下游抛出匹配异常后
After/finally无论成功异常,在退出路径执行
Around包住下游,可决定是否、何时、几次 proceed

Around 权力最大也最危险。忘记 proceed() 会短路目标;调用两次会让业务执行两次;吞异常会改变事务回滚和调用方语义;返回错误类型会导致 ClassCastException。

六、自动代理创建发生在 Bean 生命周期哪里

普通路径中,AutoProxyCreator 在 BeanPostProcessor 后置处理阶段判断目标 Bean 是否有匹配 Advisor,匹配则创建代理并把代理作为最终 Bean 暴露:

mermaid
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):

mermaid
flowchart TD
    A["调用PaymentService接口方法"] --> B["JDK Proxy实现类"]
    B --> C["InvocationHandler.invoke"]
    C --> D["获取Target与Interceptor链"]
    D --> E["MethodInvocation.proceed"]
    E --> F["调用真实Target"]

7.1 类型边界

代理可赋值给接口,但不是目标实现类实例:

java
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 方法”。

mermaid
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 ProxyCGLIB
基础接口实现目标类子类
注入类型接口最稳可按类,但仍建议抽象
final class/method不涉及类继承,但只能接口方法无法增强 final
类新增非接口方法代理不可见可覆盖时可能增强
常见配置proxyTargetClass=falseproxyTargetClass=true

代理方式不是“有接口永远 JDK”的绝对规则,Boot 默认、ProxyFactory 配置、optimize、接口集合和目标类都会影响最终选择。排查时看实际 bean.getClass()AopUtils.isJdkDynamicProxy/isCglibProxy

十、拦截器链 proceed() 原理

Spring 常构造 ReflectiveMethodInvocation,保存 proxy、target、method、arguments、interceptors 和当前下标。

伪代码:

java
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 通常:

java
before();
try {
    Object result = invocation.proceed();
    afterReturning(result);
    return result;
} catch (Throwable ex) {
    afterThrowing(ex);
    throw ex;
} finally {
    afterFinally();
}

这是责任链与递归调用栈结合:proceed 不是直接调用目标,而是推进到下一个拦截器;只有下标到末尾才调用 Target。

十一、多个切面顺序怎样理解

假设外层 A、内层 B:

text
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 后返回默认值会把失败伪装成成功,也可能让事务代理提交。

十三、自调用为什么失效

外部调用:

text
调用方 → Proxy.outer/inner → Interceptor链 → Target方法

Target 内部:

java
public void outer() {
    this.inner();
}

此时已经进入 Target 对象方法,this 是目标对象,普通 Java 虚调用不会回到外层 Proxy:

mermaid
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 解决方案优先级

  1. 把 inner 拆到职责独立的另一个 Bean;
  2. 把切面边界放在真正外部入口;
  3. 注入自身代理,需处理循环依赖和设计可读性;
  4. AopContext.currentProxy(),需要 exposeProxy,耦合 Spring 且基于 ThreadLocal;
  5. 使用 AspectJ 编织,复杂度更高。

不要为了让注解生效随意把私有方法改 public;先确定它是否真是独立业务边界。

十四、exposeProxy 与 AopContext

启用 exposeProxy=true 后,代理调用期间把当前代理放入 ThreadLocal,目标方法可:

java
((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

java
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 不是代理、切点不匹配、过早实例化和异步上下文丢失;
  • 能设计不泄露敏感数据、不产生高基数和不阻塞业务的监控切面。