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
| @Pointcut("execution(public * com.example..*(..))") public void anyPublicMethodInComExample() {}
@Pointcut("execution(* com.example.service.UserService.*(..))") public void userServiceMethods() {}
@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 {
@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); } }
|