使用过滤器自动打印接口入参、出参首先要了解一个过滤器OncePerRequestFilter,一般使用这个过滤器进行日志打印。
一、OncePerRequestFilter
1)、什么是OncePerRequestFilter
回顾一下 Filter
的工作原理。Filter
可以在 Servlet
执行之前或之后调用。当请求被调度给一个 Servlet
时,RequestDispatcher
可能会将其转发给另一个 Servlet
。另一个 Servlet
也有可能使用相同的 Filter
。在这种情况下,同一个 Filter
会被调用多次。
但是,有时需要确保每个请求只调用一次特定的 Filter。一个常见的用例是在使用 Spring Security 时。当请求通过过滤器链(Filter Chain)时,对请求的身份证认证应该只执行一次。
在这种情况下,可以继承 OncePerRequestFilter
。Spring 保证 OncePerRequestFilter
只对指定请求执行一次。
2)、 OncePerRequestFilter
用法
java
@Component
public class AuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return true;
}
}
定义一个继承了 OncePerRequestFilter
的 AuthenticationFilter
Filter 类,一般重写的方法是以上两个。
shouldNotFilter:
- 这个方法用于指示是否应该跳过过滤器的执行。默认情况下,它返回
false
,表示应该执行过滤器。你可以根据需要重写这个方法,根据请求的条件来决定是否执行过滤器。例如,可以根据请求的路径或者参数来决定是否执行过滤器。
doFilterInternal:
- 这个方法就是具体的过滤器的逻辑
二、logFilter 具体实现:
举两个示例:都大同小异:
示例一:
java
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* @Title: LogFilter
* @Package com.hoau.hbdp.framework.server.web.filter
* @描述: Controller请求日志过滤器,不能使用拦截器实现,拦截器不能读取requestBody中参数
* @author
* @date
* @version V1.0
*/
@Slf4j
@Component
public class LogFilter extends OncePerRequestFilter {
Logger logger = LoggerFactory.getLogger(getClass());
/**
* 是否记录请求日志
*/
private boolean needLogRequest = true;
/**
* 是否记录响应日志
*/
private boolean needLogResponse = true;
/**
* 是否记录header
*/
private boolean needLogHeader = true;
/**
* 是否记录参数
*/
private boolean needLogPayload = true;
/**
* 记录的最大payload大小
*/
private int maxPayloadLength = 2*1024*1024;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 不进行过滤的请求pattern
*/
private List<String> excludeUrlPatterns = new ArrayList<String>(Arrays.asList("/health"));
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String url = request.getServletPath();
boolean matched = false;
for (String pattern : excludeUrlPatterns) {
matched = antPathMatcher.match(pattern, url);
if (matched) {
break;
}
}
return matched;
}
/**
* Same contract as for {@code doFilter}, but guaranteed to be
* just invoked once per request within a single request thread.
* See {@link #shouldNotFilterAsyncDispatch()} for details.
* <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
* default ServletRequest and ServletResponse ones.
*
* @param request
* @param response
* @param filterChain
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Date requestDate = new Date();
boolean isFirstRequest = !isAsyncDispatch(request);
//包装缓存requestBody信息
HttpServletRequest requestToUse = request;
if (isNeedLogPayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
}
//包装缓存responseBody信息
HttpServletResponse responseToUse = response;
if (isNeedLogPayload() && !(response instanceof ContentCachingResponseWrapper)) {
responseToUse = new ContentCachingResponseWrapper(response);
}
try {
filterChain.doFilter(requestToUse, responseToUse);
} finally {
//记录请求日志
if (isNeedLogRequest()) {
logRequest(requestToUse,requestDate);
}
//记录响应日志
if (isNeedLogResponse()) {
logResponse(responseToUse);
//把从response中读取过的内容重新放回response,否则客户端获取不到返回的数据
resetResponse(responseToUse);
}
}
}
/**
* 记录请求日志
* @param request
* @param requestDate
* @author
* @date
*/
protected void logRequest(HttpServletRequest request, Date requestDate) throws IOException {
String payload = isNeedLogPayload() ? getRequestPayload(request) : "";
logger.info(createRequestMessage(request, payload,requestDate));
}
/**
* 记录响应日志
* @param response
*/
protected void logResponse(HttpServletResponse response) {
String payload = isNeedLogPayload() ? getResponsePayload(response) : "";
logger.info(createResponseMessage(response, payload, new Date()));
}
/**
* 重新将响应参数设置到response中
* @param response
* @throws IOException
*/
protected void resetResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
wrapper.copyBodyToResponse();
}
}
/**
* 获取请求体中参数
* @param request
* @return
*/
protected String getRequestPayload(HttpServletRequest request) throws IOException {
String payload = "";
ContentCachingRequestWrapper wrapper =
WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
payload = getPayloadFromBuf(buf, wrapper.getCharacterEncoding());
}
return payload;
}
/**
* 获取响应体中参数
* @param response
* @return
*/
protected String getResponsePayload(HttpServletResponse response) {
String payload = "";
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
payload = getPayloadFromBuf(buf, wrapper.getCharacterEncoding());
}
return payload;
}
/**
* 创建请求日志实际需要打印的内容
* @param request
* @param payload
* @param requestDate
* @return
*/
protected String createRequestMessage(HttpServletRequest request, String payload, Date requestDate) {
StringBuilder msg = new StringBuilder();
msg.append("Inbound Message\n----------------------------\n");
msg.append("Address: ").append(request.getRequestURL()).append("\n");
msg.append("HttpMethod: ").append(request.getMethod()).append("\n");
// msg.append("QueryString: ").append(request.getQueryString()).append("\n");
// msg.append("RequestId: ").append(RequestContext.getRequestId()).append("\n");
// msg.append("RequestDate: ").append(DateUtils.convert(requestDate)).append("\n");
msg.append("Encoding: ").append(request.getCharacterEncoding()).append("\n");
msg.append("Content-Type: ").append(request.getContentType()).append("\n");
if (isNeedLogHeader()) {
msg.append("Headers: ").append(new ServletServerHttpRequest(request).getHeaders()).append("\n");
}
if (isNeedLogPayload()) {
int length = Math.min(payload.length(), getMaxPayloadLength());
msg.append("Payload: ").append(payload.substring(0, length)).append("\n");
}
msg.append("----------------------------------------------");
return msg.toString();
}
/**
* 创建响应日志实际需要打印的内容
* @param response
* @param payload
* @param responseDate
* @return
*/
protected String createResponseMessage(HttpServletResponse response, String payload, Date responseDate) {
StringBuilder msg = new StringBuilder();
msg.append("Outbound Message\n----------------------------\n");
// msg.append("RequestId: ").append(RequestContext.getRequestId()).append("\n");
// msg.append("ResponseDate: ").append(DateUtils.convert(responseDate)).append("\n");
msg.append("Encoding: ").append(response.getCharacterEncoding()).append("\n");
msg.append("Content-Type: ").append(response.getContentType()).append("\n");
if (isNeedLogHeader()) {
msg.append("Headers: ").append(new ServletServerHttpResponse(response).getHeaders()).append("\n");
}
boolean needLogContentType = true;
String contentType = response.getContentType();
// //excel文件导出的不需要记录
// if ("application/octet-stream;charset=UTF-8".equals(contentType)) {
// needLogContentType = false;
// }
//是JSON格式的才输出
needLogContentType = StringUtils.isEmpty(contentType) || contentType.toUpperCase().contains("JSON") || contentType.contains("text");
if (isNeedLogPayload() && needLogContentType) {
int length = Math.min(payload.length(), getMaxPayloadLength());
msg.append("Payload: ").append(payload.substring(0, length)).append("\n");
}
msg.append("----------------------------------------------");
return msg.toString();
}
/**
* 将bytep[]参数转换为字符串用于输出
* @param buf
* @param characterEncoding
* @return
*/
protected String getPayloadFromBuf(byte[] buf, String characterEncoding) {
String payload = "";
if (buf.length > 0) {
int length = Math.min(buf.length, getMaxPayloadLength());
try {
payload = new String(buf, 0, length, characterEncoding);
} catch (UnsupportedEncodingException ex) {
logger.error(ex.getMessage(), ex);
}
}
return payload;
}
public boolean isNeedLogRequest() {
return needLogRequest;
}
public void setNeedLogRequest(boolean needLogRequest) {
this.needLogRequest = needLogRequest;
}
public boolean isNeedLogResponse() {
return needLogResponse;
}
public void setNeedLogResponse(boolean needLogResponse) {
this.needLogResponse = needLogResponse;
}
public boolean isNeedLogHeader() {
return needLogHeader;
}
public void setNeedLogHeader(boolean needLogHeader) {
this.needLogHeader = needLogHeader;
}
public boolean isNeedLogPayload() {
return needLogPayload;
}
public void setNeedLogPayload(boolean needLogPayload) {
this.needLogPayload = needLogPayload;
}
public int getMaxPayloadLength() {
return maxPayloadLength;
}
public void setMaxPayloadLength(int maxPayloadLength) {
this.maxPayloadLength = maxPayloadLength;
}
public List<String> getExcludeUrlPatterns() {
return excludeUrlPatterns;
}
public void setExcludeUrlPatterns(List<String> excludeUrlPatterns) {
this.excludeUrlPatterns = excludeUrlPatterns;
}
}
上述代码解释:
1、首先请求进入过滤器之后,先进入shouldNotFilter方法,通过 getServletPath() 获取访问路径,然后再根据**AntPathMatche
类(专门用来进行路劲匹配的,可以单独了解一下)**来进行路径匹配,看是否是需要进行过滤,如果是就返回false 代表执行这个过滤器。
2、然后请求进入doFilterInternal方法,先进行一系列判断,然后如果需要记录日志,就将HttpServletRequest 对象转换为 ContentCachingRequestWrapper 对象,转换成ContentCachingRequestWrapper对象是为了缓存请求体内容,并允许多次读取和修改。因为HttpServletRequest
对象只能被读取一次,读取后的数据就无法再次获取。所以一般都会先转换为ContentCachingRequestWrapper对象类型。
然后经过判断,再将HttpServletResponse 转换为:ContentCachingResponseWrapper,原因同理。
3、最后在finally 中打印 请求体数据和响应体数据,
首先 logRequest 方法记录请求日志,在logRequest方法中经过判断,进 getRequestPayload 方法,目的是为了获取请求体中参数,在这个方法中用到了一个方法:
WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
这个方法我的理解是为了将HttpServletRequest对象 转换成 ContentCachingRequestWrapper对象 ,目的在上面也讲了,拿到ContentCachingRequestWrapper对象之后,就可以根据 wrapper.getContentAsByteArray() 方法获取请求体的字节数据了。再根据 getPayloadFromBuf 方法将字节数据转换成字符串。
获取到请求数据之后,再根据 createRequestMessage 方法拼接需要打印的数据即可。
响应数据同理,但需要注意的是,我们需要使用 wrapper.copyBodyToResponse() 方法,重新将响应参数设置到response中,不然客户端获取不到响应数据!
示例二:
java
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONValidator;
import jdk.nashorn.internal.ir.annotations.Ignore;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Slf4j
@Component
//@Order()
public class LogFilter extends OncePerRequestFilter implements Ordered {
/**
* 配置要记录请求的路径前缀
*/
private static final String NEED_TRACE_PATH_PREFIX = "/";
/**
* 忽略为multipart/form-data的ContentType的请求
*/
private static final Set<String> IGNORE_CONTENT_TYPE =new HashSet<>(Arrays.asList("multipart/form-data","application/octet-stream"));
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE+1 ;
}
@Override
@SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!isRequestValid(request)) {
filterChain.doFilter(request, response);
return;
}
RequestWrapper request1=new RequestWrapper(request);
ResponseWrapper response1=new ResponseWrapper(response);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
long startTime = System.currentTimeMillis();
String path = request.getRequestURI();
try {
if (path.startsWith(NEED_TRACE_PATH_PREFIX)) {
// 1. 记录日志
consoleRequestLog(request1);
}
} catch (Exception ignore){
log.error("请求日志打印异常",ignore);
}
filterChain.doFilter(request1, response1);
status = response1.getStatus();
try {
if (path.startsWith(NEED_TRACE_PATH_PREFIX)) {
// 1. 记录日志
consoleResponseLog(path, startTime, status, response1);
}
updateResponse(response1, response);
} catch (Exception ignore) {
log.error("响应日志输出异常",ignore);
}
}
private Boolean ignoreCheck(String contentType){
if(StrUtil.isNotBlank(contentType)) {
for (String s : IGNORE_CONTENT_TYPE) {
if (contentType.contains(s)){
return true;
}
}
}
return false;
}
/**
* 输出请求日志
* @param request
*/
private synchronized void consoleRequestLog(RequestWrapper request){
log.info("请求 | 请求路径:[{}] | 请求方法:[{}] | 请求IP:[{}] | 请求参数:{} | 请求Body:{} ",
request.getRequestURI(),
request.getMethod(),
request.getRemoteAddr(),
JSON.toJSONString(request.getParameterMap()),
getRequestBody(request)
);
}
/**
* 获取请求body
* @param request
* @return 请求body
*/
private String getRequestBody(RequestWrapper request) {
String requestBody="{}";
// ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
// if (wrapper != null) {
try {
// requestBody =new String(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
requestBody =request.getBody();
if (StrUtil.isNotBlank(requestBody) && (JSONValidator.from(requestBody).validate() || (requestBody.startsWith("<") && requestBody.endsWith(">")))) {
return requestBody;
} else if (StrUtil.isNotBlank(requestBody)) {
return "IOStream";
} else {
return "";
}
} catch (Exception ignore) {
log.error("请求体转换异常",ignore);
}
// }
return requestBody;
}
/**
* @Description: 打印日志
* @Param: [path - 请求路径, request - Http请求, startTime - 开始毫秒, status - 响应状态码, response - Http响应]
*/
private synchronized void consoleResponseLog(String path, long startTime, int status, ResponseWrapper response) {
log.info("返回 | 处理耗时:[{}ms] | 响应时间:[{}] | 响应状态:[{}] | 响应Body:{} ",
System.currentTimeMillis() - startTime,
LocalDateTime.now(),
status,
getResponseBody(response)
);
}
/**
* @Description: 判断请求是否合法
* @Param: [request]
* @return: {@link boolean}
*/
private boolean isRequestValid(HttpServletRequest request) {
try {
new URI(request.getRequestURL().toString());
return true;
} catch (URISyntaxException ex) {
return false;
}
}
/**
* @Description: 获取响应Body
* @Param: [response]
* @return: {@link String}
*/
private String getResponseBody(ResponseWrapper response) {
if (Objects.isNull(response.getDataStream())){
return "";
}
String responseBody = new String(response.getDataStream());;
if(JSONValidator.from(responseBody).validate()||responseBody.startsWith("<")&&responseBody.endsWith(">")) {
return responseBody;
}else{
return "FileStream";
}
}
/**
* @Description: 更新响应
* @Param: [response]
*/
private void updateResponse(ResponseWrapper response1,HttpServletResponse response) throws IOException {
response.getOutputStream().write(response1.getDataStream());
response.getOutputStream().flush();
// ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
// Objects.requireNonNull(responseWrapper).copyBodyToResponse();
}
}
java
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ByteUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.Part;
import java.io.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
@Slf4j
public class RequestWrapper extends ContentCachingRequestWrapper {
private final String MULTIPARTHEADER="multipart/form-data";
private byte[] body;
private Collection<Part> parts;
private BufferedReader reader;
private ServletInputStream inputStream;
public RequestWrapper(HttpServletRequest request) throws IOException, ServletException {
super(request);
if (StrUtil.isNotBlank(request.getContentType())&&request.getContentType().contains(MULTIPARTHEADER)){
parts=request.getParts();
}else {
//读一次 然后缓存起来
body = IoUtil.readBytes(request.getInputStream());
inputStream = new RequestCachingInputStream(body);
}
}
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return parts;
}
public String getBody() {
try {
if (Objects.nonNull(body)&&body.length>0) {
return new String(body, getCharacterEncoding());
}else{
return "";
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (inputStream != null) {
return inputStream;
}
return new RequestCachingInputStream(body);
}
@Override
public BufferedReader getReader() throws IOException {
if (reader == null) {
reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding()));
}
return reader;
}
private static class RequestCachingInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public RequestCachingInputStream(byte[] bytes) {
inputStream = new ByteArrayInputStream(bytes);
}
@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 readlistener) {
}
}
}
java
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ResponseWrapper extends ContentCachingResponseWrapper {
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
public byte[] getDataStream() {
return getContentAsByteArray();
}
}
代码解释:
1、首先实现了 Ordered 接口,需要实现 getOrder()
方法来指定 Bean 的加载顺序。
2、请求进入之后的逻辑类似,先进入 doFilterInternal() 方法,先判断请求是否合法,如果不合法就不进行日志打印。
3、然后将 HttpServletRequest 转换为 RequestWrapper 对象,其实RequestWrapper对象是自定义对象,继承了 ContentCachingRequestWrapper 对象。所以实际上还是转换成了 ContentCachingRequestWrapper 对象,主要是为了缓存请求数据。
RequestWrapper
的类,这个类继承自ContentCachingRequestWrapper
,并且用于包装HttpServletRequest
对象。主要功能:
处理请求体内容:
- 当请求的
Content-Type
是multipart/form-data
时,通过request.getParts()
获取多部分请求的所有Part
对象。- 否则,将请求体内容读取一次并缓存起来,以便后续的读取。
提供方法获取请求体内容:
getBody()
:返回请求体内容的字符串表示。getInputStream()
:返回输入流,可以用于读取请求体内容。getReader()
:返回字符流的BufferedReader
对象,可以用于读取请求体内容。代码解析:
构造方法
RequestWrapper(HttpServletRequest request)
:
- 根据请求的
Content-Type
来判断是否是multipart/form-data
类型的请求。- 如果是
multipart/form-data
,则调用request.getParts()
获取所有的Part
对象。- 否则,读取一次请求体内容,并将其缓存在
body
数组中。
getParts()
方法:
- 如果是
multipart/form-data
请求,则直接返回之前获取的parts
集合。- 否则,返回
null
。
getBody()
方法:
- 返回请求体内容的字符串表示,首先判断
body
数组是否为空,然后将其转换为字符串返回。
getInputStream()
方法:
- 如果
inputStream
不为空,则直接返回该输入流。- 否则,创建一个新的
RequestCachingInputStream
对象,并将之前缓存的body
数组传入其中。
getReader()
方法:
- 如果
reader
为空,则创建一个新的BufferedReader
对象,并使用inputStream
创建。- 否则,直接返回之前创建的
reader
对象。
RequestCachingInputStream
内部类:
- 继承自
ServletInputStream
,用于提供一个包装body
数组的输入流。- 实现了
read()
方法,用于读取字节数据。- 实现了
isFinished()
方法,用于判断输入流是否已经读取完毕。- 实现了
isReady()
方法,始终返回true
。- 实现了
setReadListener()
方法,空实现。
4、缓存完请求对象的内容之后, 使用consoleRequestLog()方法打印日志,在这个方法中,它记录了 请求路径:[{}] 、 请求方法:[{}]、请求IP:[{}]、请求参数:{} 、 请求Body:{},request.getParameterMap() 就是专门获取get请求 请求参数的。
其中 请求Body 需要使用 getRequestBody 方法获取,这个方法中主要是这个判断需要说一下:
java
if (StrUtil.isNotBlank(requestBody) && (JSONValidator.from(requestBody).validate() || (requestBody.startsWith("<") && requestBody.endsWith(">"))))
这个判断主要是为了 requestBody 不为空时,且当请求体是 json数据时,或xml数据时才进行打印。
JSONValidator.from(requestBody).validate()
:使用 JSON 格式验证器验证请求体内容是否是有效的 JSON 格式。(requestBody.startsWith("<") && requestBody.endsWith(">"))
:检查请求体内容是否以<
开头且以>
结尾。
响应数据同理!
三、MDC:
MDC
,即Mapped Diagnostic Context
,是 logback 日志框架提供的一种上下文信息存储的机制,可以在日志输出中方便地添加和显示额外的上下文信息。MDC 的作用:
MDC 允许你在一个线程中存储一些额外的信息,并在该线程执行的任何代码中访问这些信息。这些信息可以是任何与日志记录相关的数据,比如用户 ID、请求 ID、会话 ID 等。通过 MDC,你可以将这些信息附加到日志消息中,使日志更加丰富和有用。
简单说一下使用 MDC 添加日志链路追踪id 和 ip 以及 userAgent
java
import cn.hutool.core.lang.UUID;
import cn.hutool.http.Header;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@WebFilter(filterName = "logRequestIdFilter", urlPatterns = "/*")
@Order(Integer.MIN_VALUE)
public class LogRequestIdFilter implements Filter {
public static final String TRACE_ID = "traceId";
public static final String IP = "ip";
public static final String CLIENT_INFO = "client_info";
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
String requestId = httpRequest.getHeader(TRACE_ID);
String userAgent = httpRequest.getHeader(Header.USER_AGENT.getValue());
String realIp = RemortIPUtil.getRealIp(httpRequest);
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
MDC.put(TRACE_ID, requestId);
MDC.put(IP,realIp);
MDC.put(CLIENT_INFO,userAgent);
httpResponse.setHeader(TRACE_ID, requestId);
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
MDC.clear();
}
}
}
java
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Pattern;
@Slf4j
public class RemortIPUtil {
// 将长Ip截取
public static String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
// 获得反向代理IP
String ip = request.getHeader("x-forwarded-for");
if (null!=ip&&""!=ip) {
if (ip.indexOf(",") > -1) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
public static String getRemortIPLong(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
public static String getRealIp(HttpServletRequest req) {
String regex = "([0-9]{1,3}.){3}[0-9]{1,3}";
String ip = req.getRemoteAddr();
if(null != req.getHeader("HTTP_CDN_SRC_IP") && Pattern.matches(regex, req.getHeader("HTTP_CDN_SRC_IP"))) {
ip = req.getHeader("HTTP_CDN_SRC_IP");
}else if(null != req.getHeader("X-FORWARDED-FOR") && Pattern.matches(regex, req.getHeader("X-FORWARDED-FOR"))) {
ip = req.getHeader("X-FORWARDED-FOR");
}else if(null != req.getHeader("X-REAL-IP") && Pattern.matches(regex, req.getHeader("X-REAL-IP"))) {
ip = req.getHeader("X-REAL-IP");
}else if(null != req.getHeader("HTTP_X_REAL_FORWARDED_FOR") && Pattern.matches(regex, req.getHeader("HTTP_X_REAL_FORWARDED_FOR"))) {
ip = req.getHeader("HTTP_X_REAL_FORWARDED_FOR");
}else if(null != req.getHeader("HTTP_X_FORWARDED_FOR") && Pattern.matches(regex, req.getHeader("HTTP_X_FORWARDED_FOR"))) {
ip = req.getHeader("HTTP_X_FORWARDED_FOR");
}else if(null != req.getHeader("HTTP_X_REAL_IP") && Pattern.matches(regex, req.getHeader("HTTP_X_REAL_IP"))) {
ip = req.getHeader("HTTP_X_REAL_IP");
}else if(null != req.getHeader("HTTP_CLIENT_IP") && Pattern.matches(regex, req.getHeader("HTTP_CLIENT_IP"))) {
ip = req.getHeader("HTTP_CLIENT_IP");
}
return ip;
}
public static Long ip2Long(String ip) {
String regex = "([0-9]{1,3}.){3}[0-9]{1,3}";
if(Pattern.matches(regex, ip)) {
String[] ips = ip.split("\\.");
Long v = Long.valueOf(ips[0]);
Long v1 = Long.valueOf(ips[1]);
Long v2 = Long.valueOf(ips[2]);
Long v3 = Long.valueOf(ips[3]);
return (v << 24) + (v1 << 16) + (v2 << 8) + v3;
}
return null;
}
}
- 将获取到的
TRACE_ID
、User-Agent
和realIp
放入MDC
中:TRACE_ID
:用于标识请求的唯一标识符。从请求头中获取,如果请求头中没有,则生成一个随机的 UUID。User-Agent
:客户端的用户代理信息,通常是浏览器的相关信息。realIp
:真实的客户端 IP 地址,可能会通过代理等方式隐藏。
在MDC中设置了之后, 我们在logback日志的配置文件中,可以是使用下面的方式直接引用:
- %X{ip}
- %X{traceId}
在配置logback日志配置文件时,在需要的地方引入即可。这样每个日志打印时都会有 traceId 和 ip 显示了。