Spring Boot如何利用AOP巧妙记录操作日志? 本篇

本篇要点

  • 简要回顾SpringAOP的相关知识点:关键术语,通知类型,切入点表达式等等。
  • 介绍SpringBoot快速启动测试AOP,巧妙打印日志信息。

简单回顾SpringAOP的相关知识点

SpringAOP的相关的知识点包括源码解析,为了加深印象,这边再做一个简短的回顾:

1、AOP关键术语

Spring Boot如何利用AOP巧妙记录操作日志?

  • 切面(Aspect):也就是我们定义的专注于提供辅助功能的模块,比如安全管理,日志信息等。
  • 连接点(JoinPoint):切面代码可以通过连接点切入到正常业务之中,图中每个方法的每个点都是连接点。
  • 切入点(PointCut):一个切面不需要通知所有的连接点,而在连接点的基础之上增加切入的规则,选择需要增强的点,最终真正通知的点就是切入点。
  • 通知方法(Advice):就是切面需要执行的工作,主要有五种通知:before,after,afterReturning,afterThrowing,around。
  • 织入(Weaving):将切面应用到目标对象并创建代理对象的过程,SpringAOP选择再目标对象的运行期动态创建代理对
  • 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加方法或字段。

2、通知的五种类型

  • 前置通知Before:目标方法调用之前执行的通知。
  • 后置通知After:目标方法完成之后,无论如何都会执行的通知。
  • 返回通知AfterReturning:目标方法成功之后调用的通知。
  • 异常通知AfterThrowing:目标方法抛出异常之后调用的通知。
  • 环绕通知Around:可以看作前面四种通知的综合。

3、切入点表达式

上面提到:连接点增加切入规则就相当于定义了切入点,当然切入点表达式分为很多种,这里主要学习execution和annotation表达式。

execution

  • 写法:execution(访问修饰符 返回值 包名.包名……类名.方法名(参数列表))
  • 例:execution(public void com.smday.service.impl.AccountServiceImpl.saveAccount())
  • 访问修饰符可以省略,返回值可以使用通配符*匹配。
  • 包名也可以使用*匹配,数量代表包的层级,当前包可以使用..标识,例如* *..AccountServiceImpl.saveAccount()
  • 类名和方法名也都可以使用*匹配:* *..*.*()
  • 参数列表使用..可以标识有无参数均可,且参数可为任意类型。

全通配写法:* *…*.*(…)

通常情况下,切入点应当设置在业务层实现类下的所有方法:* com.smday.service.impl.*.*(..)。

@annotation

匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。

@annotation(com.hyh.annotation.Log):指定Log注解方法的连接点。

4、AOP应用场景

  • 记录日志
  • 监控性能
  • 权限控制
  • 事务管理

快速开始

引入依赖

如果你使用的是SpringBoot,那么只需要引入:spring-boot-starter-aop,框架已经将spring-aop和aspectjweaver整合进去。

1
2
3
4
xml复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义日志信息封装

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
typescript复制代码/**
* Controller层的日志封装
* @author Summerday
*/
@Data
@ToString
public class WebLog implements Serializable {

private static final long serialVersionUID = 1L;

// 操作描述
private String description;

// 操作时间
private Long startTime;

// 消耗时间
private Integer timeCost;

// URL
private String url;

// URI
private String uri;

// 请求类型
private String httpMethod;

// IP地址
private String ipAddress;

// 请求参数
private Object params;

// 请求返回的结果
private Object result;

// 操作类型
private String methodType;
}

自定义注解@Log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

/**
* 描述
*/
String description() default "";

/**
* 方法类型 INSERT DELETE UPDATE OTHER
*/
MethodType methodType() default MethodType.OTHER;
}

定义测试接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@RestController
public class HelloController {

@PostMapping("/hello")
@Log(description = "hello post",methodType = MethodType.INSERT)
public String hello(@RequestBody User user) {
return "hello";
}

@GetMapping("/hello")
@Log(description = "hello get")
public String hello(@RequestParam("name") String username, String hobby) {
int a = 1 / 0;
return "hello";
}
}

定义切面Aspect与切点Pointcut

用@Aspect注解标注标识切面,用@PointCut定义切点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 定义切面
* @author Summerday
*/

@Aspect
@Component
public class LogAspect {

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

/**
* web层切点
* 1. @Pointcut("execution(public * com.hyh.web.*.*(..))") web层的所有方法
* 2. @Pointcut("@annotation(com.hyh.annotation.Log)") Log注解标注的方法
*/
@Pointcut("@annotation(com.hyh.annotation.Log)")
public void webLog() {
}
}

定义通知方法Advice

这里使用环绕通知,

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
scss复制代码/**
* 定义切面
* @author Summerday
*/

@Aspect
@Component
public class LogAspect {

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

/**
* web层切点
* 1. @Pointcut("execution(public * com.hyh.web.*.*(..))") web层的所有方法
* 2. @Pointcut("@annotation(com.hyh.annotation.Log)") Log注解标注的方法
*/

@Pointcut("@annotation(com.hyh.annotation.Log)")
public void webLog() {
}


/**
* 环绕通知
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//获取请求对象
HttpServletRequest request = getRequest();
WebLog webLog = new WebLog();
Object result = null;
try {
log.info("=================前置通知=====================");
long start = System.currentTimeMillis();
result = joinPoint.proceed();
log.info("=================返回通知=====================");
long timeCost = System.currentTimeMillis() - start;
// 获取Log注解
Log logAnnotation = getAnnotation(joinPoint);
// 封装webLog对象
webLog.setMethodType(logAnnotation.methodType().name());
webLog.setDescription(logAnnotation.description());
webLog.setTimeCost((int) timeCost);
webLog.setStartTime(start);
webLog.setIpAddress(request.getRemoteAddr());
webLog.setHttpMethod(request.getMethod());
webLog.setParams(getParams(joinPoint));
webLog.setResult(result);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
log.info("{}", JSONUtil.parse(webLog));
} catch (Throwable e) {
log.info("==================异常通知=====================");
log.error(e.getMessage());
throw new Throwable(e);
}finally {
log.info("=================后置通知=====================");
}
return result;
}

/**
* 获取方法上的注解
*/
private Log getAnnotation(ProceedingJoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
return method.getAnnotation(Log.class);
}


/**
* 获取参数 params:{"name":"天乔巴夏"}
*/
private Object getParams(ProceedingJoinPoint joinPoint) {
// 参数名
String[] paramNames = getMethodSignature(joinPoint).getParameterNames();
// 参数值
Object[] paramValues = joinPoint.getArgs();
// 存储参数
Map<String, Object> params = new LinkedHashMap<>();
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
// MultipartFile对象以文件名作为参数值
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
value = file.getOriginalFilename();
}
params.put(paramNames[i], value);
}
return params;
}

private MethodSignature getMethodSignature(ProceedingJoinPoint joinPoint) {
return (MethodSignature) joinPoint.getSignature();
}


private HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getRequest();
}

}

这里处理webLog的方式有很多种,考虑性能,可以采用异步方式存入数据库,相应代码已经上传至Gitee。

测试

1
2
3
4
bash复制代码POST http://localhost:8081/hello
Content-Type: application/json

{ "id" : 1, "username" : "天乔巴夏", "age": 18 }

结果如下:

1
2
3
4
json复制代码=================前置通知=====================
=================返回通知=====================
{"ipAddress":"127.0.0.1","description":"hello post","httpMethod":"POST","params":{"user":{"id":1,"age":18,"username":"天乔巴夏"}},"uri":"/hello","url":"http://localhost:8081/hello","result":"hello","methodType":"INSERT","startTime":1605596028383,"timeCost":28}
=================后置通知=====================

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%