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

使用过滤器自动打印接口入参、出参首先要了解一个过滤器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;
    }
}

定义一个继承了 OncePerRequestFilterAuthenticationFilter 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 对象。

主要功能:

  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 方法获取,这个方法中主要是这个判断需要说一下:

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

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

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

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

相关推荐
一颗松鼠几秒前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
有梦想的咸鱼_2 分钟前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
海阔天空_20137 分钟前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
天下皆白_唯我独黑15 分钟前
php 使用qrcode制作二维码图片
开发语言·php
小灰灰__15 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭18 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
小远yyds20 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
何曾参静谧32 分钟前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
程序媛小果39 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林44 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac