Spring AOP 的实现和切点表达式的介绍

发布于:2024-11-29 ⋅ 阅读:(24) ⋅ 点赞:(0)

1. 快速入手

AOP:就是面相切面编程,切面指的就是某一类特定的问题,也可以理解为面相特定方法编程,例如之前使用的拦截器,就是 AOP 思想的一种应用,统一数据返回格式和统一异常处理也是 AOP 思想的实现方式

比如说需要统计每个方法执行的耗时,如果正常来写的话,需要在方法的开头和结尾来定义时间戳相减

如果有很多方法都需要计算的话,总不能每个方法都写这些重复的代码吧,接下来看通过使用 AOP 思想是如何实现的

首先需要添加对应的依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后需要添加@Component注解,

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {
    @Around("execution(* com.example.springbook.controller.*.*(..))")
    public Object timeRecord(ProceedingJoinPoint pjt){
        //记录开始时间
        long start = System.currentTimeMillis();
        //执行目标方法
        Object o = null;
        try {
            o = pjt.proceed();
        } catch (Throwable e){
            throw new RuntimeException(e);
        }
        log.info(pjt.getSignature() + "执行耗时:" + (System.currentTimeMillis() - start));
        return o;
    }
}

再去调用接口的话就会计算出来对应方法消耗的时间

来简单分析一下上面的代码:

2. 通知类型

Spring AOP 的通知类型有以下几种

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

接下来同时测试一下这些通知类型,

来看一下接口正常返回的情况下的执行顺序

再来看接口发生异常的情况下的执行顺序:

从上面的结果上就可以看出,Around 可以完成其他类型的功能

需要注意的是:

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

3. @Pointcut

在上面的代码中还存在一个问题,每次写一个方法都需要写一个切点表达式,如果说更换切点的话,那么所有的切点表达式都要修改一下,就可以通过@Pointcut 注解,把公共的切点表达式提取出来,需要用到时引用该切点表达式即可

这样提取出来,其他方法想要调用直接写上方法名称即可,和定义的常量类似,那么同一个类下可以直接调用,如果是不同的类的话需要把全限定名写上,并写明是 xx 类的 xx 方法

@Around("com.example.springaop.aspect.AspectDemo.pt()")

执行之后也是生效了

但是如果定义时设置为了 private 的话其他类就不能执行了

@Pointcut("execution(* com.example.springaop.controller.*.*(..))")
private void pt(){

}

4. 切面优先级

当在一个项目中定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法,那么目标方法执行的时候,这些切面类中的通知方法都会执行,那么这时就会有一个优先级,哪个切面类先执行

通过测试发现,执行的顺序也是类似于一个切面的

关于切面类的执行顺序,默认是按照类名的字典序来执行的

可以@Order注解通过来修改优先级

这样 AspectDemo2 的优先级就变为最高的了,就先执行,也就是数字越大优先级越高

5. 切点表达式

5.1. execution 表达式

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

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

来看具体示例:

  1. 表示匹配 TestController 下的 public 修饰,返回类型为 String 方法名为 t1,无参方法

  1. 如果省略访问修饰符,表示匹配 public 修饰或者 protected 修饰的方法

  1. 表示匹配所有返回类型

  1. 如果再把方法名设为 * 表示所有方法,上面就是匹配该类下的所有无参方法

  1. 如果设为 .. 就表示所有方法,无论有参还是无参都能匹配

  1. 表示 controller 包下的所有类的所有方法都匹配

  1. 表示 com 下类名为 TestController 的所有方法

  1. 表示 demo 下的所有包的所有类的所有方法

5.2. @annotation

使用 execution 表达式匹配的方法都是具有一定规律的,比如 xx 包的 xx 类的 xx 方法,那么如果没有规律可循的话就需要使用 @annotation 注解了

首先,可以通过自定义注解的方式,自定义注解的创建需要选择 @Annotation

@Retention(RetentionPolicy.RUNTIME) //注解的有效阶段
@Target({ElementType.METHOD}) //表示方法注解
public @interface TimeRecord {
}

然后在原来计时的方法上来使用 @annotation 来指明要使用的注解

接下来只要是添加了自定义的注解都会执行这里的方法

通过这种方式就实现了想要给哪个方法生效就直接加上注解就可以了

除了自定义注解,其他现存的注解也是可以这样使用的

例如,可以把@RequestMapping的路径写在@annotation里,就表示只要加了@RequestMapping的方法都可以生效