首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【SpringBoot】Spring AOP 核心概念 && 通知类型 && @PointCut && 切面优先级 @Order && 切点表达式

【SpringBoot】Spring AOP 核心概念 && 通知类型 && @PointCut && 切面优先级 @Order && 切点表达式

原创
作者头像
lirendada
发布2026-07-31 16:27:40
发布2026-07-31 16:27:40
1080
举报
文章被收录于专栏:JavaJava

Spring AOP 详解

一、Spring AOP 核心概念

概念

含义

切面(Aspect)

横切逻辑的模块(一个类),切面 = 切点 + 通知 + 连接点

通知(Advice)

就是具体要做的工作,比如上面的 "记录代码耗时"

切点(Pointcut)

指定哪些方法要被拦截 比如 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式

连接点(JoinPoint)

满足切点表达式规则的方法,就是连接点比如切点表达式指定的 com.example.demo.controller 路径下的方法,都是连接点

切点和连接点的关系:

  • 连接点是满足切点表达式的元素
  • 切点可以看做是保存了众多连接点的一个集合

比如:

  • 切点表达式:学校全体教师
  • 连接点:张三、李四等各个老师

二、通知类型

注解

含义

@Before

在目标方法执行前执行

@After

在目标方法执行后(有异常也会执行)

@AfterReturning

在方法返回后执行(有异常不会执行)

@AfterThrowing

在方法抛出异常时执行

@Around

环绕通知,可控制方法执行前后时机,最强大

代码语言:javascript
复制
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 {
    // 前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore() {
        log.info("执行 Before 方法");
    }
    
    // 后置通知
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("执行 After 方法");
    }
    
    // 返回后通知
    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("执行 AfterReturning 方法");
    }
    
    // 抛出异常后通知
    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执行 doAfterThrowing 方法");
    }
    
    // 添加环绕通知
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around 方法开始执行");
        Object result = joinPoint.proceed();
        log.info("Around 方法结束执行");
        return result;
    }
}

写一些测试程序:

代码语言:javascript
复制
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1";
    }
    
    @RequestMapping("/t2")
    public boolean t2() {
        int a = 10 / 0;
        return true;
    }
}
  1. 正常运行的情况:http://127.0.0.1:8080/test/t1

观察日志:

  • 程序正常运行的情况下,@AfterThrowing 标识的通知方法不会执行
  • @Around 标识的通知方法包含两部分,一个 "前置逻辑",一个 "后置逻辑"。其中 "前置逻辑" 先于 @Before 标识的通知方法执行"后置逻辑" 晚于 @After 标识的通知方法执行

2. 异常时的情况:http://127.0.0.1:8080/test/t2

观察日志:

程序发生异常的情况下:

  • @AfterReturning 标识的通知方法在出现异常时候不会执行
  • @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)

💥注意事项:

  • @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,而其它通知类型不需要考虑目标方法的执行。
  • @Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
  • 一个切面类可以有多个切点。

三、@PointCut

上面代码存在一个问题,就是存在大量重复的切点表达式 execution(* com.example.demo.controller.*.*(..)),Spring 提供了 @PointCut 注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可

上述代码就可以修改为:

代码语言:javascript
复制
@Slf4j
@Aspect
@Component
public class AspectDemo {
    // 定义切点(公共的切点表达式)
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    // 前置通知
    @Before("pt()")
    public void doBefore() {
        //...代码省略
    }
    
    // 后置通知
    @After("pt()")
    public void doAfter() {
        //...代码省略
    }
    
    // 返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        //...代码省略
    }
    
    // 抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        //...代码省略
    }
    
    // 添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        //...代码省略
    }
}
  • 当切点定义使用 private 修饰时,仅能在当前切面类中使用。
  • 当其他切面类也要使用当前切点定义时,就需要把 private 改为 public,引用方式为:全限定类名.方法名()
    代码语言:javascript
    复制
    @Slf4j
    @Aspect
    @Component
    public class AspectDemo2 {
        @Before("com.example.demo.aspect.AspectDemo.pt()") // 引用别的文件中的切点!
        public void doBefore() {
            log.info("执行 AspectDemo2 -> Before 方法");
        }
    }

四、切面优先级 @Order

当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?

AspectDemo2

代码语言:javascript
复制
@Component
public class AspectDemo2 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo2 -> Before 方法");
    }
    
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo2 -> After 方法");
    }
}

AspectDemo3

代码语言:javascript
复制
@Component
public class AspectDemo3 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo3 -> Before 方法");
    }
    
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo3 -> After 方法");
    }
}

AspectDemo4

代码语言:javascript
复制
@Component
public class AspectDemo4 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo4 -> Before 方法");
    }
    
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo4 -> After 方法");
    }
}

运行程序,访问接口:http://127.0.0.1:8080/test/t1

观察日志:

通过上述程序的运行结果,可以看出:

存在多个切面类时,默认按照切面类的类名字母排序:

  • @Before 通知:字母排名靠前的先执行
  • @After 通知:字母排名靠前的后执行

但这种方式不方便管理,我们的类名更多还是具备一定含义的。

所以 Spring 提供了一个新的注解,控制这些切面通知的执行顺序@Order

代码语言:javascript
复制
@Aspect
@Component
@Order(2)
public class AspectDemo2 {
    //...代码省略
}

@Aspect
@Component
@Order(1)
public class AspectDemo3 {
    //...代码省略
}

@Aspect
@Component
@Order(3)
public class AspectDemo4 {
    //...代码省略
}

重新运行程序,访问接口:http://127.0.0.1:8080/test/t1

观察日志:

@Order 注解标识的切面类,执行顺序如下:

  • @Before 通知:数字越小先执行
  • @After 通知:数字越大先执行

总结:@Order 控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。

五、切点表达式

切点表达式常见有两种表达方式

  1. execution():根据方法的签名来匹配
  2. @annotation():根据注解匹配

1. execution表达式

execution() 是最常用的切点表达式,用来匹配方法,语法为:

代码语言:javascript
复制
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

其中【访问修饰符】和【异常】可以省略!

切点表达式支持通配符表达:

  1. *匹配任意字符,只匹配一个元素(返回类型、包、类名、方法、方法参数)
    1. 包名使用 * 表示任意包(一层包使用一个 *
    2. 类名使用 * 表示任意类
    3. 返回值使用 * 表示任意返回值类型
    4. 方法名使用 * 表示任意方法
    5. 参数使用 * 表示一个任意类型的参数
  2. ..匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
    1. 使用 .. 配置包名,标识此包以及此包下的所有子包
    2. 可以使用 .. 配置参数,任意个任意类型的参数

切点表达式示例:

TestController 下的 public 修饰,返回类型为 String 方法名为 t1 的无参方法:

代码语言:javascript
复制
execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符:

代码语言:javascript
复制
execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型:

代码语言:javascript
复制
execution(* com.example.demo.controller.TestController.t1())

匹配 TestController 下的所有无参方法:

代码语言:javascript
复制
execution(* com.example.demo.controller.TestController.*())

匹配 TestController 下的所有方法:

代码语言:javascript
复制
execution(* com.example.demo.controller.TestController.*(..))

匹配 controller 包下所有的类的所有方法:

代码语言:javascript
复制
execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的 TestController

代码语言:javascript
复制
execution(* com..TestController.*(..))

匹配 com.example.demo 包下,子孙包下的所有类的所有方法:

代码语言:javascript
复制
execution(* com.example.demo..*(..))

2. @annotation自定义注解

execution() 更适用有规则的,如果我们要匹配多个无规则的方法呢,比如:TestController 中的 t1()UserController 中的 u1() 两个方法,这个时候使用 execution() 这种切点表达式来描述就不是很方便了.

我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点!

实现步骤:

  1. 自定义注解
  2. 使用 @annotation 表达式来描述切点
  3. 在连接点的方法上添加自定义注解

首先提供测试用例:

代码语言:javascript
复制
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1";
    }
    
    @RequestMapping("/t2")
    public boolean t2() {
        return true;
    }
}
代码语言:javascript
复制
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1(){
        return "u1";
    }
    
    @RequestMapping("/u2")
    public String u2(){
        return "u2";
    }
}
① 自定义注解
代码语言:javascript
复制
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {

}
  1. @Target:标识 Annotation 所修饰的对象范围,即该注解可以用在什么地方。
  2. 常用取值:
  • ElementType.TYPE:用于描述类、接口或 enum 声明
  • ElementType.METHOD:描述方法
  • ElementType.PARAMETER:描述参数
  • ElementType.TYPE_USE:可以标注任意类型
  1. @Retention:标明注解的生命周期
  2. 有三种取值:
  • RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如 @SuppressWarnings,以及 lombok 提供的注解 @Data@Slf4j 等。
  • RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取。通常用于一些框架和工具的注解。
  • RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如 Spring 的 @Controller@ResponseBody 等。
② 切面类

使用 @annotation 切点表达式定义切点,只对自定义注解 @MyAspect 生效:

代码语言:javascript
复制
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    // 前置通知
    @Before("@annotation(com.example.demo.aspect.MyAspect)")
    public void before(){
        log.info("MyAspect -> before ...");
    }
    
    // 后置通知
    @After("@annotation(com.example.demo.aspect.MyAspect)")
    public void after(){
        log.info("MyAspect -> after ...");
    }
}
③ 添加自定义注解

TestController 中的 t1()UserController 中的 u1() 这两个方法上添加自定义注解 @MyAspect,其他方法不添加:

代码语言:javascript
复制
@MyAspect
@RequestMapping("/t1")
public String t1() {
    return "t1";
}
代码语言:javascript
复制
@MyAspect
@RequestMapping("/u1")
public String u1(){
    return "u1";
}

运行程序,测试接口

http://127.0.0.1:8080/test/t1

观察日志:

可以看到切面通知被执行了。

继续测试:

http://127.0.0.1:8080/test/t2,切面通知未执行

http://127.0.0.1:8080/user/u1,切面通知执行

Spring AOP的实现方式(常见面试题)

  1. 基于注解@Aspect
  2. 基于自定义注解
  3. 基于Spring API(通过 xml 配置的方式,自从 SpringBoot 广泛使用之后,这种方法几乎看不到了,了解即可)
  4. 基于代理来实现(更加久远的一种实现方式,写法笨重,不建议使用)

参考:https://cloud.tencent.com/developer/article/2032268

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Spring AOP 详解
    • 一、Spring AOP 核心概念
    • 二、通知类型
    • 三、@PointCut
    • 四、切面优先级 @Order
    • 五、切点表达式
      • 1. execution表达式
      • 2. @annotation自定义注解
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档