功能概述

  • 切面定义:

    • 使用 @Aspect 注解标记该类为一个切面。
    • 使用 @Component 注解将该类注册为一个 Spring Bean。
    • 使用 @Order(0) 注解指定该切面的执行顺序,值越小优先级越高。
  • 切点定义:

    • 使用 @Pointcut 注解定义一个切点 controllerLog(),匹配 com.szx.exam.controller 包及其子包下的所有控制器方法。
      前置日志记录:
    • logBefore 方法负责在控制器方法执行前记录请求的相关信息。
    • 获取当前请求的 HttpServletRequest 对象。
    • 记录请求的 URL、HTTP 方法、客户端 IP、客户端名称、控制器类名、方法名以及方法参数。
    • 使用 lombok 注解引入日志记录器 log,并在 finally 块中输出日志信息。
  • 环绕通知:

    • 使用 @Around 注解定义一个环绕通知 around,在控制器方法执行前后进行操作。
    • 记录方法开始执行的时间。
    • 调用 logBefore 方法记录前置日志。
    • 执行控制器方法,并记录返回结果。
    • 记录方法执行时间。
    • 清除 TraceId。

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.0</version>
</dependency>

因为接口中有json输出,所以需要额外引入一个fastjson

代码编写

新建 ControllerLogAspect 类 ,编写如下代码

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
import com.alibaba.fastjson.JSON;
import com.szx.exam.util.RequestKit;
import com.szx.exam.util.TraceIdKit;
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.springframework.core.annotation.Order;
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.util.LinkedHashMap;
import java.util.Map;

@Aspect
@Order(0)
@Component
@Slf4j
public class ControllerLogAspect {

// 拦截com.szx.exam下面的所有controller
@Pointcut("within(com.szx.exam.controller..*)")
public void controllerLog() {
}


private void logBefore(ProceedingJoinPoint pjd) {
Map<String, String> logMap = new LinkedHashMap<>();

try {
Object[] args = pjd.getArgs();

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
throw new Exception("requestAttributes is null");
}

HttpServletRequest httpServletRequest = requestAttributes.getRequest();
TraceIdKit.getTraceId(httpServletRequest);

logMap.put("URL", httpServletRequest.getRequestURI());
logMap.put("HTTP_METHOD", httpServletRequest.getMethod());
logMap.put("IP", RequestKit.getIp(httpServletRequest));
logMap.put("ClientName",RequestKit.getClientName(httpServletRequest));

String className = pjd.getTarget().getClass().getName();
logMap.put("CLASS", className);

String methodName = pjd.getSignature().getName();
logMap.put("METHOD", methodName);

StringBuilder sb = new StringBuilder();
for (Object object : args) {
try {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(JSON.toJSONString(object));
} catch (Exception ignored) {
sb.append("null, ");
}
}

logMap.put("ARGS", sb.toString());
} catch (Exception e) {
log.error("logBefore", e);
} finally {
for (Map.Entry<String, String> entry : logMap.entrySet()) {
log.info("{}: {}", entry.getKey(), entry.getValue());
}
}
}

@Around("controllerLog()")
public Object around(ProceedingJoinPoint pjd) throws Throwable {
long start = System.currentTimeMillis();

logBefore(pjd);

try {
Object result = pjd.proceed();
log.info("RESP: {}", JSON.toJSONString(result));

return result;
} finally {
log.info("COST: {} ms", System.currentTimeMillis() - start);
TraceIdKit.clearTraceId();
}
}

}

额外使用了两个工具类

RequestKit

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
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.net.URLEncoder;

/**
* 请求转化
*
*/
@SuppressWarnings("unused")
public class RequestKit {
/**
* 获取真实 IP 地址
*
* @param request 通用请求
* @return IP 地址
*/
public static String getIp(HttpServletRequest request) {
try {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
if (ipAddress != null && ipAddress.length() > 15) {
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
} catch (Exception e) {
throw new IllegalArgumentException("get ip exception: " + e.getMessage());
}
}

/**
* 获取客户端信息
*
* @param request 通用请求
* @return 客户端信息
*/
public static String getClientName(HttpServletRequest request) {
String clientName = "--";
String userAgent = request.getHeader("User-Agent");
if (StringUtils.isEmpty(userAgent)) {
return clientName;
}
if (userAgent.toLowerCase().contains("windows")) {
clientName = "windows";
}
if (userAgent.toLowerCase().contains("mac")) {
clientName = "mac";
}
if (userAgent.toLowerCase().contains("x11")) {
clientName = "unix";
}
if (userAgent.toLowerCase().contains("android")) {
clientName = "android";
}
if (userAgent.toLowerCase().contains("iphone")) {
clientName = "iphone";
}
return clientName;
}

public static String encode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (Exception e) {
return value;
}
}
}

TraceIdKit

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
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

@SuppressWarnings("unused")
public class TraceIdKit {

private static final String TRACE_ID = "traceId";


public static String getTraceId() {
String traceId = MDC.get(TRACE_ID);
if (StringUtils.isBlank(traceId)) {
traceId = generateTraceId();
MDC.put(TRACE_ID, traceId);
}

return traceId;
}

public static String getTraceIdHeader() {
return TRACE_ID;
}

public static void getTraceId(HttpServletRequest httpServletRequest) {
String traceId = httpServletRequest.getHeader(TRACE_ID);
if (StringUtils.isBlank(traceId)) {
traceId = getTraceId();
}

MDC.put(TRACE_ID, traceId);
}

public static void clearTraceId() {
MDC.clear();
}

private static String generateTraceId() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replace("-", "");
}

}

编写完成后,重启项目,当我们访问一个接口时,就会有对应的日志输出了

测试

编写一个测试接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/exam")
public class ExamController {

@GetMapping("getAllList")
public Result getAllList(String name) {
return Result.ok("success");
}


@PostMapping("add")
public Result add(@RequestBody HashMap<String,Object> map) {
return Result.ok(map);
}
}

测试访问,查看日志输出结果

image-20241202114705487

image-20241202114921442

结合 logback,查看记录的日志文件

image-20241202115034224

我们后面的开发中,就不用为每个接口都去处理日志信息,通过aop切片即可自动的实现日志记录功能。