Spring AOP

目录

什么是AOP?

[什么是Spring AOP?](#什么是Spring AOP?)

AOP的快速入门

引入依赖

编写程序

[Spring AOP核心概念](#Spring AOP核心概念)

切点

连接点

通知

切面

通知类型

环绕通知(@Around)

前置通知(@Before)

后置通知(@After)

返回通知(@AfterReturning)

异常通知(@AfterThrowing)

@PointCut

@Order

切点表达式

[execution 表达式](#execution 表达式)

@annotation

自定义注解

描述切点

添加自定义注解


什么是AOP?

**AOP(Aspect-Oriented Programming,面向切面编程):**是一种软件开发的编程范式,旨在将横切关注点(cross-cutting concerns)与核心业务逻辑分离,以提高代码的模块化性、可维护性和复用性

我们首先来理解,什么是面向切面编程

切面 ,就是指某一类特定问题 ,因此AOP也可以理解为面向特定方法编程

例如:在实现登录逻辑时,登录校验 就是一类特定的问题,而登录校验拦截器,就是对登录校验 这类问题的统一处理,因此,拦截器也是 AOP 的一种应用

AOP是一种思想,拦截器是AOP思想的一种实现,统一数据返回格式 和 统一异常处理,也是 AOP思想的一种实现

简而言之,AOP是一种思想,是对某一类问题的集中处理

什么是Spring AOP?

AOP是一种思想,它的实现方法有很多(如 Spring AOP、AspectJ、CGLIB)

也就是说,Spring AOP 是 AOP 的一种实现方式

AOP的快速入门

我们首先来看一个例子:

假设此时某些方法的执行效率较低,耗时较长,需要对接口进行优化

那么,我们需要定位出耗时较长的方法,再对其进行优化

那么,如何进行定位呢?

我们可以统计每个方法的耗时,在方法运行前和方法运行后,记录下方法的开始时间和结束时间,两者之差就是该方法的耗时

采用上述方法确实可以计算出耗时,从而定位方法,但是,当方法较多时,若我们在每个方法中都要记录耗时时间,就会增加很多重复的工作量

此时,我们就可以使用 AOP,在保持原始方法不动的基础上,对特定的方法进行功能增强

接下来,我们就来学习 Spring AOP 是如何实现的

首先,需要引入 AOP 依赖

引入依赖

pom.xml中添加配置:

XML 复制代码
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

接下来,就可以编写 AOP 程序了

编写程序

java 复制代码
@Slf4j
@Component
@Aspect
public class TimeAspect {
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 记录开始时间
        long startTime = System.currentTimeMillis();
        // 执行方法
        Object result = joinPoint.proceed();
        // 记录结束时间
        long endTime = System.currentTimeMillis();
        // 打印耗时时间
        log.info("耗时: " + (endTime - startTime) + " ms");
        return result;
    }
}

com.example.demo.controller 目录下创建TestController,并记录 t1 方法执行时间:

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("TestController t1...");
        return "t1";
    }
}

运行程序,观察日志:

我们先来对 recordTime 进行简单理解:

@Aspect :标识这是一个切面类

@Around:环绕通知,在目标方法前后都会执行,() 中的表达式表示对哪些方法进行增强

joinPoint.proceed:执行原方法

可以将代码划分为三个部分:

此时,我们就通过 AOP 程序完成了接口执行耗时的统计

在上述程序中,我们可以看到 AOP 可以在不修改原始方法的基础上,对原始的方法进行了功能的增强,这样减少了重复代码,让我们维护起来更加方便,也提高了开发效率

接下来,我们就对 AOP 进行进一步的学习

Spring AOP核心概念

切点

切点(Pointcut) :也称为 切入点 ,提供一组规则 ,用来定义在何处切入(即在哪些方法或类上应用增强逻辑),告诉程序对哪些方法进行功能增强

其中,execution(* com.example.demo.controller.*.*(..)) 就是切点表达式

连接点

连接点(Join Point) :在程序执行过程中可以插入切面的点,也就是满足切点表达式规则的方法,即可以被 AOP 控制的方法

execution(* com.example.demo.controller.*.*(..)) 中,com.example.demo.controller路径下的方法,都是连接点

连接点和切点的关系

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

例如:

切点表达式:一班所有学生

连接点:张三、李四、王五...

通知

通知(Advice) :在切点定义的连接点上要执行的代码,也就是要执行的逻辑

在 AOP 面向切面编程中,我们将重复的代码逻辑抽取出来单独定义,而这部分代码就是通知的内容

切面

切面(Aspect) :切面描述的是当前 AOP 程序需要针对哪些方法,在什么时候执行什么样的操作

切面(Aspect) = 切点(Pointcut) + 通知(Advice)

切面所在的类,一般称之为切面类 (被**@Aspect** 注解标识的类)

通知类型

通知是切点匹配时执行的代码块,根据执行时机的不同,通知可分为以下几种类型:

环绕通知(@Around)

环绕通知在切点方法执行前后都可以执行,可以控制切点方法是否执行

java 复制代码
    // 环绕通知
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AspectDemo around start...");
        // 执行方法
        Object result = joinPoint.proceed();
        log.info("AspectDemo around end...");
        return result;
    }

@Around 通知的返回值是一个Object 类型,代表原方法的返回值,在原方法执行完毕后,可以对其进行处理后再返回

若是不返回,则无法将目标方法的返回值传递给调用者,可能会导致程序行为不符合预期

例如:

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("TestController t1...");
        return "t1";
    }
}

运行程序,并访问: 127.0.0.1:8080/test/t1

观察结果:

不进行返回:

java 复制代码
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AspectDemo around start...");
        // 执行方法
        Object result = joinPoint.proceed();
        log.info("AspectDemo around end...");
    }

再次运行程序,并访问: 127.0.0.1:8080/test/t1

可以看到,当不进行返回时,并不能正确显示返回结果 "t1"

因此,在使用环绕通知时,不要忘了返回目标方法的返回值

我们再来看参数 ProceedingJoinPoint

我们先看 JoinPoint接口:

JoinPoint 是 Spring AOP 中的一个接口, 表示在应用程序执行过程中某个特定的点 ,通常是方法的调用,通过 JoinPoint 可以获取关于被拦截方法的各种信息,例如:

可以使用getSignature() 方法可以获取被拦截方法的签名,包括方法名、返回类型和参数类型等信息;可以通过**getArgs()**方法获取被拦截方法的参数数组

JoinPoint 常用于前置通知、后置通知异常通知

ProceedingJoinPoint 也是 Spring AOP 中的一个接口,它扩展了 JoinPoint 接口,主要用于环绕通知中控制目标方法的执行 ,通过 ProceedingJoinPoint可以获取方法的相关信息,并能够在环绕通知中调用目标方法

其中,proceed() 方法用于执行目标方法 ,若在环绕通知中需要执行目标方法,就可以调用该方法,由于被拦截的方法可能会抛出各种异常,因此,proceed() 方法为了保证在环绕通知中能够正确处理目标方法的异常情况,在其签名中声明了可能会抛出Throwable,让我们可以根据自己的需求捕获和处理异常

前置通知(@Before)

前置通知是在切点方法执行之前执行的通知

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    // 前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore(JoinPoint joinPoint) {
        // log.info("AspectDemo doBefore...");
        log.info("AspectDemo doBefore... " + joinPoint.getSignature());
    }
}

运行程序,访问 127.0.0.1:8080/test/t1,观察日志:

前置方法在目标方法运行前执行

对于参数 JoinPoint,若是不需要获取被拦截方法的信息,也可以不进行传递

java 复制代码
    // 前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore() {
         log.info("AspectDemo doBefore...");
    }

后置通知(@After)

后置通知是在切点方法执行后执行的通知无论是否抛出异常都会执行

java 复制代码
    // 后置通知
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("AspectDemo doAfter...");
    }

测试:

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("TestController t1...");
        return "t1";
    }
    @RequestMapping("/t2")
    public String t2() {
        log.info("TestController t2...");
        int k = 10 / 0;
        return "t2";
    }
}

运行结果:

可以看到,无论是否抛出异常,后置通知都执行了

返回通知(@AfterReturning)

返回通知是在切点方法成功执行后执行的通知当抛出异常时不会执行

java 复制代码
    // 返回后通知
    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterReturn(JoinPoint joinPoint) {

        log.info("AspectDemo doAfterReturn...");
    }

执行 t1 和 t2 方法:

只有 t1执行完后执行了 返回通知

异常通知(@AfterThrowing)

异常通知是在切点方法抛出异常后执行的通知,可以获取到异常信息

java 复制代码
    // 抛出异常后通知
    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("AspectDemo doAfterThrowing...");
    }

执行 t1 和 t2 方法:

只有 t2 方法抛出了异常之后执行了 异常通知

那么,当这些通知同时出现时,它们的执行先后顺序是怎样的呢?

我们来进行测试:

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
     // 环绕通知
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AspectDemo around start...");
        // 执行方法
         Object result = joinPoint.proceed();
        log.info("AspectDemo around end...");
        return result;
    }

    // 前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore() {
         log.info("AspectDemo doBefore...");
    }

    // 后置通知
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("AspectDemo doAfter...");
    }

    // 返回后通知
    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterReturn(JoinPoint joinPoint) {

        log.info("AspectDemo doAfterReturn...");
    }

    // 抛出异常后通知
    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("AspectDemo doAfterThrowing...");
    }
}

运行程序,观察日志:

可以看到,程序正常运行情况下:

@AfterThrowing标识的通知方法不会执行

目标方法执行前:@Around 的前置逻辑会先于**@Before** 标识的通知方法执行

目标方法执行后:@AfterReturning 标识的通知方法在程序结束后最先执行,其次是**@After** 标识的通知方法,最后是 @Around 的后置逻辑

而当出现异常情况时:

@AfterReturning 标识的通知方法不会执行

@AfterThrowing 标识的通知方法执行了

目标方法执行前:@Around 的前置逻辑会先于 @Before 标识的通知方法执行

目标方法执行后: @AfterThrowing 标识的通知方法先执行, @After 标识的通知方法后执行,由于抛出了异常,@Around的后置逻辑未被执行

@PointCut

在上述实现通知方法时,出现了一个问题:

就是出现了大量重复的切点表达式: execution(* com.example.demo.controller.*.*(..))

Spring 提供了**@PointCut** 注解,可以将公共的切点表达式提取出来,需要用到时引入该切点表达式即可

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    // 定义切点(公共的切点表达式)
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
     // 环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AspectDemo around start...");
        // 执行方法
         Object result = joinPoint.proceed();
        log.info("AspectDemo around end...");
        return result;
    }

    // 前置通知
    @Before("pt()")
    public void doBefore() {
         log.info("AspectDemo doBefore...");
    }

    // 后置通知
    @After("pt()")
    public void doAfter() {
        log.info("AspectDemo doAfter...");
    }

    // 返回后通知
    @AfterReturning("pt()")
    public void doAfterReturn(JoinPoint joinPoint) {

        log.info("AspectDemo doAfterReturn...");
    }

    // 抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        log.info("AspectDemo doAfterThrowing...");
    }
}

当切点定义使用 private 修饰时,仅能在当前切面类使用

若其他切面类也要使用当前切点定义时,需要将 private 修改为 public

java 复制代码
    // 定义切点(公共的切点表达式)
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pt(){}

且在其他切面类引用的方式为:全限定类名.方法名()

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    // 前置通知
    @Before("com.example.demo.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("AspectDemo2 doBefore...");
    }
}

@Order

当有多个切面类,且这些切面类的多个切入点都匹配到了同一个目标方法时,那么这些切面类的通知方法都会执行,那么,它们的执行顺序是怎样的呢?

我们来测试一下:

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo1 {
    // 定义切点(公共的切点表达式)
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pt(){}
    
    // 前置通知
     @Before("pt()")
    public void doBefore() {
         log.info("AspectDemo1 doBefore...");
    }

    // 后置通知
    @After("pt()")
    public void doAfter() {
        log.info("AspectDemo1 doAfter...");
    }
}
java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    // 前置通知
    @Before("com.example.demo.aspect.AspectDemo1.pt()")
    public void doBefore() {
        log.info("AspectDemo2 doBefore...");
    }

    // 后置通知
    @After("com.example.demo.aspect.AspectDemo1.pt()")
    public void doAfter() {
        log.info("AspectDemo2 doAfter...");
    }
}
java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo3 {
    // 前置通知
     @Before("com.example.demo.aspect.AspectDemo1.pt()")
    public void doBefore() {
         log.info("AspectDemo3 doBefore...");
    }

    // 后置通知
    @After("com.example.demo.aspect.AspectDemo1.pt()")
    public void doAfter() {
        log.info("AspectDemo3 doAfter...");
    }
}
java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("TestController t1...");
        return "t1";
    }
}

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

观察日志:

可以看到:

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

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

@After 通知:字母排名靠前的后执行
但我们能否自己指定切面类的执行顺序呢?

Spring 为我们提供了**@Order** 注解,用来控制这些切面通知的执行顺序,先执行优先级较高的切面,再执行优先级较低的切面,最后执行目标方法

再次运行程序,观察日志:

可以发现:

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

@Before 通知:数字小的先执行

@After 通知:数字小的后执行

Order 值越小,优先级越高

优先级高的切面类先执行,优先级低的切面类后执行,最后再执行目标方法

目标方法执行后,优先级低的切面类先执行,优先级高的切面类后执行

切点表达式

在上述代码中,我们一直在使用切点表达式来描述切点,接下来,我们就来详细介绍一下切点表达式的语法:

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

(1)execution(...):根据方法的签名来匹配

(2)@annotation(...):根据注解来匹配

我们首先来看 execution 表达式

execution 表达式

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

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

其中,访问修饰符 和 异常 可以省略

切点表达式中可以使用通配符:

*: 匹配任意个字符,只匹配一个元素(返回类型、包、类名、方法名或方法参数)

返回值使用 * 表示任意返回类型

包名使用 * 表示任意包(一层包使用一个 *)

类名使用 * 表示任意类

方法名使用 * 表示任意方法

参数使用 * 表示任意类型的参数

..: 匹配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

使用 .. 配置包名,表示此包和包下的所有子包

使用 .. 配置参数,表示任意个任意类型的参数

例如:

execution(public String com.example.demo.controller.TestController.*(..))

匹配 TestController 类中所有 public 修饰的,返回值为 String 类型的方法

execution(public int com.example.demo.controller.TestController.t1())

匹配 TestController 下 public 修饰,返回值为 int,方法名为 t1 的无参方法

execution(* com.example.demo.controller.*.*(String, int))

匹配 com.example.demo.controller 包下参数类型为 (String, int) 的方法

execution(* com.example.demo..*(..))

匹配 com.example.demo 包下(包括子包)的所有类中的所有方法

execution(* com.example.demo.TestController.*(..) throws IOException)

匹配 TestController 类中抛出 IOException 异常的方法

@annotation

execution 表达式更适用于有规则的方法,但是,若我们要匹配多个无规则的方法,例如 TestController 中的 t1()、UserController 中的 login()

此时,我们使用 execution 切点表达式来描述就不太方便了

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

实现步骤:

(1)编写自定义注解

(2)使用 @annotation 表达式来描述切点

(3)在连接点方法上添加自定义注解

我们先准备测试代码:

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("TestController t1...");
        return "t1";
    }
    @RequestMapping("/t2")
    public String t2() {
        log.info("TestController t2...");
        return "t2";
    }
}
java 复制代码
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public boolean login(String name, String password) {
        return true;
    }
    
}
自定义注解

创建一个注解类:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
    
}

@Target 标识了 Annotation 所修饰的对象范围,即该注解用在什么地方

常用取值:

ElementType.TYPE:用于描述类、接口(包括注解类型)或 enum 声明

ElementType.FIELD:用于描述类的字段(属性)

ElementType.METHOD:用于描述方法

**ElementType.CONSTRUCTOR:**用于描述构造方法

**ElementType.PACKAGE:**用于包声明

**ElementType.TYPE_USE:**用于标注任意类型

@Retention 指 Annotation 被保留的时间长短,标明注解的生命周期

RetentionPolicy.SOURCE:注解仅在源代码中保留,编译时会被丢弃。这意味着注解不会出现在字节码中,也不会在运行时可用,例如 @Slf4j、@Data

RetentionPolicy.CLASS:注解在编译时被保留(存在于源代码和字节码中),但在运行时不再可用。也就意味着在编译时和字节码中可以通过反射获取到该注解的信息,但实际运行时无法获取,常用于一些框架和工具的注解

RetentionPolicy.RUNTIME:运行时注解,存在于源代码、字节码 和 运行时中。它既会保留在字节码中,也可以通过反射获取,常用于一些需要运行时处理的注解,如 Spring 的 @Controller、@ResponseBody

描述切点

使用**@annotation** 切点表达式定义切点,只对 @MyAspect生效

java 复制代码
@Slf4j
@Aspect
@Component
public class MyAspectDemo {
    // 环绕通知
    @Around("@annotation(com.example.demo.config.MyAspect)")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AspectDemo around start...");
        // 执行方法
        Object result = joinPoint.proceed();
        log.info("AspectDemo around end...");
        return result;
    }
}
添加自定义注解

在 TestController 中的t1() 和 UserController 中的 login() 方法上添加自定义注解 @MyAspect:

java 复制代码
    @MyAspect
    @RequestMapping("/t1")
    public String t1() {
        log.info("TestController t1...");
        return "t1";
    }
java 复制代码
    @MyAspect
    @RequestMapping("/login")
    public boolean login(String name, String password) {
        return true;
    }

运行程序,测试:

可以看到,t1() 和 login() 方法切面通知被执行,t2() 方法切面通知未被执行

相关推荐
Be_Somebody1 分钟前
[这可能是最好的Spring教程!]Maven的模块管理——如何拆分大项目并且用parent继承保证代码的简介性
java·spring boot·spring·spring入门
计算机学姐10 分钟前
基于Python的高校成绩分析管理系统
开发语言·vue.js·后端·python·mysql·pycharm·django
VertexGeek12 分钟前
Rust学习(三):rust基础Ⅱ
开发语言·学习·rust
一个数据小开发18 分钟前
业务开发问题之ConcurrentHashMap
java·开发语言·高并发·map
会飞的架狗师34 分钟前
【Spring】Spring框架中有有哪些常见的设计模式
java·spring·设计模式
wclass-zhengge42 分钟前
SpringCloud篇(服务拆分 / 远程调用 - 入门案例)
后端·spring·spring cloud
Jakarta EE44 分钟前
在JPA和EJB中用乐观锁解决并发问题
java
三小尛1 小时前
插入排序(C语言)
c语言·开发语言
南宫理的日知录1 小时前
106、Python并发编程:深入浅出理解线程池的内部实现原理
开发语言·python·学习·编程学习
花心蝴蝶.1 小时前
并发编程中常见的锁策略
java·jvm·windows