java日志框架总结(七、使用过滤器自动打印接口入参、出参)

        使用过滤器自动打印接口入参、出参首先要了解一个过滤器OncePerRequestFilter,一般使用这个过滤器进行日志打印。

        一、OncePerRequestFilter

        1)、什么是OncePerRequestFilter  

        回顾一下 Filter 的工作原理。Filter 可以在 Servlet 执行之前或之后调用。当请求被调度给一个 Servlet 时,RequestDispatcher 可能会将其转发给另一个 Servlet。另一个 Servlet 也有可能使用相同的 Filter。在这种情况下,同一个 Filter 会被调用多次

但是,有时需要确保每个请求只调用一次特定的 Filter。一个常见的用例是在使用 Spring Security 时。当请求通过过滤器链(Filter Chain)时,对请求的身份证认证应该只执行一次。

在这种情况下,可以继承 OncePerRequestFilter。Spring 保证 OncePerRequestFilter 只对指定请求执行一次。

        2)、 OncePerRequestFilter 用法
@Component
public class AuthenticationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {filterChain.doFilter(request, response);}@Overrideprotected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {return true;}
}

         定义一个继承了 OncePerRequestFilter 的 AuthenticationFilter Filter 类,一般重写的方法是以上两个。

       shouldNotFilter

  • 这个方法用于指示是否应该跳过过滤器的执行。默认情况下,它返回false,表示应该执行过滤器。你可以根据需要重写这个方法,根据请求的条件来决定是否执行过滤器。例如,可以根据请求的路径或者参数来决定是否执行过滤器。

        doFilterInternal:

  • 这个方法就是具体的过滤器的逻辑

        二、logFilter 具体实现:

        举两个示例:都大同小异:
     

          示例一:

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"));@Overrideprotected 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*/@Overrideprotected 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中,不然客户端获取不到响应数据!

        示例二:
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"));@Overridepublic 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();}}
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);}}@Overridepublic 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);}}@Overridepublic ServletInputStream getInputStream() throws IOException {if (inputStream != null) {return inputStream;}return new RequestCachingInputStream(body);}@Overridepublic 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);}@Overridepublic int read() throws IOException {return inputStream.read();}@Overridepublic boolean isFinished() {return inputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener readlistener) {}}
}
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 对象。

主要功能:

  1. 处理请求体内容

    • 当请求的 Content-Typemultipart/form-data 时,通过 request.getParts() 获取多部分请求的所有 Part 对象。
    • 否则,将请求体内容读取一次并缓存起来,以便后续的读取。
  2. 提供方法获取请求体内容

    • getBody():返回请求体内容的字符串表示。
    • getInputStream():返回输入流,可以用于读取请求体内容。
    • getReader():返回字符流的 BufferedReader 对象,可以用于读取请求体内容。

代码解析:

  1. 构造方法 RequestWrapper(HttpServletRequest request)

    • 根据请求的 Content-Type 来判断是否是 multipart/form-data 类型的请求。
    • 如果是 multipart/form-data,则调用 request.getParts() 获取所有的 Part 对象。
    • 否则,读取一次请求体内容,并将其缓存在 body 数组中。
  2. getParts() 方法:

    • 如果是 multipart/form-data 请求,则直接返回之前获取的 parts 集合。
    • 否则,返回 null
  3. getBody() 方法:

    • 返回请求体内容的字符串表示,首先判断 body 数组是否为空,然后将其转换为字符串返回。
  4. getInputStream() 方法:

    • 如果 inputStream 不为空,则直接返回该输入流。
    • 否则,创建一个新的 RequestCachingInputStream 对象,并将之前缓存的 body 数组传入其中。
  5. getReader() 方法:

    • 如果 reader 为空,则创建一个新的 BufferedReader 对象,并使用 inputStream 创建。
    • 否则,直接返回之前创建的 reader 对象。
  6. RequestCachingInputStream 内部类:

    • 继承自 ServletInputStream,用于提供一个包装 body 数组的输入流。
    • 实现了 read() 方法,用于读取字节数据。
    • 实现了 isFinished() 方法,用于判断输入流是否已经读取完毕。
    • 实现了 isReady() 方法,始终返回 true
    • 实现了 setReadListener() 方法,空实现。

        4、缓存完请求对象的内容之后, 使用consoleRequestLog()方法打印日志,在这个方法中,它记录了  请求路径:[{}] 、 请求方法:[{}]、请求IP:[{}]、请求参数:{} 、 请求Body:{},request.getParameterMap() 就是专门获取get请求  请求参数的。

        其中 请求Body 需要使用 getRequestBody 方法获取,这个方法中主要是这个判断需要说一下:

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

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";@Overridepublic 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();}}
}
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();}// 获得反向代理IPString 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_IDUser-AgentrealIp 放入 MDC 中:
  • TRACE_ID:用于标识请求的唯一标识符。从请求头中获取,如果请求头中没有,则生成一个随机的 UUID。
  • User-Agent:客户端的用户代理信息,通常是浏览器的相关信息。
  • realIp:真实的客户端 IP 地址,可能会通过代理等方式隐藏。

        在MDC中设置了之后, 我们在logback日志的配置文件中,可以是使用下面的方式直接引用:

  • %X{ip}
  • %X{traceId}

        在配置logback日志配置文件时,在需要的地方引入即可。这样每个日志打印时都会有 traceId 和 ip 显示了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/698798.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT/GPT4科研应用与AI绘图及论文写作

2023年随着OpenAI开发者大会的召开&#xff0c;最重磅更新当属GPTs&#xff0c;多模态API&#xff0c;未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚于互联网和个人电脑的问世。360创始人周鸿祎认为未来各行各业如果不能搭上这班车…

重看LeakCanary

LeakCanary是我很久之前看的东西了&#xff0c;我当时侯对它的印象就是它可以用来检测内存泄漏&#xff0c;具体原理就是将弱引用对象延迟个5s然后看是否被回收,如果没有被回收,那么就说明发生了内存泄漏,其他的也没有仔细地看 现在就详细地梳理一遍这个流程&#xff1a; 1.L…

微服务篇之分布式事务

一、Seata架构 Seata事务管理中有三个重要的角色&#xff1a; TC (Transaction Coordinator) - 事务协调者&#xff1a;维护全局和分支事务的状态&#xff0c;协调全局事务提交或回滚。 TM (Transaction Manager) - 事务管理器&#xff1a;定义全局事务的范围、开始全局事务、…

docker学习总结

docker 1.初识Docker1.1.什么是Docker1.1.1.应用部署的环境问题1.1.2.Docker解决依赖兼容问题1.1.3.Docker解决操作系统环境差异1.1.4.小结 1.2.Docker和虚拟机的区别1.3.Docker架构1.3.1.镜像和容器1.3.2.DockerHub1.3.3.Docker架构1.3.4.小结 1.4.安装Docker 2.Docker的基本操…

Kubernetes Prometheus 系列|Prometheus介绍和使用|Prometheus+Grafana集成

目录 第1章Prometheus 入门1.1 Prometheus 的特点1.1.1 易于管理1.1.2 监控服务的内部运行状态1.1.3 强大的数据模型1.1.4 强大的查询语言 PromQL1.1.5 高效1.1.6 可扩展1.1.7 易于集成1.1.8 可视化1.1.9 开放性 1.2 Prometheus 的架构1.2.1 Prometheus 生态圈组件1.2.2 架构理…

Go 数据库编程精粹:database/sql 实用技巧解析

Go 数据库编程精粹&#xff1a;database/sql 实用技巧解析 简介database/sql 库的基础知识核心概念连接池驱动事务 环境配置 建立数据库连接连接到数据库示例&#xff1a;连接 MySQL 数据库连接池管理 执行查询和处理结果基本查询执行多行查询执行单行查询 结果处理处理多行结果…

基于Java SSM框架实现问卷调查系统项目【项目源码】

基于java的SSM框架实现问卷调查系统演示 B/S结构 BROWSER/SERVER程序架构方式是使用电脑中安装的各种浏览器来进行访问和使用的&#xff0c;相比C/S的程序结构不需要进行程序的安装就可以直接使用。BROWSER/SERVER架构的运行方式是在远程的服务器上进行安装一个&#xff0c;然…

普中51单片机学习(DS1302)

DS1302时钟 DS1302实时时钟具有能计算2100年之前的秒、分、时、日、日期、星期、月、年的能力&#xff0c;还有闰年调整的能力。内部含有31个字节静态RAM&#xff0c;可提供用户访问。采用串行数据传送方式&#xff0c;使得管脚数量最少&#xff0c;简单SPI 3线接口。工作电压…

4.8 Verilog过程连续赋值

关键词&#xff1a;解除分配&#xff0c;强制&#xff0c;释放 过程连续赋值是过程赋值的一种。赋值语句能够替换其他所有wire 或 reg 的赋值&#xff0c;改写wire 或 reg 类型变量的当前值。 与过程赋值不同的是&#xff0c;过程连续赋值表达式能被连续的驱动到wire 或 reg …

C++——基础语法(2):函数重载

4. 函数重载 函数重载就是同一个函数名可以重复被定义&#xff0c;即允许定义相同函数名的函数。但是相同名字的函数怎么在使用的时候进行区分呢&#xff1f;所以同一个函数名的函数之间肯定是要存在不同点的&#xff0c;除了函数名外&#xff0c;还有返回类型和参数两部分可以…

Composition API 和 Options API

为什么Composition API 比 Options API 更好 Composition API是Vue.js 3.x版本引入的一种新的组织代码的方式。它相对于Options API有一些明显的优势&#xff0c;使得它在某些场景下更加灵活和易于使用。 更好的逻辑组织&#xff1a;Composition API允许将相关代码逻辑打包在一…

如何进行数据库分区和分片操作?

什么是数据库分区和分片&#xff1f; 数据库分区和分片都是数据库物理设计中的技术&#xff0c;旨在提高数据库的性能和管理大规模数据。 数据库分区是一种物理数据库的设计技术&#xff0c;其主要目的是在特定的SQL操作中减少数据读写的总量以缩减响应时间。分区并不是生成新…

mysql 输出所在月份的最后一天

内置函数 LAST_DAY(date) 参数&#xff1a; date &#xff1a;一个日期或日期时间类型的值&#xff0c;表示要获取所在月份最后一天的日期。 返回值&#xff1a; 返回一个日期值&#xff0c;表示输入日期所在月份的最后一天。 栗子 月总刷题数和日均刷题数_牛客题霸_牛客…

本地配置多个git账户及ll设置

本地配置多个git账户 清除全局配置将命令行&#xff0c;切换到ssh目录生成GitLab和Gitee的公钥、私钥去对应的代码仓库添加 SSH Keys添加私钥ll设置 管理密钥验证仓库配置关于gitgitee.com: Permission denied (publickey) 清除全局配置 此步骤可以不做&#xff0c;经测试不影…

总结一下最近几个主界面

目前展示了用Avalonia做几个主要流行的主界面&#xff0c;演示了一下组件的使用。用不同的实现方式实现一些方法。 1、独立大屏展示&#xff0c;类似一个实时监控&#xff0c;这是一种目前很方便的大屏效果。 主要涉及的内内容&#xff1a; &#xff08;1&#xff09;窗标题实…

【视频编码\VVC】环路滤波基础知识

本文为新一代通用视频编码H.266\VVC原理、标准与实现的简化笔记。 定义&#xff1a;在视频编码过程中进行滤波&#xff0c;滤波后的图像用于后续编码。 目的&#xff1a;1、提升编码图像的质量。2、为后续编码图像提供高质量参考&#xff0c;获得更好的预测效果。 VVC中主要…

使用LinkedList实现堆栈及Set集合特点、遍历方式、常见实现类

目录 一、使用LinkedList实现堆栈 堆栈 LinkedList实现堆栈 二、集合框架 三、Set集合 1.特点 2.遍历方式 3.常见实现类 HashSet LinkedHashSet TreeSet 一、使用LinkedList实现堆栈 堆栈 堆栈&#xff08;stack&#xff09;是一种常见的数据结构&#xff0c;一端…

后端程序员入门react笔记(五)ajax请求

常见的ajax Ajax 最原始的方式&#xff0c;基于原生的jsXmlHttpRequest 多个请求之间如果有先后关系&#xff0c;会存在很多层回调的问题&#xff0c;也是基于原生jsJquery Ajax 基于原生XHR封装&#xff0c;依赖Jquery框架&#xff0c;由jquery 框架去封装原生的XML(Xml)封装…

【高德地图】Android高德地图控件交互详细介绍

&#x1f4d6;第5章 与地图控件交互 ✅控件交互&#x1f9ca;缩放按钮&#x1f9ca;指南针&#x1f9ca;定位按钮&#x1f9ca;地图Logo ✅手势交互&#x1f9ca;缩放手势&#x1f9ca;滑动手势&#x1f9ca;旋转手势&#x1f9ca;倾斜手势&#x1f9ca;指定屏幕中心点的手势操…

CSP-J 2023 T1 小苹果

文章目录 题目题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 题目传送门题解思路总代码 提交结果尾声 题目 题目描述 小 Y 的桌子上放着 n n n 个苹果从左到右排成一列&#xff0c;编号为从 1 1 1 到 n n n。 小苞是小 Y 的好朋友&#xff0c;每天她都会从…