一: Spring AOP
备注:之前学习 Spring 学到 AOP 就去梳理之前学习的知识点了,后面因为各种原因导致 Spring AOP 的博客一直搁置。。。。。。下面开始正式的讲解。
学习完 Spring 的统一功能后,我们就进入了 Spring AOP 的学习。Spring 的第一大核心是 IoC,而 AOP 则是 Spring 框架的第二大核心,意思是面向切面编程。AOP 的核心思想是针对特定问题进行集中处理,例如我们之前的 “登录校验” 就是一个典型的特定问题,我们当时是通过校验拦截器实现这一问题的统一处理。因此,拦截器可以被视为 AOP 思想的一种具体应用。 AOP 作为一种思想,为开发提供了结构化的方式,而拦截器则是实现这一思想的重要工具。
1.1 AOP 概述
什么是 Spring AOP?AOP 是一种思想,其实现方式有很多,包括 Spring AOP、AspectJ、CGLIB 等。其中 Spring AOP 是众多实现方式之一。学习了 Spring 的一些统一功能后,很多人可能会误认为这就完全掌握了 Spring AOP,实际上并非如此。AOP 的作用维度更为细致,能够根据包、类、方法名和参数等进行拦截,以实现更加复杂的业务逻辑。
例如,在一个项目中,我们开发了多个业务功能,但发现某些业务处理的执行效率较低,需要进行优化。第一步是定位执行耗时较长的业务方法,然后针对这些方法进行优化。为了实现这一点,我们需要统计每个业务方法的执行时间。我们可以在业务方法运行前后记录开始时间和结束时间,计算得出方法的耗时。
虽然这个方法在理论上可行,但在实际开发中,每个业务模块可能包含多个接口,每个接口又有许多方法。如果要在每个方法中都添加耗时统计代码,将增加开发人员的工作量。而 AOP 可以在不修改原始代码的情况下,对特定方法进行功能增强,实现无侵入性设计,降低代码耦合度。接下来,我们将深入了解 Spring AOP 如何实现这一点。
1.2 Spring AOP 快速入门
首先需要在 pom.xml 中引入 AOP 依赖才能使用 AOP 的相关功能:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
导入依赖后开始编写 AOP 程序:
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 {
/**
* 记录方法耗时
*/
// 定义环绕通知,拦截目标方法并在其前后执行自定义逻辑。
// execution:匹配方法执行的连接点。*:通配符,匹配任意返回类型、方法名、参数。
// com.example.demo.controller.*.*(..):拦截 controller 包下所有类的所有方法,参数不限。
// ProceedingJoinPoint pjp 封装了被拦截的目标方法信息(如方法名、参数、目标对象等)。
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
// 记录方法执行开始时间
long begin = System.currentTimeMillis();
// 执行原始方法,pjp.proceed() 调用被拦截的目标方法,这是环绕通知中必须执行的步骤,否则目标方法不会运行。
Object result = pjp.proceed();
// 记录方法执行结束时间
long end = System.currentTimeMillis();
// 记录方法执行耗时
log.info(pjp.getSignature() + " 执行耗时: {}ms", end - begin);
return result;
}
}
1.3 Spring AOP 详解
1.3.1 切点
切点也称为 “切入点”,切点用于定义一组规则,告知程序需要对哪些方法进行功能增强,指定作用域和作用方式
1.3.2 连接点
连接点是指满足切点表达式规则的方法,它代表了可以被 AOP 控制的具体方法。连接点与切点之间的关系是:连接点是符合切点表达式的特定元素,而切点则可以看作是包含多个连接点的集合。例如,全体教师可以看作是一个切点,而张三老师或者李四老师就可以看作是一个连接点。
1.3.3 通知
通知是指具体要执行的工作,通常指那些重复的逻辑或共性功能,最终体现为一个方法。在 AOP 中,我们将这部分重复的代码逻辑进行抽取并单独定义,这些抽取出的代码便构成了通知的内容。
1.3.4 切面
切面是切点与通知的结合,通过切面可以描述 AOP 程序在针对哪些方法时,何时执行什么样的操作。切面不仅包含通知逻辑的定义,还包括连接点的定义。通常,将包含切面的类称为切面类,这些类通过 @Aspect 注解进行标识。
1.4 通知类型
Spring 中 AOP 的通知类型有以下几种:
注解 | 描述 |
---|---|
@Around | 环绕通知,标注的方法在目标方法前后都会被执行。 |
@Before | 前置通知,标注的方法在目标方法之前执行。 |
@AfterReturning | 返回后通知,标注的方法在目标方法正常返回后执行,异常时不会执行。 |
@After | 后置通知,标注的方法在目标方法之后执行,无论是否有异常都会执行。 |
@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;
}
}
- 访问 http://127.0.0.1:8080/test/t1,此时是正常的运行情况:
- 访问 http://127.0.0.1:8080/test/t2,此时是异常的运行情况:
此处需要注意 @AfterReturning 方法不会被执行,而 @AfterThrowing 方法则会被执行。此外,在 @Around 环绕通知中只有头部部分会被执行。下面是别的注意事项:
- 一个切面类可以定义多个切点。
- @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 方法以便执行原始方法,而其他通知不需要考虑目标方法是否执行。
- @Around 环绕通知方法的返回值必须指定为 Objec 以便接收原始方法的返回值;否则在原始方法执行完毕后无法获取返回值。
1.5 @PointCut
上述代码存在一个问题,即大量重复使用切点表达式 execution(* com.example.demo.controller..(…))。为了解决这个问题,Spring 提供了 @Pointcut 注解,可以将公共的切点表达式提取出来。这样,在需要使用时只需引用该切入点表达式即可。因此,可以对代码进行如下修改:
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 {
// 定义切点(公共的切点表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 After 方法");
}
// 返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
log.info("执行 AfterReturning 方法");
}
// 抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
log.info("执行 doAfterThrowing 方法");
}
// 添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around 方法开始执行");
Object result = joinPoint.proceed();
log.info("Around 方法结束执行");
return result;
}
}
当切点定义使用 private 修饰时,该切点只能在当前切面类中使用。如果其他切面类也需要使用这个切点定义,则需要将其访问修饰符改为 public。引用时的格式为:全限定类名.方法名()。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
// 前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
}
1.6 切面优先级 @Order
在一个项目中如果定义了多个切面类,并且它们的多个切入点都匹配到同一个目标方法,当该方法执行的时候,这些切面类中的通知方法将依次执行,那么此时这几个通知方法的执行顺序是怎么样的呢?我们看看下面的这段代码:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo2 -> After 方法");
}
}
@Slf4j
@Aspect
@Component
public class AspectDemo3 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo3 -> Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo3 -> After 方法");
}
}
@Slf4j
@Aspect
@Component
public class AspectDemo4 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo4 -> Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo4 -> After 方法");
}
}
访问 http://127.0.0.1:8080/test/t1:
通过上述程序的运行结果可以看出:在存在多个切面类的情况下,切面通知的执行顺序默认是按照类名的字母顺序排列的:对于 @Before 通知,字母排名靠前的切面会优先执行;对于 @After 通知,字母排名靠前的切面则会后执行。然而,这种方式不便于管理,特别是当类名具有实际意义时。为了解决这个问题,Spring 提供了 @Order 注解,用于显式控制切面通知的执行顺序。使用该注解可以根据需要定义切面类的优先级。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
@Order(2)
public class AspectDemo2 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo2 -> After 方法");
}
}
@Slf4j
@Aspect
@Component
@Order(1)
public class AspectDemo3 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo3 -> Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo3 -> After 方法");
}
}
@Slf4j
@Aspect
@Component
@Order(3)
public class AspectDemo4 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt() {}
// 前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo4 -> Before 方法");
}
// 后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo4 -> After 方法");
}
}
访问 http://127.0.0.1:8080/test/t1:
通过上述程序的运行结果可以得出结论:使用 @Order 注解标识的切面类,其执行顺序如下:对于 @Before 通知,数字越小的切面优先执行;而对于 @After 通知,数字越大的切面则优先执行。换句话说,@Order 注解控制了切面的优先级,优先执行优先级较高的切面,随后执行优先级较低的切面,最终执行目标方法。
1.7 切点表达式
在上述代码中我们一直使用切点表达式来描述切点。接下来,我们将介绍切点表达式的语法。常见的切点表达式主要有两种形式:
- execution(??):根据方法的签名进行匹配。
- @annotation(??):根据方法上的注解进行匹配。
1.7.1 execution 表达式
execution() 是最常用的切点表达式,用于匹配方法,其语法为如下图所示,其中访问修饰符和异常可以省略。切点表达式支持通配符,主要包括两种:
好的,感谢你的提醒。以下是重新整理后的内容,去掉了反引号和加粗的格式:
-
使用 * :匹配任意字符,只匹配一个元素(返回类型、包、类名、方法或方法参数)。
- 包名使用 * 表示任意包。仅匹配一层包。
- 类名使用 * 表示任意类。
- 返回值使用 * 表示任意返回值类型。
- 方法名使用 * 表示任意方法。
- 参数使用 * 表示一个任意类型的参数。
-
使用 .. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意数量的参数。
- 使用 .. 配置包名,标识此包及其所有子包。
- 可以使用 .. 配置参数,表示任意个、任意类型的参数。
// 匹配 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..*(..))
1.7.2 @annotation
execution 表达式更适用有规则的,如果我们要匹配多个无规则的方法呢,这个时候我们使用 execution 这种切点表达式来描述就不是很方便了,我们可以借助自定义注解的方式以及另⼀种切点表达式 @annotation 来描述这⼀类的切点,@annotation 描述切点的方步骤如下:
- 编写自定义注解。
- 使用 @annotation 表达式来定义切点。
- 在连接点的方法上添加自定义注解。
1.7.2.1 自定义注解 @MyAspect
创建一个注解类的流程与创建普通类类似,只需选择 Annotation 类型即可。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义自定义注解 MyAspect
@Target(ElementType.METHOD) // 注解适用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可用
public @interface MyAspect {
// 注解属性可以根据需要进行定义
String value() default ""; // 默认属性
}
这段代码比较简单,我们这里简单讲解了解即可,@Target 标识了 Annotation 所修饰的对象范围,即该注解可以用在什么地方,@Retention 指 Annotation 被保留的时间长短,标明注解的生命周期。
ElementType | 描述 |
---|---|
ElementType.TYPE | 用于描述类、接口(包括注解类型)或 enum 声明 |
ElementType.METHOD` | 描述方法 |
ElementType.PARAMETER | 描述参数 |
ElementType.TYPE_USE | 可以标注任意类型 |
RetentionPolicy | 描述 |
---|---|
RetentionPolicy.SOURCE | 表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。 |
RetentionPolicy.CLASS | 编译时注解,表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取。 |
RetentionPolicy.RUNTIME | 运行时注解,表示注解存在于源代码、字节码和运行时中。这意味着在编译时、字节码中和实际运行时都可以通过反射获取到该注解的信息。 |
1.7.2.2 编写切面类并添加自定义注解
使用 @annotation 切点表达式定义切点,只对 @MyAspect 生效,切面类代码如下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@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 ...");
}
}
添加自定义注解 @MyAspect:
@MyAspect
@RequestMapping("/t1")
public String t1() {
return "t1";
}
访问 http://127.0.0.1:8080/test/t1:
1.8 Spring AOP 的实现方式
方法 | 描述 |
---|---|
基于注解 @Aspect | 利用 Spring AOP 提供的 @Aspect 注解功能来实现切面编程。 |
基于自定义注解 | 使用自定义注解结合 @annotation 来定义切点,灵活地控制切面的执行。 |
基于 Spring API | 通过 XML 配置的方式实现 AOP,这种方法在 Spring Boot 广泛使用之后几乎被淘汰,但可以作为补充了解。 |
基于代理实现 | 一种较早的实现方式,通过代理方式实现 AOP,写法相对繁琐,不推荐使用。 |
1.9 Spring AOP 原理
偷个懒,下次有空了再补。。。。。。