欢迎光临
我们一直在努力

深入浅出 Spring AOP:从切点表达式到动态代理原理

文章目录

    • 一、execution 切点表达式
      • 1. 语法格式
      • 2. 通配符规则
    • 二、AOP 基础概念
      • 1. AOP 是什么
      • 2. 核心术语
      • 3. 五种通知类型
      • 4. 多切面执行顺序
      • 5. 正常 / 异常时的执行流程
    • 三、Spring AOP 实现原理
      • 1. 代理模式:代理对象持有目标对象
      • 2. 两种动态代理方式
      • 3. SpringBoot 2.x 的代理选择规则
    • 四、全文总结
    • 五、核心知识点复盘
    • 六、常见问题 / 避坑指南

面向切面编程(AOP)是 Spring 里绕不开的核心能力。很多人会「照葫芦画瓢」地写切面,但对 execution 表达式到底怎么匹配、五种通知分别在什么时候执行、底层又是靠什么机制拦下来的,往往一知半解。

这篇文章会从最常用的切点表达式讲起,再串起 AOP 的基础概念,最后揭开动态代理的底层原理,配合可直接运行的完整代码,带你真正把 AOP 吃透。文中所有代码均来自一个可运行的 Spring Boot 工程。


一、execution 切点表达式

切点(Pointcut)决定了 AOP 到底要「拦」哪些方法。Spring AOP 里最常用、也最强大的切点表达式,就是 execution()。

1. 语法格式

execution() 用来精确匹配目标方法,完整语法如下:

execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

其中:

  • 访问修饰符(如 public、private)和 异常声明 两部分可以省略;
  • 返回类型、包名、类名、方法名、参数列表是核心,必须写清楚。

一个最典型的例子,也是本项目切面里实际用到的:

execution(* com.zmt.aop.controller.*.*(..))

拆开看,从左到右:

片段含义
第一个 * 任意返回类型
com.zmt.aop.controller 固定的包路径
第二个 * 该包下的任意类
第三个 * 类里的任意方法
(..) 任意个数、任意类型的参数

连起来读就是:匹配 com.zmt.aop.controller 包下,所有类的所有方法,不管返回什么、不管传什么参数。

2. 通配符规则

execution 表达式里有两个「万能」通配符,理解了它们就掌握了 90% 的切点写法。

(1)* —— 匹配任意单个元素

* 只能匹配「一段」,可以出现在返回类型、包名、类名、方法名、参数等位置:

* 匹配任意返回类型
com.zmt.aop.* 匹配任意一层包(aop 的下一级包)
com.zmt.aop.controller.* 匹配该包下的任意类
*.*(..) 任意类的任意方法

注意:包名里用 * 只代表一层包。例如 com.*.controller 只能匹配 com.zmt.controller 这种「两层」结构,不能匹配 com.a.b.controller 这种更深的路径。

(2).. —— 匹配多个连续的任意符号

.. 是「全能选手」,用在两个地方:

  • 包路径中:代表「当前包及其所有子包」;
  • 参数列表中:代表「任意个数、任意类型的参数」。

execution(* com.zmt.aop..*(..)) 匹配 aop 包及其所有子包里的任意方法
execution(* com.zmt..User.*(..)) 匹配 com.zmt 任意层级下 User 类的任意方法
execution(* *(..)) 匹配所有类的所有方法(慎用,范围过大)

常见组合示例:

// 1. 匹配 controller 包下任意类的任意方法(本项目切点)
execution(* com.zmt.aop.controller.*.*(..))

// 2. 匹配所有 public 方法
execution(public * *(..))

// 3. 匹配返回值为 String 的方法
execution(String com.zmt.aop.controller.*.*(..))

// 4. 匹配只有一个参数、参数类型为 String 的方法
execution(* com.zmt.aop.controller.*.*(String))

// 5. 匹配方法名以 save 开头的方法
execution(* com.zmt.aop..*.save*(..))

小结:记一句话——* 管「一段」,.. 管「一串」。包路径里 * 是一层,.. 是任意层;参数里 * 是一个,.. 是任意个。


二、AOP 基础概念

写切面之前,得先把 AOP 这套「行话」理顺,否则看别人的切面代码会一头雾水。

1. AOP 是什么

AOP(Aspect Oriented Programming,面向切面编程),和 OOP(面向对象编程)是互补关系,而不是替代关系。

OOP 擅长把业务按「对象」组织,但有些需求会横向贯穿很多类——比如:

  • 登录鉴权(几乎每个接口都要检查)
  • 统一返回数据格式(每个 Controller 都要包一层 Result)
  • 统一异常处理(每个方法都可能抛异常)
  • 方法耗时统计、日志打印、事务管理……

这些「和业务逻辑无关、但到处都要写」的代码,如果手写在每个方法里,会造成大量重复和耦合。AOP 的思路是:把这些共性逻辑集中到一个地方,统一处理,然后在需要的地方「自动织入」。

Spring 框架提供了 AOP 的拦截器技术(动态代理,第三节细讲),把这种思想落到了实处。日常开发里最典型的应用就是 Spring 的 @Transactional 事务、@Cacheable 缓存,它们的本质都是 AOP。

2. 核心术语

AOP 有几个绕不开的名词,逐个说清楚:

术语含义一句话解释
切面(Aspect) 封装共性逻辑的类,用 @Aspect 标识 「干活的模块」
切点(Pointcut) 匹配目标方法的规则,决定 AOP 作用范围 「在哪儿干活」
连接点(JoinPoint) 程序中被拦截的具体目标方法 「被干的活」
通知(Advice) 切面里具体执行的逻辑及其时机 「什么时候、干什么活」

用通俗类比再顺一遍:

切点 = 计算机专业的全体学生(一条筛选规则);连接点 = 具体的学生张三、李四(被规则命中的一个个实例);通知 = 9 月份开学(具体要做的事);切面 = 整个「开学」这件事的统一处理逻辑。

落到代码上,一个完整的切面长这样:

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect // 1. 声明这是一个切面
@Component // 2. 交给 Spring 容器管理
public class LogAspect {

// 3. 定义切点:匹配 controller 包下所有方法
@Pointcut("execution(* com.zmt.aop.controller.*.*(..))")
private void pt() {}

// 4. 通知:把具体逻辑绑定到这个切点上
@Before("pt()")
public void doBefore() {
System.out.println("前置通知:目标方法执行前调用");
}
}

  • @Aspect 标识切面类,@Component 让它被 Spring 扫描到(二者缺一不可);
  • @Pointcut 定义切点规则,方法体为空、只是承载表达式;
  • @Before("pt()") 里的 pt() 直接引用上面定义的切点,避免表达式重复书写。

3. 五种通知类型

通知的核心,是它在目标方法的哪个阶段执行。Spring AOP 一共支持 5 种:

通知类型执行时机是否受异常影响典型用途
@Before 目标方法执行前 不受影响 参数校验、权限检查
@AfterReturning 目标方法正常返回后 抛异常则不执行 记录成功结果
@AfterThrowing 目标方法抛出异常时 只有异常才执行 异常日志、告警
@After 目标方法执行后 无论如何都执行(类似 finally) 清理资源、收尾
@Around 目标方法执行前后 可控制是否执行目标方法 耗时统计、事务、缓存

重点区分三对容易混淆的:

  • @After vs @AfterReturning:@After 无论正常还是异常都会执行,@AfterReturning 只有正常返回才执行。可以理解为 @After 是「兜底」,@AfterReturning 是「庆功」。
  • @After vs @AfterThrowing:@After 异常时也执行,@AfterThrowing 只有异常时才执行。
  • @Around 最特殊:它既能做前置也能做后置,还能决定目标方法到底要不要执行,这是其他四种通知做不到的。

完整的五种通知示例:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo {

@Pointcut("execution(* com.zmt.aop.controller.*.*(..))")
private void pt() {}

// 环绕通知:方法执行前后都会执行,还能控制目标方法是否执行
@Around("pt()")
public Object doAround(ProceedingJoinPoint pjp) {
log.info("doAround 前");
Object result = null;
try {
// 关键:调用 proceed() 才会真正执行目标方法
result = pjp.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
log.info("doAround 后");
}
// 关键:必须把目标方法的返回值返回给调用方
return result;
}

@Before("pt()")
public void doBefore() {
log.info("doBefore …");
}

@After("pt()")
public void doAfter() {
log.info("doAfter …");
}

@AfterReturning("pt()")
public void doAfterReturning() {
log.info("doAfterReturning …");
}

@AfterThrowing("pt()")
public void doAfterThrowing() {
log.info("doAfterThrowing …");
}
}

两个必踩的坑(@Around 专属):

  • @Around 的参数必须是 ProceedingJoinPoint(其他通知用 JoinPoint),只有它能调用 proceed() 执行目标方法。忘了写 pjp.proceed(),目标方法根本不会执行。
  • @Around 方法的返回值会被当作目标方法的返回值传给调用方,必须 return 出去,否则调用方拿到的是 null。
  • 4. 多切面执行顺序

    一个方法可能同时被多个切面拦截,谁先谁后?用 @Order(数值) 控制,数字越小,优先级越高,越「靠外」执行。

    本项目用三个切面演示(AspectDemo2、AspectDemo3、AspectDemo4):

    // 优先级最高,最先执行前置、最后执行后置
    @Aspect
    @Component
    @Order(1)
    public class AspectDemo2 {
    @Before("execution(* com.zmt.aop.controller.*.*(..))")
    public void doBefore() { log.info("AspectDemo2 doBefore …"); }
    @After("execution(* com.zmt.aop.controller.*.*(..))")
    public void doAfter() { log.info("AspectDemo2 doAfter …"); }
    }

    // @Order(1)
    public class AspectDemo4 { /* 同上的 doBefore / doAfter */ }

    // @Order(5),优先级最低
    public class AspectDemo3 { /* 同上的 doBefore / doAfter */ }

    执行顺序是嵌套的,像洋葱一样一层套一层:

    AspectDemo2(-1) 前置
    AspectDemo4(1) 前置
    AspectDemo3(5) 前置
    目标方法
    AspectDemo3(5) 后置
    AspectDemo4(1) 后置
    AspectDemo2(-1) 后置

    可以看到:数字越小越先「进门」,越后「出门」。这个「嵌套」模型,和下文第三节「动态代理层层包裹」的实现是对应的。

    5. 正常 / 异常时的执行流程

    把所有通知放到一起,目标方法的完整生命周期如下。

    (1)方法正常执行(无异常):

    @Around 前 → @Before → 目标方法 → @AfterReturning → @After → @Around 后

    (2)方法抛出异常:

    @Around 前 → @Before → 目标方法(抛出异常) → @AfterThrowing → @After → @Around 后

    对比两个流程,规律很清晰:

  • @Before 永远在最前面(在目标方法之前);
  • @After 永远在最后面(目标方法之后),异常也跑不掉;
  • @AfterReturning 和 @AfterThrowing 是互斥的——要么正常返回走前者,要么抛异常走后者,不可能两个都执行;
  • @Around 把整个流程包了起来,最先开始、最后结束。
  • 一句话记忆:@Around 是「外壳」,@Before 打头阵,@After 收尾兜底,中间「返回」和「异常」二选一。


    三、Spring AOP 实现原理

    搞清楚「怎么写」之后,必须弄明白「为什么能拦下来」。Spring AOP 的底层就是动态代理,而动态代理又依托 Java 的反射机制。

    1. 代理模式:代理对象持有目标对象

    先看「代理」这个设计模式。核心是:调用方实际调用的是代理对象,代理对象在内部持有目标对象,由代理完成「切面逻辑 + 调用目标方法」的拼接。

    用「房屋中介」类比最形象:

    • 房东(目标对象)负责真正的出租/出售房子;
    • 中介(代理对象)在房东前后加上「带看、谈价、收中介费」等逻辑;
    • 租客(调用方)找的是中介,而不是直接找房东。

    先定义「房东」的接口和实现:

    // 接口:规定了中介和房东都要做的事
    public interface HouseSubject {
    void rentHouse();
    void saleHouse();
    }

    // 目标对象:真正的房东
    public class RealHouseSubject implements HouseSubject {
    @Override
    public void saleHouse() {
    System.out.println("我是房东,我出售房子");
    }
    @Override
    public void rentHouse() {
    System.out.println("我是房东,我出租房子");
    }
    }

    静态代理——手写一个代理类,固定实现接口、在方法前后写死逻辑:

    // 静态代理:代理类在编译期就写好了,和目标类一一对应
    public class HouseProxy implements HouseSubject {
    private HouseSubject realHouse; // 持有目标对象

    public HouseProxy(HouseSubject realHouse) {
    this.realHouse = realHouse;
    }

    @Override
    public void rentHouse() {
    System.out.println("我是中介,我开始代理"); // 前置增强
    realHouse.rentHouse(); // 调用目标方法
    System.out.println("我是中介,我结束代理"); // 后置增强
    }

    @Override
    public void saleHouse() {
    System.out.println("我是中介,我开始代理");
    realHouse.saleHouse();
    System.out.println("我是中介,我结束代理");
    }
    }

    静态代理的痛点很明显:每多一个目标类/接口,就要手写一个代理类;方法多了,每个都要重复写前后逻辑。所以有了动态代理。

    2. 两种动态代理方式

    动态代理的核心区别在于:代理类不是在编译期手写的,而是在运行时通过反射动态生成,因此能灵活适配任意目标类。Java 生态里有两种主流实现。

    (1)JDK 动态代理 —— 只能代理「实现了接口」的类

    JDK 自带,通过 java.lang.reflect.Proxy + InvocationHandler 实现。它生成的代理对象必须实现某个接口,调用方通过接口类型来使用代理。

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;

    public class Main {
    public static void main(String[] args) {
    // 目标对象
    HouseSubject target = new RealHouseSubject();

    // 动态生成代理对象(参数:类加载器、要实现的接口、处理器)
    HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
    target.getClass().getClassLoader(), // 类加载器
    new Class[]{HouseSubject.class}, // 目标实现的接口
    new JDKInvocationHandler(target) // 处理器
    );

    proxy.rentHouse();
    proxy.saleHouse();
    }
    }

    // 处理器:拦截所有方法调用,统一加前后逻辑
    class JDKInvocationHandler implements InvocationHandler {
    private Object target; // 持有目标对象

    public JDKInvocationHandler(Object target) {
    this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // proxy:代理对象;method:被调用的方法;args:方法参数
    System.out.println("我是代理,我开始代理");
    Object result = method.invoke(target, args); // 通过反射执行目标方法
    System.out.println("我是代理,我结束代理");
    return result;
    }
    }

    (2)Cglib 动态代理 —— 接口有无都能代理

    Cglib 通过生成目标类的子类来代理,因此不仅能代理实现接口的类,也能代理没有实现接口的普通类。Spring 内置了重打包的 Cglib(org.springframework.cglib),无需额外依赖。

    import org.springframework.cglib.proxy.Enhancer;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;

    public class Main {
    public static void main(String[] args) {
    HouseSubject target = new RealHouseSubject();

    // 通过 Enhancer 生成目标类的子类代理
    HouseSubject proxy = (HouseSubject) Enhancer.create(
    target.getClass(), // 目标类
    new CglibMethodInterceptor(target) // 拦截器
    );

    proxy.rentHouse();
    proxy.saleHouse();
    }
    }

    class CglibMethodInterceptor implements MethodInterceptor {
    private Object target;

    public CglibMethodInterceptor(Object target) {
    this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    System.out.println("我是代理,我开始代理");
    Object result = proxy.invoke(target, args); // 执行目标方法
    System.out.println("我是代理,我结束代理");
    return result;
    }
    }

    两者本质区别一句话总结:

    维度JDK 动态代理Cglib 动态代理
    实现原理 基于接口,生成接口实现类 基于继承,生成目标类的子类
    能否代理普通类 ❌ 必须实现接口 ✅ 接口有无均可
    性能 早期稍慢,如今差距不大 生成代理时稍慢,运行快
    限制 无法代理 final 类/方法、private 方法

    3. SpringBoot 2.x 的代理选择规则

    Spring 具体用哪种代理,由 proxyTargetClass 配置项控制(对应 @EnableAspectJAutoProxy 的 proxyTargetClass 属性):

    proxyTargetClass目标对象实现接口目标对象未实现接口
    false 使用 JDK 代理 使用 Cglib 代理
    true 使用 Cglib 代理 使用 Cglib 代理

    即:false 时「有接口优先 JDK,无接口才 Cglib」;true 时「无论有没有接口,统一用 Cglib」。

    补充一点:Spring Boot 2.x 开始,默认就开启了 proxyTargetClass = true,也就是默认统一走 Cglib 代理。所以现在大多数 Spring Boot 项目里,即使你的 Service 实现了接口,实际拦下来用的也是 Cglib。

    回到「房屋中介」的类比,把两种代理的区别对应起来:

    • 静态代理:类似某个房东固定签约的专属中介,每来一个房东都得专门配一个,死板;
    • 动态代理:类似平台上架房源后,任意中介都能带客户看房,运行时灵活生成、自动适配,这就是 AOP 能「无侵入」地给海量方法加逻辑的原因。

    四、全文总结

    这篇文章沿着「怎么匹配 → 怎么使用 → 为什么能生效」三条主线,把 Spring AOP 讲了一遍:

  • 切点表达式 execution 是 AOP 的「瞄准镜」,* 管一段、.. 管一串,掌握这俩通配符就能写出绝大多数切点;
  • AOP 概念:切面是模块、切点是规则、连接点是被拦截的方法、通知是执行的具体逻辑和时机,5 种通知在目标方法生命周期里各有位置;
  • 底层原理:AOP 靠动态代理实现,JDK 代理基于接口、Cglib 代理基于继承,Spring Boot 2.x 默认走 Cglib。
  • AOP 的价值在于「横向关注点集中处理」——把登录校验、统一返回、异常处理、耗时统计这类横切逻辑从业务代码里剥离出来,让业务方法只关心业务本身。理解它,是读懂 Spring 事务、缓存、日志切面等大量基础设施的前提。

    五、核心知识点复盘

    知识点核心结论
    execution 语法 execution(访问修饰符? 返回类型 包名.类名.方法(参数) 异常?),修饰符和异常可省略
    * 通配符 匹配任意单个元素;包路径里代表一层包
    .. 通配符 匹配任意连续元素;包路径里代表当前及所有子包,参数里代表任意参数
    五种通知 @Before 前、@After 后(必执行)、@AfterReturning 正常返回后、@AfterThrowing 异常时、@Around 环绕可拦截
    通知执行顺序 无异常:Around前→Before→方法→AfterReturning→After→Around后
    异常执行顺序 Around前→Before→方法(异常)→AfterThrowing→After→Around后
    多切面顺序 @Order 数字越小优先级越高,嵌套执行
    JDK vs Cglib JDK 基于接口,Cglib 基于继承(子类),Cglib 可代理无接口的类
    proxyTargetClass false 有接口走 JDK;true 统一 Cglib;Spring Boot 2.x 默认 true

    六、常见问题 / 避坑指南

  • 切面写了却不生效:九成是切点表达式写错——包路径拼错、类名/方法名大小写不符、漏了 * 或 ..。先用最宽的表达式(如 execution(* *(..)))验证切面本身没问题,再逐步收窄定位。

  • 忘了加 @Component 或没开启 AOP:切面类既要 @Aspect 也要 @Component 交给容器管理;纯 Spring 环境还需 @EnableAspectJAutoProxy(Spring Boot 自动配置一般已开启,可不用手动加)。

  • @Around 忘写 pjp.proceed() 或没 return:前者导致目标方法压根不执行,后者导致调用方拿到 null。@Around 必须用 ProceedingJoinPoint 作为参数。

  • 同类内部方法互调不生效:在一个类里 this.methodB() 调用本类的另一个方法,走的是 this 引用而不是代理对象,AOP 拦不到。这是 AOP 最经典、也最隐蔽的坑,需要通过注入代理对象或拆分 Service 等方式规避。

  • Cglib 拦不住 final/private/static 方法:Cglib 靠继承生成子类,final 类、final 方法、private 方法都无法被代理;JDK 代理则要求类必须实现接口。

  • @AfterReturning / @AfterThrowing 拿不到返回值或异常:想拿到目标方法的返回值或抛出的异常,需要配合 returning / throwing 属性显式绑定参数名,且通知方法参数名必须与之保持一致。

  • @After 和 @AfterReturning 搞混:@After 无论如何都执行(兜底),@AfterReturning 只有正常返回才执行。资源清理、收尾逻辑用 @After,成功后的业务处理用 @AfterReturning。

  • 赞(0)
    未经允许不得转载:171主机测评 » 深入浅出 Spring AOP:从切点表达式到动态代理原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址