【Spring 核心:AOP】基础到深入:思想、实现方式、切点表达式与自定义注解全梳理

文章目录

    • 一、什么是AOP
    • [二、Spring AOP 使用入门](#二、Spring AOP 使用入门)
      • [2.1 引入AOP依赖](#2.1 引入AOP依赖)
      • [2.2 编写AOP程序](#2.2 编写AOP程序)
    • [三、Spring AOP 详解](#三、Spring AOP 详解)
      • [3.1 Spring AOP核心概念](#3.1 Spring AOP核心概念)
        • [3.1.1 切点(Pointcut)](#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.3 @PointCut)
      • [3.4 切面优先级 @Order](#3.4 切面优先级 @Order)
      • [3.5 切点表达式](#3.5 切点表达式)
        • [3.5.1 execution表达式](#3.5.1 execution表达式)
        • [3.5.2 @annotation](#3.5.2 @annotation)

一、什么是AOP

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

什么是AOP?

Aspect Oriented Programming(面向切面编程)

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

  • 面向特定方法编程:比如"登录校验",就是一类特定问题。登录校验拦截器,就是对"登录校验"这类问题的统一处理。所以,拦截器也是AOP的一种应用。AOP是一种思想,拦截器是AOP思想的一种实现。Spring框架实现了这种思想,提供了拦截器技术的相关接口。

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

什么是Spring AOP?

AOP是一种思想,它的实现方法有很多,有Spring AOP,也有AspectJ、CGLIB等。Spring AOP是其中的一种实现方式。

AOP作用的维度更加细致(可以根据包、类、方法名、参数等进行拦截),能够实现更加复杂的业务逻辑。

举个例子:

现在有一个项目,项目中开发了很多的业务功能。现在有一些业务的执行效率比较低,耗时较长,我们需要对接口进行优化。第一步就需要定位出执行耗时比较长的业务方法,再针对该业务方法来进行优化。

java 复制代码
public void function1(){
    test1();
}

public void function1(){
    Long startTime = System.currentTimeMillis();//记录开始时间
    test1();
    Long endTime = System.currentTimeMillis();//记录结束时间
    Log.info("function1执行耗时:" + (endTime - startTime) + "ms");//记录方法执行耗时
}

这种方法是可以解决问题的,但一个项目中会包含很多业务模块,每个业务模块又有很多接口,一个接口又包含很多方法,如果要在每个业务方法中都记录方法的耗时,会增加很多的工作量。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 复制代码
@Slf4j
@Aspect
@Component
public class TimeAspect {
    /**
     * 记录方法耗时
     */
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();
        //执行原始方法
        Object result = pjp.proceed();
        //记录方法执行结束时间
        long end = System.currentTimeMillis();
        //记录方法执行耗时
        log.info(pjp.getSignature() + "执行耗时: {}ms", end - begin);
        return result;
    }
}

对程序进行简单的讲解

  1. @Aspect:标识这是一个切面类。
  2. @Around:环绕通知,在目标方法的前后都会被执行。后面的表达式表示对哪些方法进行增强。
  3. ProceedingJoinPoint.proceed():让原始方法执行。

整个代码划分为三部分:

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

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

三、Spring AOP 详解

Spring AOP 主要是以下几部分:

  • Spring AOP中涉及的核心概念
  • Spring AOP通知类型
  • 多个AOP程序的执行顺序

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

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

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

3.1.2 连接点(Join Point)

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

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

java 复制代码
@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面向切面编程当中,j将这部分重复的代码逻辑抽取出来单独定义,这部分代码就是通知的内容。

3.1.4 切面(Aspect)

切面(Aspect)= 切点(Pointcut)+ 连接点(Join Point)+ 通知(Advice)

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

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

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

3.2 通知类型

通知的类型:@Around 就是其中一种通知类型,表示环绕通知。Spring中AOP的通知类型有以下几种:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

代码例子:

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    //前置通知
    @Before("execution(* com.whx.aop.demo.controller.*.*(..))")
    public void doBefore() {
        log.info("执行 Before 方法");
    }

    //后置通知
    @After("execution(* com.whx.aop.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("执行 After 方法");
    }

    //返回后通知
    @AfterReturning("execution(* com.whx.aop.demo.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("执行 AfterReturning 方法");
    }

    //抛出异常后通知
    @AfterThrowing("execution(* com.whx.aop.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执行 doAfterThrowing 方法");
    }

    //添加环绕通知
    @Around("execution(* com.whx.aop.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around 方法开始执行");
        Object result = joinPoint.proceed();
        log.info("Around 方法结束执行");
        return result;
    }
}

测试程序:

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

    @RequestMapping("/t2")
    public boolean t2() {
        int a = 10 / 0;
        return true;
    }
}

正常运行的情况:

观察日志:

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

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

异常时的情况:

观察日志:

程序发生异常的情况下:

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

注意事项

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

3.3 @PointCut

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

上述代码就可以修改为:

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    //定义切点(公共的切点表达式)
    @Pointcut("execution(* com.whx.aop.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。引用方式为:全限定类名.方法名()

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

3.4 切面优先级 @Order

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

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

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

但实际开发中类名不会按照字母顺序来定义,此时会不方便管理。

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

使用方式如下:

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

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

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

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

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

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

3.5 切点表达式

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

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

execution() 是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

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

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

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

切点表达式示例表:

匹配场景 execution 切点表达式
TestController 下public修饰、返回String、方法名t1、无参方法 execution(public String com.example.demo.controller.TestController.t1())
省略访问修饰符(同上述方法,无public限定) execution(String com.example.demo.controller.TestController.t1())
匹配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..*(..))
3.5.2 @annotation

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

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

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

实现步骤:

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

准备测试代码:

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

    @RequestMapping("/t2")
    public boolean t2() {
        return true;
    }
}

@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1() {
        return "u1";
    }

    @RequestMapping("/u2")
    public String u2() {
        return "u2";
    }
}
  1. 自定义注解 @MyAspect:创建一个注解类(和创建Class文件一样的流程,选择Annotation)
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
  1. 切面类:使用@annotation 切点表达式定义切点,只对 @MyAspect 生效

切面类代码如下:

java 复制代码
@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 ...");
    }
}
  1. 添加自定义注解:在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解 @MyAspect,其他方法不添加

接口代码:

java 复制代码
@MyAspect
@RequestMapping("/t1")
public String t1() {
    return "t1";
}

@MyAspect
@RequestMapping("/u1")
public String u1() {
    return "u1";
}

同理,不仅可以自定义注解,还可以对已有的注解进行生效,比如接口的@RequestMapping注解,使得对所有添加了@RequestMapping注解的接口都生效。

相关推荐
编程彩机2 小时前
互联网大厂Java面试:从分布式事务到微服务优化的技术场景解读
java·spring boot·redis·微服务·面试·kafka·分布式事务
bbq粉刷匠2 小时前
Java-排序2
java·数据结构·排序算法
编程彩机2 小时前
互联网大厂Java面试:从Spring WebFlux到分布式事务的技术场景解析
java·微服务·面试·分布式事务·spring webflux
Jm_洋洋2 小时前
【C++进阶】虚函数、虚表与虚指针:多态底层机制剖析
java·开发语言·c++
小马爱打代码2 小时前
MyBatis:缓存体系设计与避坑大全
java·缓存·mybatis
时艰.2 小时前
Java 并发编程:Callable、Future 与 CompletableFuture
java·网络
码云数智-园园2 小时前
深入理解与正确实现 .NET 中的 BackgroundService
java·开发语言
好好研究2 小时前
SpringBoot整合SpringMVC
xml·java·spring boot·后端·mvc
千寻技术帮2 小时前
10386_基于SpringBoot的外卖点餐管理系统
java·spring boot·vue·外卖点餐