SpringBoot 整合 Aop

1 前言

1)环境

  • gradle-8.5
  • Amazon Corretto 17.0.4_9
  • id 'org.springframework.boot' version '3.2.3'
  • id 'io.spring.dependency-management' version '1.1.4'

2)添加依赖

1
implementation 'org.springframework.boot:spring-boot-starter-aop'

2 理论概念

  • 切面(Aspect):切入点和通知的集合
  • 连接点(Joinpoint):目标对象中可以被增强的所有方法
  • 通知(Advice):增强的代码(逻辑),分为前置,后置,最终,异常,环绕
  • 切入点(Pointcut):目标对象中经过匹配最终增强的方法
  • 引入(Introduction):动态的为某个类增加和减少方法
  • 目标对象(Target Object):被代理的对象
  • AOP 代理对象(AOP Proxy):AOP 框架创建的代理对象,用于实现切面,调用方法
  • 织入(Weaving):将通知应用到切入点的过程

@Pointcut 注解是 Spring AOP(面向切面编程)中的一个关键组成部分,它允许开发人员定义可重用的切入点表达式,这些表达式可以被多个通知(Advice)引用。

1)定义切入点表达式: 在包含 @Aspect 注解的类内部,使用 @Pointcut 注解定义一个方法,并在注解值中提供一个切入点表达式。这个表达式用于匹配满足特定条件的方法执行点。

2)定义通知(Advice): 在包含 @Aspect 注解的类内部,使用 @Before、@After、@AfterReturning、@AfterThrowing、@Around 注解定义一个通知(Advice),并在注解值中提供一个切入点表达式。通知(Advice)用于在方法执行前、后、返回值、异常抛出时执行特定的代码。

3)通知类型:

  • @Before(前置通知): 在目标方法执行前执行的通知。
  • @After(最终通知): 不管目标方法是否正常执行完成(即无论是否有异常抛出),都会在目标方法执行后执行的通知。
  • @AfterReturning (后置通知/正常返回通知): 在目标方法正常执行完毕并返回后执行的通知。可以访问到方法的返回值。
  • @AfterThrowing(抛出异常通知): 当目标方法抛出异常后执行的通知,可以访问到抛出的异常对象。
  • @Around (环绕通知): 最强大的通知类型,它可以完全控制目标方法的执行流程。可以在方法调用前后插入自定义行为,并决定何时以及是否执行目标方法。

4)切入点表达式语法:

1
2
@Pointcut("execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)")
public void myPointcutDefinition() {}

execution 切点表达式使用较多,支持的通配符:

  • *:匹配所有
  • ..:匹配多级包或者多个参数
  • +表示类以及子类

其他部分:

  • execution: 关键字,表示这是一个执行切点表达式。
  • modifiers-pattern: 可选,方法的修饰符,比如 public、protected、*(任意修饰符)等。
  • return-type-pattern: 必须,方法的返回类型,可以是具体的类型,也可以是通配符 *(表示任意类型)或 ..(表示任意数量的任意类型参数)。
  • declaring-type-pattern: 可选,方法所在的类或接口的全限定名,可以使用 * 或 .. 通配符。
  • method-name-pattern: 必须,方法名称,可以是具体的名称,也可以使用通配符 * 匹配任意名称。
  • param-pattern: 必须,方法的参数列表,格式为 (paramType1, paramType2, …),每个参数类型可以是具体类型、* 或 ..。
  • throws-pattern: 可选,方法声明抛出的异常类型列表,格式为 throws ExceptionType1, ExceptionType2, …。

例子:

1
2
3
4
5
6
7
8
9
10
11
// 匹配 com.example包及其子包下所有类的公共方法
@Pointcut("execution(public * com.example..*(..))")
public void anyPublicMethodInComExample() {}

// 匹配 com.example.service包下的 UserService 类的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}

// 匹配返回类型为void且方法名为process的任意方法
@Pointcut("execution(void process(..))")
public void voidProcessMethods() {}

其他切点表达式:

  • within - 匹配方法所在的包或者类

  • this - 用于向通知方法中传入代理对象的引用

  • target - 用于向通知方法中传入目标对象的引用

  • args - 用于向通知方法中传入参数,并且匹配参数个数

  • @args - 和 args 都是匹配参数,但是@args 要求传入切入点的参数必须标注指定注解,且不能是 SOURCE 源码注解,比如 Lombok 的

  • @within - 匹配加了某个注解的类中的所有方法

  • @target - 与@within 类似,但是要求标注到类上的注解,必须为 RUNTIME 的

  • @annotation - 匹配加了某个注解的方法

  • bean 通过 spring 容器中的 beName 匹配,可以使用通配符*来标识以什么开头,以什么结尾

5)通知(Advice)执行顺序:

  • @Before:先执行 @Before 通知,再执行切入点表达式匹配的方法。
  • @AfterReturning:先执行切入点表达式匹配的方法,再执行 @AfterReturning 通知。
  • @AfterThrowing:先执行切入点表达式匹配的方法,再执行 @AfterThrowing 通知。
  • @After:先执行切入点表达式匹配的方法,再执行 @After 通知。
  • @Around:先执行 @Around 通知,再执行切入点表达式匹配的方法,最后执行 @After 通知。

3 案例

3.1 日志记录功能

实现一个日志记录功能,要求记录用户的所有请求信息,包括请求方法、请求路径、请求参数、请求体、请求头、响应状态码、响应体等。

1)创建日志记录切面类

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
@Aspect
@Component
public class LogAspect {

private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

@Pointcut("execution(public * com.example.controller.*.*(..))")
public void logPointcut() {
}

@Before("logPointcut()")
public void doBefore(JoinPoint joinPoint) {
LOGGER.info("Request: {} {}", joinPoint.getSignature().getName(), joinPoint.getArgs());
}

@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void doAfterReturning(Object result) {
LOGGER.info("Response: {}", result);
}

@AfterThrowing(pointcut = "logPointcut()", throwing = "e")
public void doAfterThrowing(Throwable e) {
LOGGER.error("Exception: {}", e);
}
}

2)配置日志记录切面类

1
2
3
4
@Configuration
@EnableAspectJAutoProxy
public class LogConfig {
}

3)测试

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
public class TestController {

@GetMapping("/test")
public String test() {
return "test";
}
}

3.2 运行时间统计

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
39
40
41
42
43
44
45
46
47
48
49
50
51
@Aspect
@Component
public class RunTimeAop {

// 定义切入点,匹配所有Service层的方法
@Pointcut("execution(* server.services..*(..))")
public void businessServiceMethods() {
}

// 在方法执行前记录开始计时
@Around("businessServiceMethods()")
public Object logTime(ProceedingJoinPoint pjp) throws Throwable {

// 获取方法签名和参数
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Object[] args = pjp.getArgs();

StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 执行目标方法
Object result = pjp.proceed();

stopWatch.stop();

Spliterator<Object> spliterator = Arrays.spliterator(args);
Stream<Object> stream = StreamSupport.stream(spliterator, false);

if (args.length > 0 && !Objects.isNull(result)) {
// 合并参数
String combinedJsonString = stream.flatMap(element -> {
if (element instanceof Map) {
Map<String, String> map = (Map<String, String>) element;
return map.entrySet().stream()
.map(entry -> "\"" + entry.getKey() + "\": \"" + entry.getValue() + "\"");
} else if (element instanceof String) {
return Stream.of("\"" + element + "\"");
} else {
return Stream.empty();
}
})
.collect(Collectors.joining(",", "[", "]"));

System.out.printf("%s实际执行时间= %sms 随机时间= %sms 参数列表:%s%n", method.getName(), stopWatch.getTotalTimeMillis(), result, combinedJsonString);
} else {
System.out.printf("%s实际执行时间= %sms%n", method.getName(), stopWatch.getTotalTimeMillis());
}
return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class AopServiceIntMapParams {

private static final ObjectMapper objectMapper = new ObjectMapper();

public int readFakeData(Map<String, String> params,String type) throws InterruptedException, JsonProcessingException {
Random random = new Random();
int timeout = random.nextInt(701) + 50;
TimeUnit.MILLISECONDS.sleep(timeout);

String jsonString = objectMapper.writeValueAsString(params);
System.out.println(jsonString);

return timeout;
}
}
1
2
3
4
5
6
7
8
9
10
11
@Service
public class AopServiceVoidNoParams {

private static final ObjectMapper objectMapper = new ObjectMapper();

public void readFakeData2() throws InterruptedException, JsonProcessingException {
Random random = new Random();
int timeout = random.nextInt(701) + 50;
TimeUnit.MILLISECONDS.sleep(timeout);
}
}