🚩 需求描述

能够在每次访问接口时,自动记录入参、出参的全局统一日志。

📈 方案与分析

说明:日志作用的所有接口上,且需要统一形式,因此是不考虑在接口中手写的。

📋 方案一

理想的情况下是通过 spring aop 快速进入开发状态,然后在打包时通过 aspectj 处理器在编译期织入代码。因为 spring aop 采用 aspectj 语法,且提供了强大但简单的注解支持,因此可以很轻松的实现切面。但是 spring 的 aop 是运行时的(反射),相对于静态织入的 aspectj 来说,性能差距是极大的。而使用 aspectj 处理器每次需要手动清除之前编译的代码,否则新的代码无法实时编译的,比较麻烦,因此比较适合在打包时提供 aspectj 编译处理

📋  方案二

spring 其他内置增强接口, @RestControllerAdive,可以为注解类提升作用域,而  ResponseBodyAdiveRequestBodyAdive  接口,则可以为实现类提供预处理出参、入参的接口数据,两相结合,则可以实现类似的环绕式的出入参日志记录

📋  方案三

使用拦截器或过滤器,但其匹配规则是作用在 url 上,粒度太大不够细致。

📄 代码实现

此处通过 aop 实现,需要注意的是,若通过  ResponseBodyAdive  接口来处理统一返回值,则不能使用  @Around  的环绕切面,因为该切面要求方法的出参类型是不能修改的,而使用被统一返回类型包装的对象,是与切面方法的实际返回值是不一样的,故下面使用 @Before@AfterReturning 方法分别处理接口入参、和参数两个状态。

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

/**
* 记录日志信息
*/
@AllArgsConstructor
private static class LogInfo {

/**
* 请求 id
*/
String requestId;

/**
* 开始时间
*/
long startTime;

/**
* gson 工具
*/
Gson gson;
}

ThreadLocal<LogInfo> logInfo = new ThreadLocal<>();

@Pointcut(value = "@annotation(com.包名.annotation.Log)")
private void point() {
}

/**
* 方法执行前
*
* @param joinPoint 切点
*/
@Before(value = "point()")
public void LogRequestInfo(JoinPoint joinPoint) {
String uuid = UUID.randomUUID().toString();
Gson gson = new Gson();
// 缓存线程变量信息
logInfo.set(new LogInfo(uuid, System.currentTimeMillis(), gson));
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
StringBuilder requestLog = new StringBuilder();
Signature signature = joinPoint.getSignature();
requestLog.append("[").append(uuid).append("] -> ")
.append("请求信息:").append("URL = {").append(request.getRequestURI()).append("}, ")
.append("请求方式 = {").append(request.getMethod()).append("}, ")
.append("请求IP = {").append(request.getRemoteAddr()).append("}, ")
.append("类方法 = {").append(signature.getDeclaringTypeName()).append(".")
.append(signature.getName()).append("}, ");
// 处理请求参数
String[] paramNames = ((MethodSignature) signature).getParameterNames();
Object[] paramValues = joinPoint.getArgs();
int paramLength = null == paramNames ? 0 : paramNames.length;
if (paramLength == 0) {
requestLog.append("请求参数 = {} ");
} else {
requestLog.append("请求参数 = [");
for (int i = 0; i < paramLength - 1; i++) {
requestLog.append(paramNames[i]).append("=").append(gson.toJson(paramValues[i])).append(",");
}
requestLog.append(paramNames[paramLength - 1]).append("=").append(gson.toJson(paramValues[paramLength - 1])).append("]");
}
log.info(requestLog.toString());
}

/**
* 方法执行后
*
* @param target 结果
*/
@AfterReturning(returning = "target", pointcut = "point()")
public void logResultVOInfo(Object target) {
LogInfo logInfo = this.logInfo.get();
// 删除线程变量
this.logInfo.remove();
long offTime = System.currentTimeMillis() - logInfo.startTime;
log.info(String.format("[%s] -> 请求结果:%s [%dms]", logInfo.requestId, logInfo.gson.toJson(target), offTime));
}
}

因为实际上,两个切面方法是独立的作用域,为了线程安全,故使用线程变量来存储请求的日志信息,主要记录该请求的唯一编号(此处使用 UUID),开始时间,同时又都需要将对象 json 化,所以也缓存了 gson 对象。
同样的方法,也可以通过实现增强接口,重写其预处理入参、出参的方法来提供日志支持。


java spring log web aop

评论