SpringBoot 实现全局统一请求日志处理


相关工具类

  1. 内容类型判断工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 内容类型工具类 * @author TheEnd */ public class ContentTypeUtil { public static boolean isBinaryContent(HttpServletRequest request) { String contentType = request.getContentType(); return isBinaryContent(contentType); } public static boolean isBinaryContent(HttpServletResponse response) { String contentType = response.getContentType(); return isBinaryContent(contentType); } public static boolean isBinaryContent(String contentType) { if (contentType == null || contentType.isEmpty()) { return false; } return contentType.startsWith("image/") || contentType.startsWith("multipart/form-data") || contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.startsWith("application/octet-stream") || contentType.startsWith("application/vnd.ms-excel") || contentType.startsWith("application/vnd.ms-powerpoint") || contentType.startsWith("application/msword") || contentType.startsWith("application/vnd.openxmlformats-officedocument") || contentType.startsWith("application/zip") || contentType.startsWith("application/x-"); } }
  2. 客户端IP获取工具类

    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
    import javax.servlet.http.HttpServletRequest; import java.util.*; /** * 用户获取客户端IP的工具类 * @author TheEnd */ public class IpUtil { /** * 获取客户端IP * @param request 请求对象 * @return 客户端IP */ public static String getIp(HttpServletRequest request) { if (request == null) { return ""; } ArrayList<String> headerNames = Collections.list(request.getHeaderNames()); List<String> ipHeaderNames = Arrays.asList("X-Forwarded-For", "Proxy-Client-IP", "X-Real-IP"); for (String ipHeaderName : ipHeaderNames) { String ip = getHeaderValueIgnoreCase(ipHeaderName, headerNames, request); if (ip != null && !ip.isEmpty()) { return ip; } } return request.getRemoteAddr(); } private static String getHeaderValueIgnoreCase(String headerName, List<String> requestHeaderNames, HttpServletRequest request) { for (String requestHeaderName : requestHeaderNames) { if (requestHeaderName.equalsIgnoreCase(headerName)) { return request.getHeader(requestHeaderName); } } return ""; } }

获取已登录用户信息封装

此信息属于业务信息, 需要按照业务实际情况进行编写, 所以此处只提供接口和一个空实现

获取已登录用户信息接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.lang.NonNull; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 已登录用户信息获取工具 * @author TheEnd */ public interface LoginUserInfoHelper { /** * 用户ID * @param request 请求对象 * @param response 响应对象 * @return 用户ID */ String userId(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response); /** * 用户名 * @param request 请求对象 * @param response 响应对象 * @return 用户名 */ String username(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response); }
默认空实现, 如不需要记录登录人信息, 使用此类即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.lang.NonNull; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 返回空字符串的用户信息获取器 * @author The */ public class EmptyLoginUserInfoHelper implements LoginUserInfoHelper { @Override public String userId(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) { return ""; } @Override public String username(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) { return ""; } }
可以根据实际业务需求自定义实现类.

编写日志类

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
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.Date; /** * 请求日志 * @author TheEnd */ @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class RequestLog implements Serializable { /** * 请求方法 */ private String method; /** * 请求路径 */ private String url; /** * 客户端Ip */ private String ip; /** * 携带Cookie */ private String cookie; /** * 请求参数 */ private String requestParams; /** * 请求体 */ private String requestBody; /** * 响应HTTP状态码 */ private Integer responseCode; /** * 响应内容 */ private String responseBody; /** * 用户ID */ private String userId; /** * 用户名 */ private String username; /** * 模块名称 */ private String modelName; /** * 接口名称 */ private String interfaceName; /** * 备注 */ private String remarks; /** * 请求时间 */ private Date requestTime; /** * 响应时间 */ private Date responseTime; /** * 使用毫秒 */ private Integer useTime; }

编写相关注解

  1. 日志额外信息注解

    用于通过注解给日志记录增加额外信息

    可以标注在类上和方法上, 标注在类上时类中所有的方法的日志都有此信息. 类和方法上都有标注时,优先生效方法上的.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 接口额外信息 * @author TheEnd */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface LogExtra { /** * 模块名 * @return 模块名 */ String modelName() default ""; /** * 接口名 * @return 接口名 */ String interfaceName() default ""; /** * 备注 * @return 备注 */ String remarks() default ""; }
  2. 不记录日志注解

    标注了此注解的类和方法不会进行日志记录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 表名此接口不需要记录日志 * @author TheEnd */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface NotLog { }

编写序列化器

提供了序列化器接口LogSerializer和默认实现, 可以根据业务需求自定义序列化器, 创建一个类并实现LogSerializer接口即可.
  1. 序列化器接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import org.springframework.lang.NonNull; /** * 日志序列化器 * @author TheEnd */ public interface LogSerializer { /** * 序列化 * @param log 日志对象 * @return 序列化数据 */ byte[] serializer(@NonNull RequestLog log); }
  2. JDK ToString 序列化

    1
    2
    3
    4
    5
    6
    7
    8
    import org.springframework.lang.NonNull; /** * 将日志对象通过ToString进行序列化的序列化器 * @author TheEnd */ public class ToStringLogSerializer implements LogSerializer { @Override public byte[] serializer(@NonNull RequestLog log) { return log.toString().getBytes(); } }
  3. Jackson ToJson 序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.lang.NonNull; /** * 将日志对象转为JSON的序列化器 * @author TheEnd */ public class JsonLogSerializer implements LogSerializer { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public byte[] serializer(@NonNull RequestLog log) { try { return MAPPER.writeValueAsBytes(log); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } }

编写日志打印器

日志打印器用于将日志输出至指定位置, 提供打印器接口和默认实现, 可以根据业务需求, 自定义打印器.

可以将日志输出至文件,控制台,消息中间件,数据库,网络流等

  1. 打印器接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.lang.NonNull; /** * 日志打印器 * @author TheEnd */ public interface LogPrinter { /** * 打印日志 * @param log 日志对象 */ void print(@NonNull RequestLog log); }
  2. Logback 打印器

    通过Logback打印
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import lombok.Data; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.lang.NonNull; /** * 将日志写到文件的持久化器 * @author TheEnd */ @Data @RequiredArgsConstructor public class LogbackLogPrinter implements LogPrinter { private final Logger log = LoggerFactory.getLogger(LogbackLogPrinter.class); /** * 序列化器 */ private final LogSerializer serializer; @Override public void print(@NonNull RequestLog requestLog) { byte[] bytes = serializer.serializer(requestLog); log.info(new String(bytes)); } }

编写日志拦截器类

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import java.lang.annotation.Annotation; import java.util.Date; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.lang.NonNull; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 日志序列化拦截器 * @author TheEnd */ @Slf4j @ControllerAdvice @RequiredArgsConstructor public class LogPersistenceInterceptor implements HandlerInterceptor, ResponseBodyAdvice<Object> { /** * 请示时间Key */ private static final String API_START_TIME = "ApiLogSerializableInterceptor:preHandle:start:millis"; /** * 请求体Key */ private static final String API_REQUEST_BODY = "ApiLogSerializableInterceptor:preHandle:request:body"; /** * 处理器key */ private static final String API_HANDLER = "ApiLogSerializableInterceptor:preHandle:handler"; /** * JSON 工具对象 */ private final ObjectMapper logMapper = new ObjectMapper(); /** * 日志打印器 */ private final List<LogPrinter> logPrinters; /** * 当前登录用户信息获取器 */ private final LoginUserInfoHelper loginUserInfoHelper; @Override public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { request.setAttribute(API_HANDLER, handler); if (!isRecord(request, response, handler)) { return true; } Date startTime = new Date(); request.setAttribute(API_START_TIME, startTime); String body = readBody(request); request.setAttribute(API_REQUEST_BODY, body); return true; } @Override public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) { HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest(); HttpServletResponse res = ((ServletServerHttpResponse) response).getServletResponse(); Object handler = req.getAttribute(API_HANDLER); if (isRecord(req, res, handler)) { return body; } String result = ""; if (!isBinaryContent(res) && body instanceof Serializable) { try { result = logMapper.writeValueAsString(body); } catch (JsonProcessingException e) { log.error("HTTP响应体JSON转换失败", e); } } RequestLog requestLog = buildLog(req, res, handler, result); for (LogPrinter printer : logPrinters) { printer.print(requestLog); } return body; } private String readBody(HttpServletRequest request) { if (request.getContentLength() <= 0) { return ""; } BufferedReader reader = null; try { reader = request.getReader(); } catch (IOException e) { throw new RuntimeException(e); } Stream<String> lines = reader.lines(); return lines.collect(Collectors.joining(System.lineSeparator())); } private boolean isRecord(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { if (!(handler instanceof HandlerMethod)) { return false; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); if (method.isAnnotationPresent(NotLog.class)) { return false; } Class<?> declaringClass = method.getDeclaringClass(); if (declaringClass.isAnnotationPresent(NotLog.class)) { return false; } if (isBinaryContent(request)) { return false; } return true; } private RequestLog buildLog(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, @NonNull String result) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); LogExtra methodExtra = method.getAnnotation(LogExtra.class); Class<?> declaringClass = method.getDeclaringClass(); LogExtra classExtra = declaringClass.getAnnotation(LogExtra.class); LogExtra merge = mergeLogExtra(methodExtra, classExtra); RequestLog requestLog = new RequestLog(); requestLog.setMethod(request.getMethod()); requestLog.setUrl(request.getRequestURI()); requestLog.setIp(IpUtil.getIp(request)); requestLog.setCookie(cookieSerializable(request)); requestLog.setRequestParams(parameterSerializable(request)); requestLog.setRequestBody((String) request.getAttribute(API_REQUEST_BODY)); requestLog.setResponseCode(response.getStatus()); requestLog.setResponseBody(result); requestLog.setUserId(loginUserInfoHelper.userId(request, response)); requestLog.setUsername(loginUserInfoHelper.username(request, response)); requestLog.setModelName(merge.modelName()); requestLog.setInterfaceName(merge.interfaceName()); requestLog.setRemarks(merge.remarks()); Date startTime = (Date) request.getAttribute(API_START_TIME); requestLog.setRequestTime(startTime); Date responseTime = new Date(); requestLog.setResponseTime(responseTime); requestLog.setUseTime(Math.toIntExact(responseTime.getTime() - startTime.getTime())); return requestLog; } private String cookieSerializable(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); try { return logMapper.writeValueAsString(cookies); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private String parameterSerializable(HttpServletRequest request) { Map<String, String[]> parameterMap = request.getParameterMap(); try { return logMapper.writeValueAsString(parameterMap); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private LogExtra mergeLogExtra(LogExtra methodExtra, LogExtra classExtra) { return new LogExtra(){ @Override public String modelName() { if (methodExtra != null && methodExtra.modelName() != null && !methodExtra.modelName().isEmpty()) { return methodExtra.modelName(); } if (classExtra != null && classExtra.modelName() != null && !classExtra.modelName().isEmpty()) { return classExtra.modelName(); } return ""; } @Override public String interfaceName() { if (methodExtra != null && methodExtra.interfaceName() != null && !methodExtra.interfaceName().isEmpty()) { return methodExtra.interfaceName(); } if (classExtra != null && classExtra.interfaceName() != null && !classExtra.interfaceName().isEmpty()) { return classExtra.interfaceName(); } return ""; } @Override public String remarks() { if (methodExtra != null && methodExtra.remarks() != null && !methodExtra.remarks().isEmpty()) { return methodExtra.remarks(); } if (classExtra != null && classExtra.remarks() != null && !classExtra.remarks().isEmpty()) { return classExtra.remarks(); } return ""; } @Override public Class<? extends Annotation> annotationType() { return LogExtra.class; } }; } }

配置拦截器

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
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Collections; import java.util.List; /** * @author TheEnd */ @Configuration public class MvcConfigurer implements WebMvcConfigurer { /** * 日志持久化拦截器 */ private LogPersistenceInterceptor logPersistenceInterceptor; @Autowired public MvcConfigurer setLogPersistenceInterceptor(LogPersistenceInterceptor logPersistenceInterceptor) { this.logPersistenceInterceptor = logPersistenceInterceptor; return this; } /** * 日志打印器 * @return 日志打印器集合 */ @Bean public List<LogPrinter> logPrinters() { // 根据业务实际情况创建并实例化日志打印器, 并注册至IOC容器. LogbackLogPrinter printer = new LogbackLogPrinter(new JsonLogSerializer()); return Collections.singletonList(printer); } /** * 用户信息获取器 * @return 用户信息获取器 */ @Bean public LoginUserInfoHelper loginUserInfoHelper() { // 根据业务实际情况创建并实例化用户信息获取器, 并注册至IOC容器. return new EmptyLoginUserInfoHelper(); } /** * 添加拦截器 * @param registry 拦截器注册 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logPersistenceInterceptor); } }

解决请求体无法重复读取

Filter中,替换原始的请求对象,实现请求体可以重读读取
  1. 创建RequestWrapper对象, 用于包装原始Request对象

    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
    import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; /** * 请求包装器 * 用于解决请求体无法重复读取问题 * @author TheEnd */ public class RequestWrapper extends HttpServletRequestWrapper { public RequestWrapper(HttpServletRequest request) { super(request); } private volatile String requestBody; @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new StringReader(getRequestBody())); } @Override public ServletInputStream getInputStream() throws IOException { return new ServletInputStreamWrapper(requestBody.getBytes()); } private static class ServletInputStreamWrapper extends ServletInputStream { private final ByteArrayInputStream inputStream; public ServletInputStreamWrapper(byte[] body) { inputStream = new ByteArrayInputStream(body); } @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { } } private String getRequestBody() throws IOException { if (requestBody == null) { synchronized (this) { if (requestBody == null) { StringBuilder requestBodyBuilder = new StringBuilder(); BufferedReader reader = super.getReader(); String line; while ((line = reader.readLine()) != null) { requestBodyBuilder.append(line); } requestBody = requestBodyBuilder.toString(); } } } return requestBody; } }
  2. 编写过滤器并替换原始Request对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * 用于替换请求对象的过滤器 * 用于解决请求体无法重复读取问题 * @author TheEnd */ public class HttpServletRequestWrapperFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { if (!(servletRequest instanceof HttpServletRequest)) { filterChain.doFilter(servletRequest, servletResponse); return; } HttpServletRequest request = (HttpServletRequest) servletRequest; if (ContentTypeUtil.isBinaryContent(request) || request.getContentLength() < 1) { filterChain.doFilter(request, servletResponse); return; } filterChain.doFilter(new RequestWrapper(request), servletResponse); } }

评论已关闭