【JavaWeb后端学习笔记】Spring AOP面向切面编程

AOP

1、Spring AOP概述

AOP:Aspect Oriented Programming,面向特定方法编程。

AOP是通过动态代理技术实现的。SpringAOP是Spring框架的高级技术,旨在管理Bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。

假设场景:现在需要优化一个刚开发好的系统,首先要记录每一个业务方法执行的时长,也就是在业务开始时记录开始时间,业务执行完毕时记录结束时间,时间差就是该业务的执行时间。只要在每一个业务方法中增加这个操作,就能记录所有业务方法的执行时间。但是由于业务可能非常多,这样做相当繁琐,工作量也大。这是可以通过AOP面向切面编程来优化该操作。

AOP会通过动态代理技术对原有方法进行改造。AOP首先会定义一个模板方法,在模板方法内定义在业务方法执行前记录开始时间,然后执行业务方法,业务方法执行后记录结束时间,获得时间差。这样就不需要对每个业务方法进行改造,而是通过AOP来改造了。

2、SpringAOP快速入门

以记录业务方法执行时间为例。

SpringAOP的使用分一下几步:

  1. 引入SpringAOP依赖:
html 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写AOP程序。首先定义一个类,在类上加@Component注解与@Aspect注解。@Component注解表明将这个类交给IOC容器管理,@Aspect声明当前类为AOP类。然后定义一个方法模板,在方法模板中记录开始时间,调用原始方法,再记录结束时间。注意给方法模板传入ProceedingJoinPoint类对象,通过该对象的proceed()方法能够调用原始方法。
  2. 给方法模板加切入点。指定哪些包里的那些接口和类中的方法需要通过AOP改造。

最后的简单实现如下:

java 复制代码
@Slf4j
@Component // 交给IOC容器管理
@Aspect // 声明当前类为AOP类
public class TimeAspect {

    @Around("execution(* com.wrj.controller.*.*(..))") // 切入点表达式。指定com.wrj.controller下的所有接口和类中的所有方法都被改造
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 记录开始时间
        long begin = System.currentTimeMillis();
        // 2. 调用原始方法运行
        Object result = joinPoint.proceed();
        // 3. 记录结束时间,计算方法耗时
        long end = System.currentTimeMillis();
        long times = end - begin;
        log.info(joinPoint.getSignature() + "方法执行耗时:{}", times + "ms");
        return result;
    }
}

3、SpringAOP核心概念

SpringAOP中有五大核心概念:

  1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息);
  2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法);
  3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用;
  4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点);
  5. 目标对象:Target,通知所应用的对象。

通过上面SpringAOP快速入门案例进行简单解释。

通知:在上面的案例中,需要记录方法的执行时间,因此需要先记录开始时间,再记录结束时间,最后计算执行时长,这三步共性代码就是通知。

切入点:案例中方法模板上加了一个注解,注解中写了一个切入点表达式。这个表达式是指定在com.wrj.controller包下的所有接口和类中的方法都被AOP改造。匹配的条件就是切入点。

bash 复制代码
@Around("execution(* com.wrj.controller.*.*(..))")

切面:切面是切入点和通知的组合。描述的是切入点与通知的关系。即切入点匹配到的方法需要执行通知中的公共代码。

连接点:在上面的案例中com.wrj.controller下的所有方法都是连接点,因为都可以被AOP改造。

目标对象:目标对象指通知应用的对象,此处指的是com.wrj.controller包下的类对象或接口实现类对象。

4、通知类型

通知类型有5种:

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

在使用环绕通知@Around时需注意注意:

  • @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。

5、通知顺序

如果有多个切面的切入点都匹配到了同一个目标方法,目标方法运行时,多个通知方法都会被执行。这时需要考虑通知顺序。

  1. 不同的切面类中,默认按照切面类的类名字排序:
    • 目标方法前的通知方法:字母排名靠前的先执行
    • 目标方法后的通知方法:字母排名靠前的后执行
  2. 用@Order(数字)加在切面类 上来控制顺序
    • 目标方法前的通知方法:数字小的先执行
    • 目标方法后的通知方法:数字小的后执行

6、切入点表达式

切入点表达式有两种形式:execution方式与@annotation方式

6.1 execution方式

execution方式主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配。

语法为:

java 复制代码
execution(访问修饰符  返回值  包名.类名.方法名(方法参数全类名) throws 异常)

其中有几处可省略:

· 访问权限修饰符:可省略(比如:public、protected)

· 包名.类名.:可省略(注意类名后面也有点,不建议省略)

· throws异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

在切入点表达式中,可以使用通配符描述切入点

java 复制代码
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分。
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数。
  1. 在通知类型注解中通过切入点表达式设置切入点。
java 复制代码
@Component
@Aspect
@Slf4j
public class DemoAspect {

    @Before("execution(* com.wrj.controller.*.*(..))")
    public void testBefore() {
        log.info("Before...前置通知");
    }
    
    @Around("execution(* com.wrj.controller.*.*(..))")
    public Object testAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around...环绕通知,原始方法执行前逻辑");
        Object result = joinPoint.proceed();
        log.info("Around...环绕通知,原始方法执行后逻辑");
        return result;
    }

    @After("execution(* com.wrj.controller.*.*(..))")
    public void testAfter() {
        log.info("Before...后置通知");
    }
}
  1. 提取公共切入点。同样通过切入点表达式设置切入点,但是提取公共切入点。可以通过定义一个空方法,在空方法上加入注解@PointCut指定要抽取的公共的切入点。
java 复制代码
@Component
@Aspect
@Slf4j
public class DemoAspect {
    
    // 1. 定义一个空方法,加上@PointCut注解抽取公共切入点
    @Pointcut("execution(* com.wrj.controller.*.*(..))")
    public void pt() {
    }

    @Before("pt()") // 2. 在通知类型注解中加入抽取的切入点
    public void testBefore() {
        log.info("Before...前置通知");
    }

    @Around("pt()")
    public Object testAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around...环绕通知,原始方法执行前逻辑");
        Object result = joinPoint.proceed();
        log.info("Around...环绕通知,原始方法执行后逻辑");
        return result;
    }

    @After("pt()")
    public void testAfter() {
        log.info("Before...后置通知");
    }
}
  1. 引用其他切面类的切入点。前提是有一个切面类中已经抽取除了公共切入点。并且其他的切面类有足够的访问权限访问该设置了公共切入点的方法。直接在通知类型注解中通过 包名.类名.方法名() 的方式引用。
java 复制代码
@Slf4j
@Component // 交给IOC容器管理
@Aspect // 声明当前类为AOP类
public class TimeAspect {

    @Around("com.wrj.aspect.DemoAspect.pt()") // 引用com.wrj.aspect包下,DemoAspect类中的pt()方法上设置的切入点
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 记录开始时间
        long begin = System.currentTimeMillis();
        // 2. 调用原始方法运行
        Object result = joinPoint.proceed();
        // 3. 记录结束时间,计算方法耗时
        long end = System.currentTimeMillis();
        long times = end - begin;
        log.info(joinPoint.getSignature() + "方法执行耗时:{}", times + "ms");
        return result;
    }
}
  1. 通过 ||、! 、&& 逻辑运算符连接多个切入点表达式。
java 复制代码
@Component
@Aspect
@Slf4j
public class DemoAspect {

    // 使用 || 运算连接两个切入点表达式
    @Pointcut("execution(String com.wrj.controller.DemoController.demoFilter()) ||" +
              "execution(String com.wrj.controller.DemoController.demo())")
    public void pt() {
    }

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

6.2 @annotation方式

  1. 自定义注解。在使用@annotation方式前需要自定义一个注解。自定义注解为空注解即可,不需要定义属性。
java 复制代码
@Retention(RetentionPolicy.RUNTIME) // @Retention描述注解什么时候生效。RetentionPolicy.RUNTIME表示运行时生效
@Target(ElementType.METHOD) // @Target描述作用在哪些地方。ElementType.METHOD表示作用在方法上
public @interface MyAnnotation {
}
  1. 通过@annotation方式编写切入点表达式。在@annotation中写入自定义注解的全类名。含义是匹配加了自定义注解的方法。
java 复制代码
@Component
@Aspect
@Slf4j
public class DemoAspect {

    // 在@annotation中写入自定义注解的全类名
    @Pointcut("@annotation(com.wrj.annotation.MyAnnotation)")
    public void pt() {
    }

    @Before("pt()")
    public void testBefore() {
        log.info("Before...前置通知");
    }
}
  1. 在需要被AOP改动的方法上加上自定义注解。
java 复制代码
@RestController
@RequestMapping("/demo")
@Slf4j
public class DemoController {

    @MyAnnotation // 加自定义注解
    @GetMapping
    public String demoFilter() {
        System.out.println("demoFilter接口执行...");
        return "执行结束";
    }

    @MyAnnotation // 加自定义注解
    @GetMapping
    public String demo() {
        System.out.println("demoFilter接口执行...");
        return "执行结束";
    }
}

execution方式和@annotation方式可以混用。

7、连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

对于@Around 通知,获取连接点信息只能使用 ProceedingJoinPoint。

对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型。

如要使用原始方法,需要自己在模板方法形参中指定JoinPoint或ProceedingJoinPoint对象。

一些常用的方法:

java 复制代码
// 1. 获取 目标对象类名
String className = joinPoint.getTarget().getClass().getName(); 
// 2. 获取 目标方法的方法名
String methodName = joinPoint.getSignature().getName();
// 3. 获取 目标方法运行时传入的参数
Object[] args = joinPoint.getArgs();
// 4. 放行 目标方法执行并且返回目标方法运行的返回值
Object result = joinPoint.proceed();

注意:若模板方法需要返回原始方法返回的值,则模板方法的返回值类型需定义为Object。

相关推荐
雨中奔跑的小孩38 分钟前
爬虫学习案例3
爬虫·python·学习
Hello.Reader1 小时前
Spring Retry 与 Redis WATCH 结合实现高并发环境下的乐观锁
java·redis·spring
冷环渊1 小时前
React基础学习
前端·学习·react.js
T.O.P112 小时前
Spring&SpringBoot常用注解
java·spring boot·spring
今天我又学废了3 小时前
学习记录,隐式对象,隐式类
学习
m0_748234343 小时前
【SpringMVC】基于 Spring 的 Web 层MVC 框架
前端·spring·mvc
#HakunaMatata3 小时前
Java 中 List 接口的学习笔记
java·学习·list
xiangzhihong83 小时前
Spring Boot集成Knife4j文档工具
spring boot·spring
小雄abc3 小时前
决定系数R2 浅谈三 : 决定系数R2与相关系数r的关系、决定系数R2是否等于相关系数r的平方
经验分享·笔记·深度学习·算法·机器学习·学习方法·论文笔记
Magnetic_h3 小时前
【iOS】OC高级编程 iOS多线程与内存管理阅读笔记——自动引用计数(三)
笔记·学习·ios·objective-c·cocoa