AOP:Aspect Oriented Programming
(面向切面编程):核心思想是将重复的逻辑剥离出来,在不修改原始逻辑的基础上对原始功能进行增强。
优点:无侵入、减少重复代码、提高开发效率、维护方便。
一、AOP
概念
连接点(JoinPoint)
:可以被 AOP
控制的方法执行。
通知(Advice)
:重复逻辑代码。
切入点(PointCut)
:匹配连接点的条件。
切面(Aspect)
:通知+切点。
通知类型
@Around
:此注解标注的通知方法在目标方法前、后都执行。@Before
:此注解标注的通知方法在目标方法前执行。@After
:此注解标注的通知方法在目标方法后执行,无论是否有异常。@AfterReturning
:此注解标注的通知方法在目标方法后被执行,有异常不会执行。@AfterThrowing
:此注解标注的通知方法发生异常后执行。
注意:@Around
需要自己调用 ProceedingJoinPoint.proceed()
来让目标方法执行,其他通知不需要考虑目标方法执行。
1.1、通知顺序
当有多个切面的切点都匹配目标时,多个通知方法都会被执行。
- 默认按照切面类的名称字母排序:
- 目标前的通知方法,字母排名靠前的先执行。
- 目标后的通知方法,字母排名靠前的后执行。
- 用
@Order
(数字)加在切面类上来控制顺序- 目标前的通知方法:数字小的先执行。
- 目标后的通知方法,数字小的后执行。
1.2、切点表达式
切点表达式用来匹配哪些目标方法需要应用通知
-
execution
(返回值类型 包名.类名.方法名(参数类型))- *:可以通配任意返回值类型、包名、类名、方法名、或者任意类型的一个参数
- ..:可以通配任意 层级的包、或者任意类型、任意个数的参数
-
@annotation()
根据注解匹配 -
切点表达式--
execution
主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为
execution
(访问修饰符? 返回值 包名.类名?.方法名(方法参数)throws
异常?) 其中带?的表示可以省略的部分- 访问修饰符:可省略(仅能匹配
public、protected,private
不能增强) - 包名.类名:可省略
throws
异常:可省略
- 访问修饰符:可省略(仅能匹配
java
@Pointcut("execution(public * com.duan.controller.*.*(..))")
public void loggingPointcut(){
// 暂不用处理
}
- 切点表达式--
@annotation
切点表达式也支持匹配目标方法是否有注解。
java
/**
* 定义切点
*/
@Pointcut("@annotation(com.duan.anno.Log)")
public void loggingPointcut() {
}
1.3、@PoinCut
该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。
java
/**
* com.duan.controller 包中公共方法的切入点
*/
@Pointcut("execution(public * com.duan.controller.*.*(..))")
public void loggingPointcut(){
// 暂不用处理
}
@Around("loggingPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
}
1.4、连接点
连接点简单理解就是目标方法,在Spring
中用JoinPoint
抽象了连接点,用它可以获得方法执行时的相关信息,如方法名、方法参数类型等等。
- 对于
@Around
通知,获取连接点信息只能使用ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用
JoinPoint
,它是ProceedingJoinPoint
的父类
二、案例
获得业务中的增删改方法的操作日志,一般都保存到数据库中,现在我们是测试,就先不保存到数据库,直接打印出来,包含请求的接口地址、操作时间、执行方法全类名、执行方法名、方法参数、返回值、方法执行时长。
1、新建一个SpringBoot
项目aop
,所使用的依赖如下:
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.duan</groupId>
<artifactId>aop</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
</dependencies>
</project>
2、自定义一个注解 @Log
java
package com.duan.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author db
* @version 1.0
* @description Log
* @since 2024/4/11
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
3、新建一个切面类LoggingAspect
,这里经常使用的是环绕通知@Around
,代码如下:
java
package com.duan.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;
/**
* @author db
* @version 1.0
* @description LoggingAspect
* @since 2024/1/3
*/
@Aspect
@Component
@Slf4j
public class LoggingAspect {
/**
* com.duan.controller 包中公共方法的切入点
*/
@Pointcut("execution(public * com.duan.controller.*.*(..))")
public void loggingPointcut(){
// 暂不用处理
}
@Around("loggingPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
// 获取类名
String className = pjp.getTarget().getClass().getTypeName();
// 获取方法名
String methodName = pjp.getSignature().getName();
// 获取参数名
String[] parameterNames = ((MethodSignature) pjp.getSignature()).getParameterNames();
Object result = null;
// 获取参数值
Object[] args = pjp.getArgs();
// 获取请求
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 获取请求的url
String url = request.getRequestURL().toString();
// 请求参数,以参数名和值为键值对
Map<String, Object> paramMap = new HashMap<>();
IntStream.range(0, parameterNames.length).forEach(i->paramMap.put(parameterNames[i], args[i]));
// header参数
Enumeration<String> headerNames = request.getHeaderNames();
Map<String, Object> headerMap = new HashMap<>();
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headerMap.put(headerName, headerValue);
}
// 打印请求参数,记录起始时间
long start = System.currentTimeMillis();
log.info("请求| 请求接口:{} | 类名:{} | 方法:{} | header参数:{} | 参数:{} | 请求时间:{}", url, className, methodName, headerMap, paramMap, LocalDateTime.now());
try {
result = pjp.proceed();
System.out.println(result.toString());
} catch (Exception e) {
log.error("返回| 处理时间:{} 毫秒 | 返回结果 :{}", (System.currentTimeMillis() - start), "failed");
throw e;
}
// 获取执行完的时间 打印返回报文
log.info("返回| 处理时间:{} 毫秒 | 返回结果 :{}", (System.currentTimeMillis() - start), "success");
return result;
}
}
4、在用户管理中的增加用户方法上使用@Log
java
package com.duan.controller;
import com.duan.anno.Log;
import com.duan.pojo.Result;
import com.duan.pojo.User;
import com.duan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/4/15
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
@Log
public Result addUser(@RequestBody User user){
userService.AddUser(user);
return Result.success();
}
}
5、使用postman
进行测试
代码地址:https://gitee.com/duan138/practice-code/tree/dev/aop
三、总结
在日常开发中AOP
是我们经常用到的知识点,它不光可以记录日志,在不改变源码的前提下,动态的给它增加功能。或者大量的方法里面都有相同的方法,就可以用AOP进行代码提取,简化代码冗余。所以这部分内容还是要熟练掌握。
下篇文章来学习项目中常用的注解。
改变你能改变的,接受你不能改变的,关注公众号:程序员康康,一起成长,共同进步。