目录
正在加载目录…
专栏文章
专栏文章
Spring Framework 系列
1. Spring 容器启动:refresh 流程与源码解析 2. Spring 三级缓存:循环依赖与 AOP 代理 3. Spring 事务:传播机制、执行链路与常见陷阱 4. Spring IoC:Bean 生命周期与扩展机制 5. Spring AOP:动态代理、通知链与 AspectJ 6. Spring 术语速查:核心概念与常用缩写

Spring AOP:动态代理、通知链与 AspectJ

发布于 2026-05-26 09:43 · 最后编辑于 2026-07-31 15:53 · 字数 3,151 👁 71 次阅读

本文从 Kiczales 1997 年提出 AOP 范式出发,阐明"横切关注点"问题的本质,逐层深入 Spring AOP 的代理机制、Advice 执行链路源码,并厘清 Spring AOP 与 AspectJ 的边界。

目录

章节说明
理论基础AOP 编程范式起源、横切关注点、核心术语
Spring AOP vs AspectJ两者定位、织入时机、能力边界
实现原理JDK 动态代理 vs CGLIB,选择策略
源码链路代理创建 → Advice 责任链执行完整路径
通知类型与执行顺序5 种通知、单切面/多切面执行顺序
切入点表达式execution / @annotation / within 等
常见坑失效场景与根因

理论基础

横切关注点问题

1997 年,Gregor Kiczales 等人在施乐 PARC 研究中心提出 AOP(发表于 ECOOP '97),动机是解决 OOP 的一个固有局限:横切关注点(crosscutting concerns)无法被 OOP 干净地模块化

典型的横切关注点:

日志记录    安全鉴权    事务管理    缓存    监控/埋点    异常处理

用 OOP 实现时,这些关注点会导致两种问题:

问题说明
代码散射(Scattering)同一段日志/安全逻辑分散在数百个类中,改动一处需改动所有地方
代码纠缠(Tangling)业务逻辑与日志/事务代码混在一个方法里,职责不清晰

AOP 的解法:将横切关注点抽取为独立的切面(Aspect),通过织入(Weaving) 机制在不修改业务代码的前提下将其应用到目标位置。

"AOP complements OOP by providing another way of thinking about program structure." — Spring Framework 官方文档

核心术语

术语英文说明
切面Aspect横切关注点的模块化封装,包含 Pointcut + Advice
连接点Join Point程序执行中可被拦截的点(Spring AOP 仅支持方法执行这一种)
切入点Pointcut连接点的选择谓词("哪些方法"),用表达式描述
通知Advice在切入点处执行的动作("做什么"),分 Before/After/Around 等
目标对象Target被代理的原始对象
代理Proxy包裹目标对象的增强对象,AOP 的实际载体
织入Weaving将切面应用到目标的过程
引介Introduction为现有类动态添加新接口/方法(Spring AOP 支持,较少用)

织入时机的三种选择

编译时织入(Compile-time weaving)
  → AspectJ 编译器(ajc)在编译阶段修改字节码
  → 性能最好,但需特殊编译流程

加载时织入(Load-time weaving, LTW)
  → JVM 加载类时通过 Java Agent 修改字节码
  → AspectJ 支持(-javaagent:aspectjweaver.jar)

运行时织入(Runtime weaving)
  → 通过动态代理在运行时创建增强对象 ← Spring AOP 采用此方式
  → 无需修改字节码,纯 Java 实现,灵活但有局限

Spring AOP vs AspectJ

维度Spring AOPAspectJ
织入方式运行时动态代理编译时 / 加载时 / 运行时
Join Point仅方法执行方法调用、字段读写、构造器、静态初始化等
依赖纯 Spring,无需额外工具需要 AspectJ 编译器(ajc)或 Weaving Agent
性能每次调用经过代理链,略有开销字节码织入,接近原生性能
自调用⚠️ 无法拦截(同类内部调用绕过代理)✅ 可以拦截
final 类/方法⚠️ CGLIB 无法代理✅ 可以织入
适用场景企业应用中 80% 的 AOP 需求需要字段级拦截或高性能的场景

Spring 的选择:Spring AOP 使用 AspectJ 的注解语法@Aspect@Pointcut@Before 等),但底层是自己的动态代理实现,而非 AspectJ 织入器。两者共用语法,实现独立。

实现原理

JDK 动态代理

// JdkDynamicAopProxy 实现 InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取该方法对应的拦截器链
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    if (chain.isEmpty()) {
        // 无拦截器,直接调用目标方法
        return AopUtils.invokeJoinpointUsingReflection(target, method, args);
    }
    // 有拦截器,构建责任链
    MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
    return invocation.proceed();
}

条件:目标类实现了至少一个接口。代理类实现相同接口,Proxy.newProxyInstance() 生成。

CGLIB 代理

CGLIB 通过 ASM 操作字节码,生成目标类的子类,覆盖父类方法并在其中嵌入拦截逻辑。

限制

  • 目标类不能是 final(无法继承)
  • 目标方法不能是 final / private(无法覆盖)
  • 构造器会被调用两次(生成子类 + 实例化)

选择策略

// DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) {
    if (config.isOptimize() || config.isProxyTargetClass()
            || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config); // 目标本身是接口/代理类 → JDK
        }
        return new ObjenesisCglibAopProxy(config); // 否则 → CGLIB
    }
    return new JdkDynamicAopProxy(config); // 有接口且不强制 CGLIB → JDK
}

Spring Boot 2.x 起默认 spring.aop.proxy-target-class=true,强制 CGLIB。

源码链路

代理创建:BeanPostProcessor 阶段

Bean 初始化完成
  → AbstractAutoProxyCreator#postProcessAfterInitialization()
  → wrapIfNecessary(bean, beanName, cacheKey)
  → getAdvicesAndAdvisorsForBean()   // 找所有匹配该 Bean 的 Advisor
  → createProxy(beanClass, beanName, advisors, targetSource)
  → ProxyFactory#getProxy(classLoader)
  → DefaultAopProxyFactory#createAopProxy()  // 决定 JDK / CGLIB
  → 返回代理对象,替换容器中的原始 Bean

Advisor 匹配过程

// AbstractAdvisorAutoProxyCreator#findEligibleAdvisors(精简)
// 1. 从容器获取所有 Advisor Bean
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// 2. 过滤出能匹配当前 Bean 的 Advisor
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
// 3. 排序(@Order / PriorityOrdered)
sortAdvisors(eligibleAdvisors);

Advice 执行:责任链模式

代理方法调用
  → ReflectiveMethodInvocation#proceed()
  → 遍历 interceptorList(MethodInterceptor 列表)
     ├── ExposeInvocationInterceptor(暴露 invocation 到 ThreadLocal)
     ├── AspectJAroundAdvice(Around → 调用 proceed() 推进链)
     ├── MethodBeforeAdviceInterceptor(Before)
     ├── AspectJAfterAdvice(After,finally 块执行)
     ├── AfterReturningAdviceInterceptor(AfterReturning)
     └── AspectJAfterThrowingAdvice(AfterThrowing)
  → 所有 interceptor 走完后调用目标方法

责任链用递归索引推进(currentInterceptorIndex++),Around 通知通过调用 joinPoint.proceed() 驱动链条继续。

通知类型与执行顺序

5 种通知

注解执行时机能获取返回值能修改返回值能捕获异常
@Before目标方法执行前
@AfterReturning正常返回后❌(只能读)
@AfterThrowing抛出异常后✅(只能读)
@After方法退出后(finally)
@Around完全包裹目标方法

@Around 最强大,可以修改参数、返回值,吞掉或替换异常。@Before / @After 只能"观察",无法改变结果。

单切面内执行顺序(Spring 5.2.7+ 修正版)

正常执行:
  @Around(前半)→ @Before → 目标方法 → @Around(后半)→ @AfterReturning → @After

异常执行:
  @Around(前半)→ @Before → 目标方法(抛异常)→ @AfterThrowing → @After

⚠️ Spring 5.2.7 之前,同一切面内 @AfterReturning@After 的顺序与上述相反(@After 先于 @AfterReturning),5.2.7 修正为更符合直觉的顺序。

多切面执行顺序

多个切面作用于同一方法时,按 @Order 值排序,值越小优先级越高:

@Order(1) 切面 Around 前
  @Order(2) 切面 Around 前
    目标方法
  @Order(2) 切面 Around 后 / AfterReturning / After
@Order(1) 切面 Around 后 / AfterReturning / After

外层切面先进后出(类似 Filter 链)。不指定 @Order 时顺序不确定,多切面务必显式指定。

切入点表达式

execution(最常用)

execution([访问修饰符] 返回类型 [类路径.]方法名(参数类型) [throws 异常])

// 示例
execution(* com.example.service.*.*(..))          // service 包下所有方法
execution(public * com.example..*.save*(..))       // 所有 save 开头的 public 方法
execution(* *(..) throws IOException)              // 声明抛出 IOException 的方法

通配符:

  • * 匹配任意单个元素(单个包名、方法名、类型)
  • .. 匹配任意多个(多级包路径、任意参数列表)

其他常用表达式

表达式说明示例
@annotation(ann)方法上有指定注解@annotation(com.example.Log)
@within(ann)类上有指定注解(该类所有方法)@within(org.springframework.stereotype.Service)
within(type)指定类型内的方法within(com.example.service.*)
bean(name)指定 Spring Bean 名称bean(orderService)
args(types)参数类型匹配args(java.io.Serializable)
@args(ann)参数有指定注解@args(com.example.Valid)

切入点复合

@Aspect
@Component
public class LogAspect {

    // 定义可复用的切入点
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    @Pointcut("@annotation(com.example.annotation.Log)")
    public void logAnnotated() {}

    // 复合:service 层 且 有 @Log 注解
    @Around("serviceLayer() && logAnnotated()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        // ...
        return pjp.proceed();
    }
}

常见坑

#现象根因解决方案
1同类内部方法调用切面失效this 调用绕过代理对象注入自身 Bean 调用,或改用 AspectJ LTW
2private / final 方法切面失效CGLIB 无法覆盖 private/final 方法改为 public,或用 AspectJ
3接口上的注解切面失效(CGLIB 模式)CGLIB 代理的是类,读不到接口注解注解加在实现类/方法上,或切换 JDK 代理
4多切面顺序混乱未指定 @Order所有切面显式加 @Order(n)
5@Around 忘记调用 proceed()目标方法不会执行必须调用 pjp.proceed() 并返回结果
6@AfterReturning 拿不到返回值未绑定 returning 参数@AfterReturning(returning = "result")
7@Transactional + 自定义切面顺序错误事务切面和自定义切面 @Order 冲突事务切面默认 Integer.MAX_VALUE,自定义切面设更大值则在事务外

坑 1 详解:自调用失效

@Service
public class OrderService {

    // ❌ this.audit() 直接调用,绕过代理,@Log 切面不触发
    public void placeOrder() {
        this.audit("place");
    }

    @Log
    public void audit(String action) { ... }
}

// ✅ 注入自身
@Service
public class OrderService {

    @Autowired
    private OrderService self;

    public void placeOrder() {
        self.audit("place"); // 通过代理调用,切面生效
    }

    @Log
    public void audit(String action) { ... }
}

坑 5 详解:@Around 必须 proceed

// ❌ 忘记 proceed,目标方法永远不会执行
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
    log.info("before");
    // 漏了 pjp.proceed()!
    return null;
}

// ✅ 正确写法
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    log.info("before: {}", pjp.getSignature());
    Object result = pjp.proceed(); // 推进责任链/执行目标方法
    log.info("after: {}", result);
    return result; // 必须返回,否则调用方拿到 null
}

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论