SpringBoot-AOP

一、AOP 概述

AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程)也可以叫做面向方法的编程。

AOP主要是在程序运行期间在不修改源代码的基础上对已有方法进行增强,这种面向指定的一个或多个方法进行编程,我们就称之为面向切面编程

其实,AOP(面向切面编程)和OOP(面向对象编程)一样,它们都仅仅是一种编程思想,而动态代理技术是这种思想最主流的实现方式。Spring的AOP就是在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行功能增强。

二、AOP的使用

1.依赖引入

首先在pom.xml中导入AOP的依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.核心概念

  • 连接点 JoinPoint

    可以被AOP控制的那些方法都是连接点

  • 通知 Advice

    就是指的为原来方法加强的功能,比如我为A方法加入了B功能,这个B功能就是通知

  • 切入点 PointCut

    通知具体加入的位置,通常会通过一个切入点表达式来描述切入点

  • 切面 Aspect

    描述通知与切入点的对应关系(通知+切入点)

  • 目标对象 Target

目标对象指的就是通知所应用的对象,我们就称之为目标对象。

原理:

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

3.入门程序

1
2
3
4
5
6
@Service
public class Test {
public void hello(String username) {
System.out.println("Hello World");
}
}
1
2
3
4
5
6
7
8
9
10
@Aspect // 切面类注解
@Component
public class TestAOP {
@Around("execution(* com.example.study.Test.hello(String))") // 切点表达式
public void beforeAddUser(ProceedingJoinPoint pjp) {
System.out.println("Before");
pjp.proceed();
System.out.println("After");
}
}

4.通知类型

Spring中AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行,注意使用proceed()方法
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("")
public void before(JoinPoint joinPoint){
log.info("before ...");
}

//环绕通知
@Around("")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}

//后置通知
@After("")
public void after(JoinPoint joinPoint){
log.info("after ...");
}

//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}

//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}

注意事项

@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行

@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。

@PointCut注解

每次都要写切入点表达式很麻烦,可以用@PointCut抽象出来代替

1
2
3
4
5
6
7
8
9
10
11
12
 //切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.example.study.Test.*.*(..))")
private void pt(){

}

//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
……

当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public.

引用的具体语法为:全类名.方法名()

1
@Before("Test.pt()")

5.通知顺序

当多个切面类的切入点都匹配了同一个目标方法时,他们都会执行,并且有顺序:

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行

@Order注解

用来控制通知顺序的

1
2
3
4
@Order(2)  //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class AOPClass{

}

6.切入点表达式

1) execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

1
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)
  • 包名.类名: 可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

可以使用通配符描述切入点

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式的语法规则:

  1. 方法的访问修饰符可以省略
  2. 返回值可以使用*号代替(任意返回值类型)
  3. 包名可以使用*号代替,代表任意包(一层包使用一个*
  4. 使用..配置包名,标识此包以及此包下的所有子包
  5. 类名可以使用*号代替,标识任意类
  6. 方法名可以使用*号代替,表示任意方法
  7. 可以使用 * 配置参数,一个任意类型的参数
  8. 可以使用.. 配置参数,任意个任意类型的参数

可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

切入点表达式示例

  • 省略方法的修饰符号

    1
    execution(void org.example.service.impl.DeptServiceImpl.delete(java.lang.Integer))
  • 使用*代替返回值类型

    1
    execution(* org.example.service.impl.DeptServiceImpl.delete(java.lang.Integer))
  • 使用*代替包名(一层包使用一个*

    1
    execution(* org.example.*.*.DeptServiceImpl.delete(java.lang.Integer))
  • 使用..省略包名

    1
    execution(* org..DeptServiceImpl.delete(java.lang.Integer))    
  • 使用*代替类名

    1
    execution(* org..*.delete(java.lang.Integer))   
  • 使用*代替方法名

    1
    execution(* org..*.*(java.lang.Integer))   
  • 使用 * 代替参数

    1
    execution(* org.example.service.impl.DeptServiceImpl.delete(*))
  • 使用..省略参数

    1
    execution(* com..*.*(..))

2) @annotation

借助注解来描述切入点

先自定义注解

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

然后在要做为连接点的方法上添加自定义注解

切入点表达式:

1
@Before("@annotation(org.example.annotation.MyAnnotationy)")

7.连接点

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

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

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

1
2
3
4
5
6
7
8
9
10
11
//获取目标类名
String name = pjp.getTarget().getClass().getName();

//目标方法名
String methodName = pjp.getSignature().getName();

//获取方法执行时需要的参数
Object[] args = pjp.getArgs();

//执行原始方法
Object returnValue = pjp.proceed();

三、AOP进阶

1.Signature

签名类,用来为应用程序提供数字签名算法功能。数字签名用于确保数字数据的验证和完整性。

1.1 在AOP中的作用

  • 拦截器决策: 拦截器拦截连接点时,会利用 Signature 对象来判断连接点是否符合拦截条件。例如,一个拦截器可能只拦截特定方法或特定参数类型的连接点,此时 Signature 对象就充当了裁判的角色,决定拦截器是否出手。
  • 连接点信息获取: Signature 对象作为连接点信息的重要载体,为我们提供了获取连接点相关信息的手段。通过 Signature 对象,我们可以轻松获取连接点所在类、方法名称、返回类型、参数类型等信息。
  • 连接点匹配: 在 Spring AOP 中,连接点匹配是至关重要的,而 Signature 对象便是连接点匹配的利器。它可以帮助我们判断两个连接点是否匹配,以便决定是否应用相同的拦截器。

1.2 多种子类

  • MethodSignature: 当连接点指向一个方法时,Signature 对象披上 MethodSignature 的外衣,它拥有 getName()getReturnType()getParameterTypes() 等方法,分别揭示了方法名称、返回类型和参数类型的信息。
  • ConstructorSignature: 当连接点指向一个构造函数时,Signature 对象摇身一变为 ConstructorSignature,它提供了 getDeclaringClass()getParameterTypes() 等方法,分别用于获取构造函数所属的类和参数类型的信息。
  • FieldSignature: 当连接点指向一个字段时,Signature 对象又幻化成了 FieldSignature,它可以通过 getName()getType() 等方法获取字段名称和字段类型的信息。

2.JoinPoint&ProceedingJoinPoint

在Spring AOP中,JoinPointProceedingJoinPoint用于在切面中获取方法的相关信息以及控制方法的执行。ProceedingJoinPointJoinPoint 的子接口,它专门用于环绕通知(Around advice)。

2.1 JoinPoint

1
2
joinPoint.getArgs(); // 获取调用方法传入的参数
joinPoint.getSignature(); //获取方法签名

获取了Signature对象,可以通过Signature对象获得更多的信息

1
2
3
4
5
6
7
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
methodSignature.getParameterNames(); // 获取参数名
methodSignature.getParameterTypes(); // 获取参数类型
methodSignature.getReturnType(); // 获取返回值类型
methodSignature.getName(); // 获取方法名称
methodSignature.getMethod(); //获取方法
methodSignature.getMethod().getAnnotation(AutoFill.class); //获取注解

2.2 ProceedingJoinPoint

1
2
3
// 主要是proceed()方法,用于执行被拦截的方法
// 如果不使用proceed()方法,那么被拦截的方法不会执行
proceedingJoinPoint.proceed();

3.获取返回值和异常

借助@AfterReturning注解获取返回值

1
2
3
4
@AfterReturning(pointcut = "pointCut()",returning = "returnValue") // returning参数填写要接收返回值的对象,名
public void test(JoinPoint joinPoint, Object returnValue ){
System.out.println(returnValue);
}

借助@AfterThrowing注解获取异常

1
2
3
4
@AfterThrowing(value = "autoFillPointCut()",throwing = "exception") // 用于接收异常的对象
public void afterThrowing(JoinPoint joinPoint, Exception exception) { // 该异常类型会限制接收到的异常的类型
exception.printStackTrace();
}