一篇了解 :Spring AOP 及其使用

目录

一、AOP概述

二、Spring AOP 快速使用

2.1 引入AOP依赖

2.2 编写AOP程序        

三、Spring AOP 详解

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

3.1.2 连接点(Join Point)

3.1.3 通知(Advice)

3.1.4 切面(Aspect)

3.2 通知类型

3.3 @PointCut

3.4 切面优先级 @Order

3.5 切面表达式

3.5.1 execution表达式

3.5.2 @annotation

3.5.2.1 自定义注解 @MyAspect

3.5.2.2 切面类

3.5.2.3 添加自定义注解


一、AOP概述

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

AOP:Aspect Oriented Programming(面向切面编程)。

切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程。拦截器、统一数据返回格式和统一异常处理, 也是AOP思想的一种实现。

AOP是一种思想,是对某一类事情的集中处理。它的实现方法有很多,有Spring AOP,也有AspectJ、CGLIB等。Spring AOP是其中的一种实现方式。

二、Spring AOP 快速使用

2.1 引入AOP依赖

在pom.xml文件中添加配置:
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 编写AOP程序        

记录Controller中每个方法的执行时间:
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.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 详解

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

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

3.1.2 连接点(Join Point)

满足切点表达式规则的方法,就是连接点。也就是可以被AOP控制的方法。
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 通知类型

Spring中AOP的通知类型有以下几种:
  • @Around: 环绕通知,此注解标注的通知方法在目标方法前、后都被执行。
  • @Before: 前置通知,此注解标注的通知方法在目标方法前被执行。
  • @After: 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行。
  • @AfterReturning: 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
  • @AfterThrowing: 异常后通知,此注解标注的通知方法发生异常后执行。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo {

 //前置通知
 @Before("execution(* com.example.demo.controller.*.*(..))")
 public void doBefore() {
 log.info("执⾏ Before ⽅法");
 }

 //后置通知
 @After("execution(* com.example.demo.controller.*.*(..))")
 public void doAfter() {
 log.info("执⾏ After ⽅法");
 }

 //返回后通知
 @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
 public void doAfterReturning() {
 log.info("执⾏ AfterReturning ⽅法");
 }

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

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

测试:

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@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.example.demo.controller.*.*(..)), Spring提供了 @PointCut 注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可。
上述代码就可以修改为:
Slf4j
@Aspect
@Component
public class AspectDemo {

 //定义切点(公共的切点表达式)
 @Pointcut("execution(* com.example.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。引用方式为: 全限定类名.方法名()
@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,如下使用:
@Aspect
@Component
@Order(1)
public class AspectDemo2 {
 //...代码省略
}
@Aspect
@Component
@Order(2)
public class AspectDemo3 {
 //...代码省略
}
@Aspect
@Component
@Order(3)
public class AspectDemo4 {
 //...代码省略
}
@Order 注解标识的切面类, 执型顺序如下:
  • @Before 通知:数字越小先执行
  • @After 通知:数字越大先执行
@Order 控制切面的优先级, 先执行优先级较高的切面, 再执行优先级较低的切面, 最终执行目标方法。

3.5 切面表达式

切点表达式常见有两种表达方式:
  1. execution(RR):根据方法的签名来匹配
  2. @annotation(RR) :根据注解匹配

3.5.1 execution表达式

execution() 是最常用的切点表达式,用来匹配方法,语法为:
execution(< 访问修饰符 > < 返回类型 > < 包名 . 类名 .方 ( 方法参数 )> < 异常 >)
其中,访问修饰符和异常可以省略。
切点表达式支持通配符表达:
1.   * :匹配任意字符,只匹配一个元素(返回类型, 包, 类名, 方法或者方法参数)
  • 包名使用 * 表示任意包(一层包使用以个*)
  • 类名使用 * 表示任意类
  • 返回值使用 * 表示任意返回值类型
  • 方法名使用 * 表示任意方法
  • 参数使用 * 表示一个任意类型的参数
2.   .. :匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数
  • 使用 .. 配置包名,标识此包以及此包下的所有子包
  • 可以使用 .. 配置参数,任意个任意类型的参数
切点表达式示例:
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..*(..))

3.5.2 @annotation

execution表达式更适用有规则的,如果要匹配多个无规则的方法,比如:TestController中的t1()
和UserController中的u1()这两个方法。此时使用execution这种切点表达式来描述就不是很方便。 
我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点。
实现步骤:
  • 编写自定义注解
  • 使用 @annotation 表达式来描述切点
  • 在连接点的方法上添加自定义注解

准备测试代码:

@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";
 }
}
3.5.2.1 自定义注解 @MyAspect
创建一个注解类(和创建Class文件一样的流程, 选择Annotation就行)
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 {
}
@Target 标识了 Annotation 所修饰的对象范围,即该注解可以用在什么地方,常用取值:
ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明
ElementType.METHOD: 描述方法
ElementType.PARAMETER: 描述参数
ElementType.TYPE_USE: 可以标注任意类型
@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 生 效,示例如下:
@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 ...");
 }
}
3.5.2.3 添加自定义注解
在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解 @MyAspect , 其
它方法不添加:
@MyAspect
@RequestMapping("/t1")
public String t1() {
 return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1(){
 return "u1";
}

测试接口,查看日志:发现切面通知被执行了。

Spring AOP的实现方式(常见面试题)
  1.  基于注解 @Aspect 
  2. 基于自定义注解 (参考自定义注解 @annotation 部分的内容)
  3. 基于Spring API (通过xml配置的方式, 自从SpringBoot 广泛使用之后, 这种方法几乎看不到了)
  4. 基于代理来实现(更加久远的一种实现方式, 写法笨重, 不建议使用)

参考链接https://cloud.tencent.com/developer/article/2032268

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值