Spring AOP 核心概念与通知类型详解

目录

[1. 概念推导:切面是如何运转的?](#1. 概念推导:切面是如何运转的?)

[2. 流程推演:五种通知类型的执行轨迹](#2. 流程推演:五种通知类型的执行轨迹)

[流程推演 1:目标方法正常执行(/test/t1)](#流程推演 1:目标方法正常执行(/test/t1))

[流程推演 2:目标方法发生异常(/test/t2,代码为 int a = 10 / 0;)](#流程推演 2:目标方法发生异常(/test/t2,代码为 int a = 10 / 0;))

核心注意事项

[3. 流程优化:重复表达式的痛与 @Pointcut](#3. 流程优化:重复表达式的痛与 @Pointcut)

[4. 流程优化:多切面时的执行顺序与 @Order](#4. 流程优化:多切面时的执行顺序与 @Order)


系列导航:上一篇跑通了第一个 AOP 程序,本篇拆解 AOP 的核心术语,并重点推演正常和异常两种流程下,五种通知类型的执行顺序。

1. 概念推导:切面是如何运转的?

AOP 的执行逻辑可以概括为三步:找准目标 → 施加增强 → 组成切面。

  1. 怎么找准目标?

    我们需要制定一个规则,例如"拦截 controller 包下的所有方法"。这个规则叫切点(Pointcut)。

  2. 目标是哪些?

    被规则命中的真实方法,例如 queryBookById,叫连接点(JoinPoint)。切点可以理解为保存了众多连接点的集合。

  3. 施加什么增强?

    拦截到目标方法后要做的工作,例如统计耗时,叫通知(Advice)。

  4. 组合起来叫什么?

    切点 + 通知 = 切面(Aspect)。切面既告诉程序"对谁做",也告诉程序"做什么"和"什么时候做"。

Spring AOP 核心概念关系图

2. 流程推演:五种通知类型的执行轨迹

Spring 提供了 5 种通知类型:

通知类型 含义
@Around 环绕通知,目标方法前后都执行
@Before 前置通知,目标方法前执行
@AfterReturning 返回后通知,正常返回后执行,有异常不执行
@AfterThrowing 异常后通知,目标方法抛异常后执行
@After 后置通知,目标方法后执行,无论是否异常都执行,类似 finally

为了看清执行顺序,我们写一个包含 5 种通知的切面 AspectDemo,拦截 TestController。

全套演示核心代码(AspectDemo.java)

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo {

    // 公共切点:抽取表达式,所有通知复用
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}

    @Before("pt()")
    public void doBefore(JoinPoint joinPoint){
        log.info("@Before 前置通知执行");
    }

    @AfterReturning("pt()")
    public void doAfterReturning(JoinPoint joinPoint){
        log.info("@AfterReturning 返回通知执行");
    }

    @AfterThrowing("pt()")
    public void doAfterThrowing(JoinPoint joinPoint){
        log.info("@AfterThrowing 异常通知执行");
    }

    @After("pt()")
    public void doAfter(JoinPoint joinPoint){
        log.info("@After 后置通知执行");
    }

    @Around("pt()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("@Around 环绕【前置】逻辑");
        Object result = null;
        try {
            // 执行目标方法
            result = pjp.proceed();
        } catch (Throwable throwable) {
            throw throwable;
        }
        log.info("@Around 环绕【后置】逻辑");
        return result;
    }
}

测试 Controller 代码 TestController.java

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    // 正常执行接口
    @GetMapping("/t1")
    public String t1(){
        log.info("===== 目标方法 t1 正在执行 =====");
        return "success";
    }

    // 会抛出异常的接口
    @GetMapping("/t2")
    public String t2(){
        log.info("===== 目标方法 t2 正在执行 =====");
        int a = 10 / 0;
        return "success";
    }
}

流程推演 1:目标方法正常执行(/test/t1)

  1. 请求进入,首先触发 @Around 的前置逻辑。

  2. 接着触发 @Before。

  3. 执行目标方法 t1()。

  4. 目标方法正常返回,触发 @AfterReturning。

  5. 无论是否异常都要收尾,触发 @After。

  6. 最后回到 @Around,执行后置逻辑。

正常运行时,@AfterThrowing 不会执行。

正常执行时的通知顺序

流程推演 2:目标方法发生异常(/test/t2,代码为 int a = 10 / 0;)

  1. 请求进入,触发 @Around 的前置逻辑。

  2. 接着触发 @Before。

  3. 执行目标方法 t2(),抛出 ArithmeticException。

  4. 异常被捕获,触发 @AfterThrowing。

  5. 依然要执行收尾工作,触发 @After。

  6. 因为 pjp.proceed() 抛出异常,@Around 的后置逻辑不再执行,@AfterReturning 也不会执行。

异常执行时的通知顺序

核心注意事项

  • @Around 的返回值必须是 Object,因为它要接收 pjp.proceed() 的返回值;否则目标方法即使有返回值,也无法传给调用方。

  • 除 @Around 外,其他四个通知都只是"旁观者",不需要在参数中写 ProceedingJoinPoint,它们无需控制目标方法执行。

  • @Around 需要调用 proceed() 让原始方法执行;其他通知不需要考虑目标方法执行。

3. 流程优化:重复表达式的痛与 @Pointcut

痛点 :上面的 5 个通知,每个注解里都写了一长串 execution(* com.example.demo.controller.*.*(..))。如果哪天要改拦截规则,得改 5 次。

方案 :用 @Pointcut 提取公共切点表达式。

java 复制代码
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){} // 定义切点

@Before("pt()") // 引用切点
public void doBefore() { ... }

流程变化:Spring 解析时,会先找到 pt() 这个空方法,拿到里面的表达式去匹配,再把匹配结果应用到 doBefore 等通知上。

注意:@Pointcut 方法使用 private 时,仅当前切面类可用;其他切面类要引用时,需要改为 public,引用方式为 全限定类名.方法名()。

4. 流程优化:多切面时的执行顺序与 @Order

痛点 :如果 AspectDemo2、AspectDemo3 都拦截了同一个方法,谁先执行?

默认规则 :Spring 默认按切面类名的字母排序。@Before 字母靠前的先执行,@After 字母靠前的后执行。

问题:类名不可控,逻辑容易混乱。

方案 :使用 @Order(数字)。数字越小,优先级越高。

  • @Before 阶段:优先级高的切面先执行。

  • @After 阶段:优先级高的切面后执行,因为它被包在最外层。

这就像穿衣服和脱衣服:先穿的最晚脱。

@Order 的洋葱模型

相关推荐
vipxieliang14 分钟前
ValidX vs 手写验证代码:开发效率对比实测
java·spring boot·后端
vipxieliang25 分钟前
Go GC 性能优化实践:从 GOGC、GOMEMLIMIT 到分配优化
后端·go
海豚调度31 分钟前
Apache DolphinScheduler 2026 年 10 月社区贡献总结报告
后端
vipxieliang33 分钟前
ThinkORM 3.0 入门:强类型模型定义与查询构造器
后端·php·thinkphp
vipxieliang34 分钟前
虚拟线程与 GraalVM:Spring Boot 3.x 云原生性能跃迁
java·spring boot·后端
Rain的Java大神之路34 分钟前
面试官最烦的答案:“我用 Redis + Token”!接口幂等 8 种方案 + 四层防御,把重复扣款工单干到 0
java·后端·面试
vipxieliang1 小时前
PHP 密码哈希与 password_hash 最佳实践
后端·php
量化分析码农1 小时前
【Python量化波动率实战 03】波动大就少拿点:波动率目标仓位的回测实证
后端
Rainmicro1 小时前
【无标题】
java·数据库·算法