1. Spring AOP 是什么?
1.1 AOP 是什么?
AOP
是面向切面编程(Aspect-Oriented Programming)的缩写。AOP
是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,提供一种更好的代码模块化和可维护性,换句话说,就是对某一类事情的集中处理。
横切关注点指的是在应用程序中横跨多个模块或层的功能,例如日志记录、事务管理、安全性、缓存、异常处理等。
例如:在不使用AOP
的情况下,每个Controller
都要写一遍用户登录验证。当功能越来越多的时候,需要在每个功能里都写同样的代码,这就提高了代码的修改和维护的成本。对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP
来统一处理了。
1.2 Spring Boot AOP 是什么?
Spring Boot AOP
是基于Spring
框架和Spring AOP
的AOP
实现方式,专门针对Spring Boot
应用程序提供的一种简化配置和使用的方式。
Spring AOP
是Spring
框架提供的一种AOP
实现方式。AOP
是一种编程范式,而Spring AOP
是Spring
框架对AOP
的具体实现。
2. AOP 的组成
AOP的组成有:切面、连接点、切点、通知
2.1 切面(Aspect)
切面是横跨一个或多个类的模块化单元,它定义了与横切关注点相关的行为。切面由切点、通知组成,它通常以类的形式表示。
2.2 切点(Pointcut)
切点(Pointcut)在面向切面编程(AOP)中起到了选择性拦截和应用切面的作用,它可以被理解为一种规则。
比如:有一个用户对他人的文章进行评价,这时候需要检测该用户是否登录,只有登录后才能评价。这就是切点,它相当于一种规则。
2.3 通知(Advice)
通知是切面的一部分,它是在特定切点处执行的具体操作。切面由切点和通知组成,切点用于定义在哪些连接点上应用通知的规则,而通知定义了在这些连接点上执行的具体操作。在方法上添加相应的注解就表示相应的通知:
- 前置通知(@Before):在目标方法执行之前执行的通知。可以在该通知中进行一些准备工作或参数验证。
- 后置通知(@After):在目标方法执行之后执行的通知。可以在该通知中进行一些清理工作或记录日志。
- 返回通知(@AfterReturning):在目标方法成功执行并返回结果后执行的通知。可以在该通知中对方法的返回值进行处理或执行其他操作。
- 异常通知(@AfterThrowing):在目标方法抛出异常后执行的通知。可以在该通知中处理异常或执行相应的异常处理逻辑。
- 环绕通知(@Around):在目标方法执行之前和之后都执行的通知。它可以完全控制目标方法的执行过程,包括是否执行目标方法以及如何处理返回值和异常。
2.4 连接点(Join Point)
连接点是指在应用程序执行过程中的特定点或事件,例如方法的调用、方法的执行、异常的抛出、属性的访问等。**它是AOP中可以插入切面逻辑的地方。**具体来说,连接点是在程序执行期间可以被拦截的点。当程序运行到某个连接点时,AOP
框架可以介入并执行相应的切面逻辑。
3. Spring Boot AOP 的演示
3.1 添加 Spring Boot AOP 依赖
添加如下的代码在 pom.xml
文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.11</version>
</dependency>
3.2 定义切面与切点
切面一般是一个类,它里面有切点与通知,下面是切点的定义方式:
@Aspect //切面
@Component //不能省略,要在项目启动的时候启动
public class UserAOP {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
}
为什么是空方法?因为这个方法的作用是当一个标识
。
@Pointcut
注解里的表达式就是规则,它的含义:
@Pointcut
注解用于定义切点,即被切入的位置。"execution(* com.example.demo.controller.UserController.*(..))"
是切点表达式的内容。execution()
是切点指示符,表示匹配方法的执行。*
表示匹配任意返回类型的方法。com.example.demo.controller.UserController
是目标类的全限定名,表示匹配该类。*
表示匹配类中的任意方法名。(..)
表示匹配任意参数的方法。
综合起来,这个切点表达式的意思是匹配 com.example.demo.controller.UserController
类中的所有方法,无论方法的返回类型和参数如何。换言之,就是UserController
中的所有方法都被拦截了。
execution
里的语法规则:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
其中,修饰符、异常部分可以省略,其它的不能省略。
*
:匹配任意字符,可以匹配零个或多个字符。在切点表达式中,*
通配符可以用于匹配包、类或方法的名称中的任意字符部分。..
:匹配任意字符,可以匹配零个或多个字符、类或包路径。在切点表达式中,..
通配符可以用于匹配类或包路径的任意部分,例如com.example..
表示匹配com.example
包及其子包下的所有内容。+
:表示按照类型匹配指定类的所有子类。在切点表达式中,+
通配符用于表示指定类的所有子类,包括该类本身。例如,com.example.demo.controller.UserController+
表示匹配UserController
类及其所有子类。
3.3 定义通知
方法在被拦截后需要做处理,处理就是通知。
(1)前置通知 + 后置通知
@Aspect //切面
@Component //不能省略,要在项目启动的时候启动
public class UserAOP {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("执行前置通知" + LocalDateTime.now());
}
//后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行后置通知" + LocalDateTime.now());
}
}
@Before
、@After
等注解中的属性表示需要匹配的连接点(这里就是),以确定在哪些位置要应用切面的通知。
下面是Controller
的代码:
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/hi")
public String hi(){
System.out.println("执行 UserController 的 hi() 方法");
return "do user";
}
@RequestMapping("/login")
public String login(){
System.out.println("执行 UserController 的 login() 方法");
return "do login";
}
}
运行并访问:
(2)环绕通知
@Aspect //切面
@Component //不能省略,要在项目启动的时候启动
public class UserAOP {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始执行环绕通知:");
Object obj = joinPoint.proceed();
System.out.println("结束环绕通知");
//这里的 obj 就是 连接点方法的返回值,可以对其进行修改
obj = "do Around " + obj;
System.out.println(obj);
return obj;
}
}
在方法doAround
中,参数ProceedingJoinPoint joinPoint
表示连接点(即目标方法),它可以在环绕通知中被调用和操作。
代码的执行顺序如下:
- 当连接点被触发时,即目标方法即将执行前,环绕通知
doAround
会被执行。 - 第一行代码输出"开始执行环绕通知:",表示环绕通知开始执行。
joinPoint.proceed()
调用表示继续执行目标方法,此行代码会触发目标方法的执行,并将目标方法的返回值存储在obj
变量中。- 第三行代码输出"结束环绕通知",表示环绕通知的执行已经结束。
- 下一行的代码对目标方法的返回值进行修改,将其改为"do Around " + obj,并将修改后的值赋给
obj
变量。 - 接着,输出修改后的
obj
的值。 - 最后,将修改后的
obj
返回作为目标方法的结果。
结果:
(3)前置、后置通知 + 环绕通知
@Aspect //切面
@Component //不能省略,要在项目启动的时候启动
public class UserAOP {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("执行前置通知" + LocalDateTime.now());
}
//后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行后置通知" + LocalDateTime.now());
}
//环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始执行环绕通知:");
Object obj = joinPoint.proceed();
System.out.println("结束环绕通知");
//这里的 obj 就是 连接点方法的返回值,可以对其进行修改
obj = "do Around " + obj;
System.out.println(obj);
return obj;
}
}
结果:
(4)返回通知
//返回通知
@AfterReturning("pointcut()")
public void AfterReturning(){
System.out.println("执行返回通知");
}
结果:
(5)异常通知
//异常通知
@AfterThrowing("pointcut()")
public void AfterThrowing(){
System.out.println("执行异常通知");
// 可以在此处进行异常处理逻辑
}
@RequestMapping("/login")
public String login(){
System.out.println("执行 UserController 的 login() 方法");
throw new ArrayIndexOutOfBoundsException();
}
结果: