SpringBoot 实现全局统一请求日志处理
相关工具类
内容类型判断工具类
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-"); } }
客户端IP获取工具类
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 ""; } }
获取已登录用户信息封装
此信息属于业务信息, 需要按照业务实际情况进行编写, 所以此处只提供接口和一个空实现
获取已登录用户信息接口
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);
}
默认空实现, 如不需要记录登录人信息, 使用此类即可
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 "";
}
}
可以根据实际业务需求自定义实现类.
编写日志类
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;
}
编写相关注解
日志额外信息注解
用于通过注解给日志记录增加额外信息
可以标注在类上和方法上, 标注在类上时类中所有的方法的日志都有此信息. 类和方法上都有标注时,优先生效方法上的.
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 ""; }
不记录日志注解
标注了此注解的类和方法不会进行日志记录
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
接口即可.
序列化器接口
import org.springframework.lang.NonNull; /** * 日志序列化器 * @author TheEnd */ public interface LogSerializer { /** * 序列化 * @param log 日志对象 * @return 序列化数据 */ byte[] serializer(@NonNull RequestLog log); }
JDK ToString 序列化
import org.springframework.lang.NonNull; /** * 将日志对象通过ToString进行序列化的序列化器 * @author TheEnd */ public class ToStringLogSerializer implements LogSerializer { @Override public byte[] serializer(@NonNull RequestLog log) { return log.toString().getBytes(); } }
Jackson ToJson 序列化
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); } } }
编写日志打印器
日志打印器用于将日志输出至指定位置, 提供打印器接口和默认实现, 可以根据业务需求, 自定义打印器.
可以将日志输出至文件,控制台,消息中间件,数据库,网络流等
打印器接口
import org.springframework.lang.NonNull; /** * 日志打印器 * @author TheEnd */ public interface LogPrinter { /** * 打印日志 * @param log 日志对象 */ void print(@NonNull RequestLog log); }
Logback 打印器
通过
Logback
打印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)); } }
编写日志拦截器类
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;
}
};
}
}
配置拦截器
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
中,替换原始的请求对象,实现请求体可以重读读取
创建
RequestWrapper
对象, 用于包装原始Request
对象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; } }
- 编写过滤器并替换原始
Request
对象
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);
}
}
评论已关闭