Spring AOP 完整教程(中篇)

承接上篇 AOP 基础概念与计时入门案例,本篇为进阶核心内容,详细讲解 5 类通知执行时机、@Pointcut 复用切点、两种切点表达式语法、JoinPoint 连接点 API、多切面执行优先级控制,配套完整可运行代码、执行流程对比,是面试高频核心考点,下篇将结合 ThreadLocal 完成操作日志综合实战。

一、五大通知(Advice)类型全解

通知就是切面中封装的增强逻辑,根据执行时机分为 5 种,各自执行顺序、使用场景差异巨大。

1. @Around 环绕通知【重中之重,企业最常用】

  1. 执行时机:目标方法执行前、执行后都会执行;

  2. 独有特性:唯一可以手动控制原始方法是否执行;

  3. 必须调用 pjp.proceed() 才会执行目标业务方法;

  4. 方法返回值必须是 Object,用来接收目标方法返回的数据;

  5. 优势:可捕获异常、记录入参、返回值、计算耗时,一站式完成日志统计。完整示例代码:

    @Slf4j
    @Aspect
    @Component
    public class TimeAspect {
    @Around("execution(* com.itheima.service.impl..(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
    log.info("【环绕前置】方法即将执行");
    long start = System.currentTimeMillis();
    // 执行原始目标方法
    Object result = pjp.proceed();
    long end = System.currentTimeMillis();
    log.info("【环绕后置】方法执行完成,耗时{}ms", end - start);
    return result;
    }
    }

2. @Before 前置通知

目标方法运行之前执行,无法拦截、修改返回值,不能控制方法是否执行。适用场景:打印请求参数、权限校验前置判断。

复制代码
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void before(JoinPoint jp){
    log.info("【前置通知】准备调用方法:{}",jp.getSignature().getName());
}

3. @After 后置通知

目标方法执行完毕后执行,无论正常结束、抛出异常都会执行,类似代码块 finally。适用场景:统一资源释放、后置收尾打印。

4. @AfterReturning 返回通知

仅当目标方法无异常正常执行完毕才会触发;方法抛异常则不会执行。适用场景:记录接口正常返回数据。

5. @AfterThrowing 异常通知

只有目标方法抛出异常时才执行,正常走完不会触发。适用场景:全局异常日志记录、异常告警推送。

两种执行顺序演示

  1. 无异常完整执行顺序@Around 前置逻辑 → @Before → 目标方法执行 → @AfterReturning → @After → @Around 后置逻辑
  2. 目标方法抛出异常顺序@Around 前置逻辑 → @Before → 方法抛异常 → @AfterThrowing → @After

注:环绕通知内可手动 try-catch 捕获异常,阻断异常向外抛出。

二、@PointCut 抽取公共切点表达式

使用场景

项目多处切面复用同一套匹配规则,重复书写 execution 表达式冗余,使用@Pointcut注解抽取统一切点,一处定义多处引用。

代码示例

复制代码
@Slf4j
@Aspect
@Component
public class TimeAspect {
    // 抽取公共切点,匹配所有业务层方法
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    public void servicePoint(){}

    // 直接引用切点方法名,无需重复写表达式
    @Around("servicePoint()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object res = pjp.proceed();
        log.info("执行耗时:{}ms",System.currentTimeMillis()-start);
        return res;
    }

    @Before("servicePoint()")
    public void before(JoinPoint jp){
        log.info("即将执行方法:{}",jp.getSignature().getName());
    }
}

访问权限说明

  1. private 修饰:仅当前切面类内部可以引用;
  2. public 修饰:其他切面类也能引用该切点。

三、两种主流切入点表达式

方式 1:execution 方法匹配表达式(全量拦截专用)

根据包、类、方法名、返回值、参数精准匹配方法,是入门计时案例使用的表达式。

完整标准语法
复制代码
execution(访问修饰符? 返回值 包名.类名.?方法名(参数) throws 异常?)

?代表可省略内容;访问修饰符默认不写(匹配 public)。

两大通配符规则
  1. *:单个任意匹配;可匹配返回值、包名、类名、单个参数;
  2. ..:多层连续匹配;可匹配多级包、任意数量任意参数。
常用匹配示例
复制代码
// 匹配itheima下所有service包所有类所有方法
execution(* com.itheima..service.*.*(..))
// 匹配controller下所有save/update/delete开头的增删改接口
execution(* com.itheima.controller.*.save*(..)) ||
execution(* com.itheima.controller.*.update*(..)) ||
execution(* com.itheima.controller.*.delete*(..))
// 匹配返回值void、无参数方法
execution(void com.itheima.service.impl.*.*())
书写规范建议
  1. 缩小匹配范围,尽量不用..泛匹配全项目,减少拦截性能损耗;
  2. 优先匹配接口而非实现类,拓展性更强。

方式 2:@annotation 注解匹配(按需拦截专用)

自定义标记注解,仅拦截添加该注解的方法,适合少量接口单独记录日志场景。

完整使用流程
  1. 自定义标记注解

    import java.lang.annotation.*;
    @Target(ElementType.METHOD) // 仅作用在方法
    @Retention(RUNTIME)
    public @interface LogRecord {}

  2. Controller 接口添加注解标记

    @PostMapping("/dept/save")
    @LogRecord
    public Result save(Dept dept){
    deptService.save(dept);
    return Result.success();
    }

  3. 切面切点匹配注解

    @Pointcut("@annotation(com.itheima.anno.LogRecord)")
    public void logPoint(){}

两种表达式选型对比

  1. 批量拦截整个包全部业务方法 → 选用 execution;
  2. 仅少量接口需要增强,灵活按需开启 → 选用 @annotation。

四、JoinPoint 连接点 API 获取方法信息

Spring 提供 JoinPoint 对象封装目标方法全部信息,不同通知使用对象有区分:

  1. ProceedingJoinPoint:仅 @Around 环绕通知可用,独有proceed()执行目标方法;
  2. Join:@Before/@After/@AfterReturning/@AfterThrowing 通用,无执行方法 API。

通用核心方法(两类对象均可调用)

复制代码
// 获取目标类完整类名
String className = jp.getTarget().getClass().getName();
// 获取方法签名(方法名、返回值)
Signature sig = jp.getSignature();
String methodName = sig.getName();
// 获取调用时传入的所有参数
Object[] args = jp.getArgs();

环绕通知独有方法

复制代码
// 执行原始业务方法,必须调用否则目标逻辑不会执行
Object result = pjp.proceed();

完整代码示例

复制代码
@Around("servicePoint()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // 提取方法信息
    String className = pjp.getTarget().getClass().getName();
    String method = pjp.getSignature().getName();
    Object[] params = pjp.getArgs();
    log.info("执行类:{},方法:{},入参:{}",className,method,params);
    Object res = pjp.proceed();
    return res;
}

五、多切面执行顺序控制

项目中存在多个切面类(计时切面、日志切面、权限切面),同时匹配同一个目标方法时,执行顺序有默认规则,也可手动自定义优先级。

1. 默认排序规则

切面类名字母自然排序:

  • 前置通知:字母靠前的切面先执行;
  • 后置通知:字母靠前的切面后执行。

2. @Order 注解手动控制优先级

切面类添加@Order(数字),数字越小优先级越高;

  • 前置逻辑:数字小先执行;

  • 后置 / 异常逻辑:数字小后执行。示例:

    @Order(1) // 优先级更高,前置先执行
    @Aspect
    @Component
    public TimeAspect {}

    @Order(10)
    @Aspect
    @Component
    public LogAspect {}

执行流程举例

TimeAspect(Order1)、LogAspect(Order10)无异常场景:

  1. Time 环绕前置 → Log 环绕前置
  2. Time 前置 → Log 前置
  3. 执行目标方法
  4. Log 返回通知 → Time 返回通知
  5. Log 后置 → Time 后置
  6. Log 环绕后置 → Time 环绕后置

中篇全文总结

  1. 五大通知:@Around (核心)、@Before、@After、@AfterReturning、@AfterThrowing,区分有无异常的执行顺序;
  2. @Pointcut 抽取公共切点表达式,简化多处重复切点代码;
  3. 两种切点:execution 按包 / 方法批量匹配,@annotation 注解按需拦截;
  4. JoinPoint 获取类名、方法、参数,ProceedingJoinPoint 仅环绕可用,可执行目标方法;
  5. 多切面默认按类名字母排序,@Order 数字越小优先级越高。

中篇拓展实操练习

  1. 编写切面,同时实现 5 类通知,分别打印日志,观察有无异常两种执行顺序;
  2. 使用 @Pointcut 抽取 service 切点,分别在 @Before、@Around 复用;
  3. 自定义 @Log 注解,使用注解切点只拦截新增接口;
  4. 创建两个切面,添加 @Order 修改执行顺序,控制台打印验证。

中篇面试高频考点

  1. 五种通知分别在什么时机执行,异常场景执行差异;
  2. @Around 和其他通知最大区别是什么?
  3. execution 表达式中*..通配符含义;
  4. JoinPoint 和 ProceedingJoinPoint 区别、各自使用场景;
  5. 多个切面如何调整执行优先级。