Spring Boot 2系列(四十九):Spring AOP 实现统一记录请求和响应日志,解密并重设置请求入参值

在实际开发中,可能需要打印方法的入参和返回的数据以帮助出现问题时可快递定位.

常规的做法在方法中的业务处理之前使用 Logger 打印方法入参,在业务处理之后打印结果数据,这样就会在很多方法中存在重复代码。

像打印日志这类跨多个业务和模块的需求,可以通过 Spring AOP 来统一实现,完全省略了方法中手动添加 Logger 的操作。

统一打印日志

统一打印日志可以集成 ELK,归集这些日志数据,可以做一些统计分析工作,用来对功能和性能进行优化调整。

下面示例,使用前置和后置通知实现将 Controller 方法的入参和响应记录到日志,以方便快速定位问题。

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
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Aspect
@Component
public class ReqRespLogAop {

private static final Logger logger = LogManager.getLogger(ReqRespLogAop.class);

private NamedThreadLocal<Long> threadLocal = new NamedThreadLocal("StopWatch");

@Pointcut("execution(* com.xxxxxx.controller.*.*(..))")
public void pointcut() {
}

@Before("pointcut()")
public void logRequestParams(JoinPoint joinPoint) {
// 开始时间
threadLocal.set(System.currentTimeMillis());

Object[] args = joinPoint.getArgs();
//过滤序列化异常
Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
List<Object> logArgs = stream.filter(arg -> (!(arg instanceof HttpServletRequest) &&
!(arg instanceof HttpServletResponse) && !(arg instanceof MultipartFile))).collect(Collectors.toList());

String fullClassName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String remoteHost = request.getRemoteHost();
logger.info("Request Client Host:{}", remoteHost);
logger.info("Request URI:{}, API:{}, Request Body:{}", request.getRequestURI(), fullClassName + "." + methodName, JSON.toJSONString(logArgs));
}

@AfterReturning(returning = "response", pointcut = "pointcut()")
public void logResponseBody(Object response) {
if (response != null) {
//响应信息
logger.info("Response Body:{}", JSON.toJSONString(response));
}
//计算请求处理耗时,单位:秒
Long timeConsume = (System.currentTimeMillis() - threadLocal.get());
if (timeConsume > (2 * 1000)) {
logger.warn("请求处理耗时:{}", timeConsume + " ms");
} else {
logger.info("请求处理耗时:{}", timeConsume + " ms");
}
threadLocal.remove();
}
}

如果还需要记录其它方法的入参和返回数据,例如记录业务层的方法处理

在切面类里可以定义多个切点,使用切入点表达式组合的方式来实现,参考 Spring Boot 2实践系列(四十八):Spring AOP详解与应用:切入点表达式组合

解密及修改入参值

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.clearofchina.core.exception.BusinessException;
import com.clearofchina.prediagnose.annotate.UuidValid;
import com.clearofchina.prediagnose.constants.SysConstants;
import com.clearofchina.prediagnose.utils.CryptoUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.NamedThreadLocal;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Aspect
@Component
public class RequestResponseLogAop {

private static final Logger logger = LogManager.getLogger(RequestResponseLogAop.class);

private NamedThreadLocal<Long> threadLocal = new NamedThreadLocal("StopWatch");

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

/**
* @desc: 记录请求到日志
* @param: [joinPoint]
*/
@Before("pointcut()")
public void logRequestInfo(JoinPoint joinPoint) {
// 开始时间
threadLocal.set(System.currentTimeMillis());
Object[] args = joinPoint.getArgs();

//过滤序列化异常
Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
List<Object> logArgs = stream.filter(arg -> (!(arg instanceof HttpServletRequest) &&
!(arg instanceof HttpServletResponse) && !(arg instanceof MultipartFile))).collect(Collectors.toList());

String fullClassName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String remoteHost = request.getRemoteHost();
logger.info("Request Client Host:{}", remoteHost);
logger.info("Request URI:{}, API:{}", request.getRequestURI(), fullClassName + "." + methodName);
logger.info("Request Body:{}", JSON.toJSONString(logArgs));
// uuid参数做了对称加密,对其进行解密校验
this.validUuid(joinPoint, args);
}

/**
* @desc: uuid解密校验
* @param: [joinPoint, args]
*/
private void validUuid(JoinPoint joinPoint, Object[] args) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
// UuidValid 是一个自定义注解,作用在 Controller 层的方法上
UuidValid annotation = targetMethod.getAnnotation(UuidValid.class);
if (ObjectUtils.isNotEmpty(annotation)) {
Object obj = args[0];
JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(obj));
String uuid = (String) jsonObject.get("uuid");
String registerNo = null;
try {
// 入参 uuid 解密出来为 registerNo
registerNo = CryptoUtil.decryptDES(SysConstants.UUID_SECRET, uuid);
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException("UUID错误");
}

try {
// 反射拿到对象属性
Field field = obj.getClass().getDeclaredField("registerNo");
if (ObjectUtils.isNotEmpty(field)) {
field.setAccessible(true);
// 设置值
field.set(obj, registerNo);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

/**
* @desc: 记录响应到日志
* @param: [response]
*/
@AfterReturning(returning = "response", pointcut = "pointcut()")
public void logResponseInfo(Object response) {

if (response instanceof ResponseEntity) {
logger.info("Response Body:{}", "输出文件");
} else {
//响应信息
logger.info("Response Body:{}", JSON.toJSONString(response));
}

//计算请求处理耗时,单位:秒
Long timeConsume = (System.currentTimeMillis() - threadLocal.get());
if (timeConsume > (2 * 1000)) {
logger.warn("请求处理耗时:{}", timeConsume + " ms");
} else {
logger.info("请求处理耗时:{}", timeConsume + " ms");
}
threadLocal.remove();
}
}

Spring Boot 2系列(四十九):Spring AOP 实现统一记录请求和响应日志,解密并重设置请求入参值

http://blog.gxitsky.com/2019/12/08/SpringBoot-49-aop-api-log/

作者

光星

发布于

2019-12-08

更新于

2022-06-17

许可协议

评论