Spring AOP

发布于:2024-04-23 ⋅ 阅读:(14) ⋅ 点赞:(0)

AOP即 Aspect Oriented Programming 面向切面编程。

切面指的是某一类特定的问题,面向切面编程即解决某一类问题,例如前面我们介绍的拦截器,统一数据返回,统一异常处理。

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

1. Spring AOP 快速入门

1.1 引入依赖

要使用Spring AOP 需要先引入对应的依赖,在 pom.xml 文件中添加配置:

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

1.2 编写AOP程序

场景需求:现有一项目执行速度过慢,需要找到速度慢的原因,那么就需要统计每个方法的执行时间来分析。

package com.example.book.aspect;

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;

@Slf4j
@Aspect
@Component
public class TimeAspect {

    @Around("execution(* com.example.book.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //方法执行前时间戳
        long begin = System.currentTimeMillis();

        //执行方法
        Object result = joinPoint.proceed();

        //方法执行后时间戳
        long end = System.currentTimeMillis();

        //方法执行耗时
        log.info("执行时间:{}", end - begin + " ms");

        return result;
    }
}

解释:

  • @Aspect:表示这是一个切面类
  • @Around:环绕通知,在目标方法的前后都会执行,后面的表示生效的路径
  • ProceedingJoinPoint.proceed() 让原始方法执行

2. Spring AOP 核心概念

2.1 切点(Pointcut)

切点(Pointcut)也称为“切入点”

Pointcut 的作用是提供一组规则,告诉程序对哪些方法生效,即@Around方法中的值,称为切点表达式:

2.2 连接点(Join Point)

满足切点表达式规则的方法,就叫连接点

2.3 通知(Advice)

通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法),比如上面程序中记录消耗时间就是通知:

2.4 切面(Aspect)

切面就是 切点 + 通知,即整个方法

切面所在的类称为切面类

3. 通知类型

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

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

@Slf4j
@Component
@Aspect
public class AspectDemo1 {
    @Before("execution(* com.example.j20240419springaop.controller.*.*(..))")
    public void doBefore() {
        log.info("doBefore");
    }

    @After("execution(* com.example.j20240419springaop.controller.*.*(..))")
    public void doAfter() {
        log.info("doAfter");
    }

    @AfterReturning("execution(* com.example.j20240419springaop.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("doAfterReturning");
    }

    @AfterThrowing("execution(* com.example.j20240419springaop.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("doAfterThrowing");
    }

    @Around("execution(* com.example.j20240419springaop.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("before doAround");

        Object result = joinPoint.proceed();

        log.info("after doAround");

        return result;
    }
}

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public void test1() {
        log.info("run test1");
    }

    @RequestMapping("/t2")
    public void test2() {
        int a = 10 / 0;
        log.info("run test2");
    }
}

运行程序访问t1:

可以看到 @Around 的优先级最高,应为没有发生异常,所以 @AfterThrowing中的代码没有执行。

访问t2:

可以看到执行了 @AfterThrowing中的代码,@AfterReturning中的代码 和 @Around 中的方法后执行的代码没有执行。

4. @PointCut

在我们上面的代码中,同一个切点表达式被多次引用,显然让我们的代码冗余度过高,我们可以通过Spring提供的 @PointCut注解把切点表达式提取出来方便后续使用:

@Slf4j
@Component
@Aspect
public class AspectDemo1 {
    //定义切点
    @Pointcut("execution(* com.example.j20240419springaop.controller.*.*(..))")
    public void pt() {};
    @Before("pt()")
    public void doBefore() {
        log.info("doBefore");
    }

    @After("pt()")
    public void doAfter() {
        log.info("doAfter");
    }

    @AfterReturning("pt()")
    public void doAfterReturning() {
        log.info("doAfterReturning");
    }

    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        log.info("doAfterThrowing");
    }

    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("before doAround");

        Object result = joinPoint.proceed();

        log.info("after doAround");

        return result;
    }
}

如果定义的切点要在其他类使用,需要把权限设置为 public 并且在其他类中引用时填写全限定名称:

@Slf4j
@Component
@Aspect
public class AspectDemo2 {

    @Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doBefore() {
        log.info("demo2,doBefore");
    }

    @After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doAfter() {
        log.info("demo2,doAfter");
    }
}

如果多个切面应用于同一个方法,优先级是按照名称字典序排序:

可以通过 @order 注解定义优先级,括号中的值越小优先级越大:

@Order(100)
@Slf4j
@Component
@Aspect
public class AspectDemo2 {

    @Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doBefore() {
        log.info("demo2,doBefore");
    }

    @After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doAfter() {
        log.info("demo2,doAfter");
    }
}
@Order(10)
@Slf4j
@Component
@Aspect
public class AspectDemo3 {
    @Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doBefore() {
        log.info("demo3,doBefore");
    }

    @After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doAfter() {
        log.info("demo3,doAfter");
    }
}
@Order(1)
@Slf4j
@Component
@Aspect
public class AspectDemo4 {
    @Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doBefore() {
        log.info("demo4,doBefore");
    }

    @After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
    public void doAfter() {
        log.info("demo4,doAfter");
    }
}

5. 切点表达式

5.1 execution表达式

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

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

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

注意:

  • * 匹配任意一个元素(返回类型,包,类名,方法,方法参数)
  • .. 匹配多个连续的任意符号, 如包,类型,参数,不能匹配方法名

5.2 @annotation

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

实现步骤:

  1. 编写自定义注解
  2. 使用 @annotation 表达式描述切点
  3. 在连接点的方法上添加自定义注解
//限定注解的使用类型和生命周期
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {

}
@Slf4j
@Component
@Aspect
public class AspectDemo5 {
    @Around("@annotation(com.example.j20240419springaop.config.MyAspect)")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("before  ");
        Object result = joinPoint.proceed();
        log.info("after " );
        return result;
    }
}

只要添加上@MyAspect 注解的方法,就会应用到 doAround 方法。

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

  • 基于注解 @Aspect 
  • 基于自定义注解
  • 基于Spring API(通过xml配置的⽅式,⾃从SpringBoot⼴泛使⽤之后,这种⽅法⼏乎看不到了)
  • 基于代理实现