【JavaEE进阶】Spring AOP使用篇

目录

1.AOP概述

2.SpringAOP快速入门

[2.1 引入AOP依赖](#2.1 引入AOP依赖)

[2.2 编写AOP程序](#2.2 编写AOP程序)

[3. Spring AOP详解](#3. Spring AOP详解)

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

3.1.1切点(Pointcut)

[3.1.2 连接点 (Join Point)](#3.1.2 连接点 (Join Point))

[3.1.3 通知(Advice)](#3.1.3 通知(Advice))

[3.1.4 切面(Aspect)](#3.1.4 切面(Aspect))

[3.2 通知类型](#3.2 通知类型)

3.3@PointCut

[3.4 切面优先级](#3.4 切面优先级)

[3.5 切点表达式](#3.5 切点表达式)

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

[3.5.2 @annotation](#3.5.2 @annotation)

[3.5.2.1 自定义注解 @MyAspect](#3.5.2.1 自定义注解 @MyAspect)

[3.5.2.2 切面类](#3.5.2.2 切面类)

[3.5.2.3 添加自定义注解](#3.5.2.3 添加自定义注解)

[4. Spring AOP的实现方式](#4. Spring AOP的实现方式)


1.AOP概述

AOP是Spring框架的一大核心(第一大核心是loC)

什么是AOP?

Aspect Oriented Programming(面向切面编程)

什么是面向切面编程呢?切面就是指某一类特定问题, 所以 AOP 也可以理解为面向特定方法编程.

什么是面向特定方法编程呢? 比如以前学习的"登录校验", 就是一类特定问题. 登录校验拦截器, 就

是对 "登录校验" 这类问题的统一处理. 所以,拦截器也是 AOP 的一种应用. AOP 是一种思想,拦截器是 AOP 思想的一种实现. Spring 框架实现了这种思想, 提供了拦截器技术的相关接口.

同样的, 统一数据返回格式和统一异常处理,也是AOP思想的一种实现.

简单来说: AOP是一种思想,是对某一类事情的集中处理.

什么是 Spring AOP ?

AOP是一种思想,它的实现方法有很多,有Spring AOP, 也有AspectJ, CGLIB等.

Spring AOP是其中的一种实现方式.

学会了统一功能之后,是不是就学会了Spring AOP呢,当然不是.

截器作用的维度是URL(一次请求和响应), @ControllerAdvice 应用场景主要是全局异常处理

(配合自定义异常效果更佳), 数据绑定 , 数据预处理. AOP 作用的维度更加细致(可以根据包、类、方法名、参数等进行拦截), 能够实现更加复杂的业务逻辑.

举个例子:

我们现在有一个项目, 项目中开发了很多的业务功能

现在有一些业务的执行效率比较低, 耗时较长, 我们需要对接口进行优化.

第一步就需要定位出执行耗时比较长的业务方法, 在针对该业务方法来进行优化

如何定位呢? 我们就需要统计当前项目中每一个业务方法的执行耗时.

如何统计呢? 可以在业务方法运行前和运行后, 记录下方法的开始时间和结束时间, 两者之差就是这个方法的耗时.

这种方法是可以解决问题的,但一个项目中会包含很多业务模块,每个业务模块又有很多接口,一个接口又包含很多方法, 如果我们要在每个业务方法中都记录方法的耗时,对于程序员而言, 会增加很多的工作量.

AOP 就可以做到在不改动这些原始方法的基础上, 针对特定的方法进行功能的增强.

AOP 的作用: 在程序执行期间不修改源代码的基础上对已有方法的增强(无侵入性: 解耦)

接下来我们来看看 SpringAOP 如何来实现.

2.SpringAOP快速入门

学习完什么是 AOP 后, 我们先通过下面的程序体验下 AOP 的开发, 并掌握 Spring 中 AOP 的开发步骤.

需求: 统计图书系统中各个接口方法的执行时间.

2.1 引入AOP依赖

在 pom.xml 文件中添加配置

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

2.2 编写AOP程序

记录 Controller 中每个方法的执行时间

先看看传统的方法:

java 复制代码
/**
     * 根据ID查询图书信息
     *
     * @param bookId
     * @return
     */
    @RequestMapping("/queryBookById")
    public BookInfo queryBookById(Integer bookId) {
        log.info("根据ID查询图书信息, id:{}", bookId);
        long start = System.currentTimeMillis();
        BookInfo bookInfo = bookService.queryBookById(bookId);
        long end = System.currentTimeMillis();
        log.info("[BookController] queryBookById 耗时: " + (end-start) + " ms");
        return bookInfo;
    }

使用这种方式, 当要测试多个接口的时候, 就需要对每个接口的代码进行修改 , 工作量比较复杂, 很影响我们的时间和效率.

下面来看看使用AOP的代码吧:

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

@Component
@Slf4j
@Aspect
public class TimeRecordAspect {
    /**
     * 记录耗时
     */
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object TimeRecord(ProceedingJoinPoint joinPoint) throws Throwable {
        //记录开始事件
        long start = System.currentTimeMillis();
        //执行目标方法
        Object proceed = joinPoint.proceed();
        //记录结束时间
        long end = System.currentTimeMillis();
        //日志打印耗时
        log.info(joinPoint.getSignature() + " 耗时时间: " + (end- start) + " ms");
        return proceed;
    }
}

运行程序, 观察日志

对程序进行简单的讲解:

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

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

  3. ProceedingJoinPoint.proceed(): 让原始方法执行

整个代码划分为三部分

我们通过AOP入门程序完成了业务接口执行耗时的统计.

通过上面的程序,我们也可以感受到AOP面向切面编程的一些优势:

  • 代码无侵入: 不修改原始的业务方法, 就可以对原始的业务方法进行了功能的增强或者是功能的改变
  • 减少了重复代码
  • 提高开发效率
  • 维护方便

3. Spring AOP详解

下面我门再来详细学习AOP, 主要是一下几个部分

Spring AOP 中涉及的核心概念

Spring AOP 的通知类型

多个 AOP 的执行顺序

3.1 Spring AOP 核心概念

3.1.1切点(Pointcut)

切点(Pointcut), 也称之为 "切入点"

Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述), 告诉程序对哪些方法来进行功能增强.

上面的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式.

3.1.2 连接点 (Join Point)

满足切点表达式规则的方法, 就是连接点, 也就是可以被 AOP 控制的方法

以如门程序举例, 所有 com.example.demo.controller 路径下的方法, 都是连接点

java 复制代码
package com.example.demo.controller;

@RequestMapping("/book")
@RestController
public class BookController {
    @RequestMapping("/addBook")
    public Result addBook(BookInfo bookInfo) {
        //...代码省略 
    }

    @RequestMapping("/queryBookById")
    public BookInfo queryBookById(Integer bookId) {
        //...代码省略 
    }

    @RequestMapping("/updateBook")
    public Result updateBook(BookInfo bookInfo) {
        //...代码省略 
    }
}

上述 BookController 中的方法都是连接点

切点和连接点的关系

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

比如:

切点表达式: 全体教师

连接点就是: 张三, 李四等各个教室

3.1.3 通知(Advice)

通知就是具体要做的事情, 指哪些重复的逻辑, 也就是共性功能(最终体现为一个方法)

比如上述程序中记录业务方法的耗时时间, 就是通知

在AOP面向切面编程当中, 我门把这部分重复的代码逻辑抽取出来单独定义, 这部分代码就是通知类容.

3.1.4 切面(Aspect)

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

通过切面就能描述当前AOP程序需要针对于哪些方法, 在什么时候执行什么样的操作.

切面既包含了通知逻辑的定义, 也包括了连接点的定义.

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

3.2 通知类型

上面我们讲了什么是通知, 接下来学习通知的类型. @Around 就是其中的一种通知类型, 表示环绕通知.

Spring 中 AOP 的通知类型有一下几种:

@Around: 环绕通知, 次注解标注的通知方法在目标方法前, 后都被执行

@Before: 前置通知, 次注解表述的通知方法在目标方法前被执行

@After: 后置通知, 次注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行

@AfterReturning: 返回后通知, 次注解标注的通知方法在目标方法返回后被执行, 有异常不会执行

@AfterThrowing: 次注解标注的通知方法发生异常后执行

接下来我门通过代码来加深对这几个通知的理解:

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


@Component
@Slf4j
@Aspect
public class AspectDemo {
    //前置通知
    @Before("execution(* com.example.aop.controller.*.*(..))")
    public void doBefore() {
        log.info("AspectDemo do before...");
    }
    //后置通知
    @After("execution(* com.example.aop.controller.*.*(..))")
    public void doAfter() {
        log.info("AspectDemo do after...");
    }
    //环绕通知
    @Around("execution(* com.example.aop.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AspectDemo do around before...");
        Object result = null;
        try {
            result = joinPoint.proceed();
        }catch (Exception e) {
            log.error("do around 执行目标函数, 内部发生异常");
        }
        log.info("AspectDemo do around after...");
        return result;
    }
    //返回后通知
    @AfterReturning("execution(* com.example.aop.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("AspectDemo do AfterReturning...");
    }
    //抛出异常后通知
    @AfterThrowing("execution(* com.example.aop.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("AspectDemo do AfterThrowing...");
    }
}

写一些测试程序:

java 复制代码
import com.example.aop.config.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {
    @MyAspect
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法....");
        return "t1";
    }

    @RequestMapping("/t2")
    public String t2() {
        log.info("执行t2方法....");
        int a = 10/0;
        return "t2";
    }
}

运行程序, 观察日志:

1.正常运行的情况

观察日志

程序正常运行的情况下, @AfterThrowing 标识的通知方法不会执行

从上图也可以看出来, @Around 标识的通知方法包含两个部分, 一个 "前置逻辑" , 一个 "后置逻辑" .其中 "前置逻辑" 会先于 @Before 标识的通知方法执行. "后置逻辑" 会晚于 @After 标识的通知方法执行

  1. 异常时的情况

观察日志

程序发生异常的情况下:

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

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

注意事项:

@Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行, 其他通知不需要考虑目标方法执行.

@Around环绕通知方法的返回值, 必须指定为Object , 来接收原始方法的返回值, 否则原始方法执行完毕, 是获取不到返回值的.

一个切面类可以有多个切点

3.3@PointCut

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

上述代码就可以修改为:

java 复制代码
@Component
@Slf4j
@Aspect
public class AspectDemo {
    @Pointcut("execution(* com.example.aop.controller.*.*(..))")
    public void pt(){};

    //前置通知
    @Before("pt()")
    public void doBefore() {
        //...代码省略
    }
    //后置通知
    @After("pt()")
    public void doAfter() {
        //...代码省略
    }
    //添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        //...代码省略
    }
    //返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        //...代码省略
    }
    //抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        //...代码省略
    }
}

当请切点使用 private 修饰时, 仅能在当前切面类中使用, 当其他切面类也要使用当前切点定义时, 就需要把 private 改为 public, 引用方式为: 全限定类名.方法名()

java 复制代码
@Component
@Slf4j
@Aspect
public class AspectDemo2 {

    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("AspectDemo2 do before...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("AspectDemo2 do after...");
    }
}

3.4 切面优先级

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

我们还是通过程序来验证:

定义多个切⾯类:

为了防止干扰, 我们把AspectDemo这个切面先去掉(把@Component注解去掉就可以)

为了简单化, 只写了@Before 和 @After 两个通知

java 复制代码
@Component
@Slf4j
@Aspect
public class AspectDemo2 {

    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("AspectDemo2 do before...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("AspectDemo2 do after...");
    }
}
java 复制代码
@Component
@Slf4j
@Aspect
public class AspectDemo3 {
    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("AspectDemo3 do before...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("AspectDemo3 do after...");
    }
}
java 复制代码
@Component
@Slf4j
@Aspect
public class AspectDemo4 {

    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("AspectDemo4 do before...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("AspectDemo4 do after...");
    }
}

运行程序, 访问接口:

观察日志:

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

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

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

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

Spring给我们提供了一个新的注解,来控制这些切面通知的执行顺序: @Order

使用方式如下:

重新运行程序, 观察日志:

通过上述程序的运行结果, 得出结论:

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

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

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

@Order 控制切面的优先级, 先执行优先级高的切面, 在执行优先级较低的切面, 最终执行目标方法.

3.5 切点表达式

上面的代码中,我们一直在使用切点表达式来描述切点. 下面我们来介绍一下切点表达式的语法.

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

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

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

3.5.1 execution 表达式

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

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

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

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

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

b. 类名使用 * 表示任意类

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

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

e. 参数使用 * 表示一个任意类型的参数

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

a. 使用 .. 配置包名, 标识次包以及此包下的所有子包

b. 可以使用 .. 配置参数, 任意个任意类型的参数

切点表达式示例

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

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

省略访问修饰符

execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型

execution(* com.example.demo.controller.TestController.t1())

匹配 TestController 下的所有无参方法

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

匹配 TestController 下的所有方法

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

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

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

匹配所有包下面的 TestController

execution(* com..TestController.*(..))

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

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

匹配特定方法名且有特定参数的方法:

java 复制代码
execution(* myMethod(String, int))

匹配特定方法名且有特定参数, 并且抛出特定异常的方法

java 复制代码
execution(* myMethod(String, int) throws IOExeception)

3.5.2 @annotation

execution 表达式更适合有规则的, 如果我门要匹配多个无规则的方法呢, 比如: TestController中的 t1() 和 UserController 中的 u1() 这两个方法.

这个时候我们使用 execution 这种切点表达式来描述就不是很方便了.

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

实现步骤:

  1. 编写自定义注解

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

  3. 在连接点的方法上添加自定义注解

准备测试代码

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

    @RequestMapping("/t2")
    public String t2() {
        log.info("执行t2方法....");
        int a = 10/0;
        return "t2";
    }
}
java 复制代码
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController{
    @RequestMapping("/u1")
    public String u1() {
        log.info("执行u1方法...");
        return "u1";
    }

    @RequestMapping("/u2")
    public String u2() {
        log.info("执行u2方法...");
        return "u2";
    }
}
3.5.2.1 自定义注解 @MyAspect

创建一个注解类(和创建Class文件一样的流程, 选择Annotation就可以了)

java 复制代码
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 所修饰的对象范围, 及该注解可以用在什么地方.

常用取值:

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

ElementType.METHOD: 描述方法

ElementType.PARAMETER: 描述参数

ElementType.TYPE_USE: 可以标注任意类型

  1. @Retention 指 Annotation 被保留的时间长短, 标明注解的声明周期

@Retention的取值有三种

  1. RetentionPolicy.SOURCE: 表示注解仅存在于源代码中,编译成字节码后会被丢弃. 这意味着在运行时无法获取到该注解的信息, 只能在编译时使用. 比如@SuppressWarnings, 以及lombok 提供的注解 @Data, @Slf4j

  2. RetentionPolicy.CLASS: 编译时注解. 表示注解存在于源代码和字节码中,但在运行时会被丢弃. 这意味着在编译时和字节码中可以通过反射获取到该注解的信息, 但在实际运行时无法获取. 通常用于一些框架和工具的注解.

  3. RetentionPolicy.RUNTIME: 运行时注解. 表示注解存在于源代码,字节码和运行时中. 这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息. 通常用于一些需要在运行时处理的注解,如Spring的 @Controller @ResponseBody

3.5.2.2 切面类

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

切面类代码如下:

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

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

运行程序, 测试接口:

观察日志:

继续测试 t2, 观察日志:

切面通知未执行

4. Spring AOP的实现方式

1.基于注解 @Aspect

  1. 基于自定义注解(参考上面自定义注解 @annotation 部分的内容)

  2. 基于Spring API (通过xml配置的方法, 自从SpringBoot广泛使用之后, 这种方法几乎看不到了, 稍作了解即可)

  3. 基于代理来实现(更加久远的一种方式, 写法笨重, 不建议使用)

相关推荐
小屁孩大帅-杨一凡24 分钟前
java后端请求想接收多个对象入参的数据
java·开发语言
m0_6569747429 分钟前
C#中的集合类及其使用
开发语言·c#
java1234_小锋31 分钟前
使用 RabbitMQ 有什么好处?
java·开发语言
wjs202440 分钟前
R 数据框
开发语言
幺零九零零42 分钟前
【C++】socket套接字编程
linux·服务器·网络·c++
TangKenny44 分钟前
计算网络信号
java·算法·华为
肘击鸣的百k路1 小时前
Java 代理模式详解
java·开发语言·代理模式
城南vision1 小时前
Docker学习—Docker核心概念总结
java·学习·docker
捕鲸叉1 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
wyh要好好学习1 小时前
SpringMVC快速上手
java·spring