SpringBoot 记录HTTP请求日志,curl格式,可直接运行于bash,也可导入postman

910 阅读4分钟

新建LoggableDispatcherServlet 类:

package com.hsh.common.dispatch;


import cn.hutool.core.collection.CollectionUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.compress.utils.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.*;

@Order(Ordered.HIGHEST_PRECEDENCE)//最高优先级 方便拦截404什么的
public class LoggableDispatcherServlet extends DispatcherServlet {

    private static final Logger logger = LoggerFactory.getLogger("HttpLogger");

    private static final ObjectMapper mapper = new ObjectMapper();

    private static final long serialVersionUID = -2151909516770706554L;

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
//        if ("GET".equals(request.getMethod())) {
//            super.doDispatch(request, response);
//            return;
//        }
        List<String> curlItemList = new ArrayList<>();
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        setThrowExceptionIfNoHandlerFound(true);
        ObjectNode rootNode = mapper.createObjectNode();
        ObjectNode reqNode = mapper.createObjectNode();
        ObjectNode resNode = mapper.createObjectNode();
        String method = request.getMethod();
        curlItemList.add(MessageFormat.format("-X ''{0}''", method));
        rootNode.put("method", method);
        String requestUrl = request.getRequestURL().toString();
        rootNode.put("url", requestUrl);
        rootNode.put("remoteAddr", request.getRemoteAddr());
        rootNode.put("x-forwarded-for", request.getHeader("x-forwarded-for"));
        rootNode.set("request", reqNode);
        rootNode.set("response", resNode);
        reqNode.set("headers", mapper.valueToTree(getRequestHeaders(request)));

        try {
            reqNode.set("query", mapper.valueToTree(request.getParameterMap()));
            if ("GET".equals(method)) {
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append("'").append(requestUrl);
                Enumeration<String> parameterNames = request.getParameterNames();
                if (parameterNames.hasMoreElements()) {
                    stringBuilder.append("?");
                }
                while (parameterNames.hasMoreElements()) {
                    String name = parameterNames.nextElement();
                    String[] values = request.getParameterValues(name);
                    for (String value : values) {
                        stringBuilder.append(name).append("=").append(value).append("&");
                    }
                }
                stringBuilder.append("'");
                curlItemList.add(stringBuilder.toString());
                super.doDispatch(request, responseWrapper);
            } else {
                curlItemList.add("'" + requestUrl + "'");
                if (isFormPost(request)) {
                    ContentCachingRequestWrapper bufferedServletRequestWrapper = new ContentCachingRequestWrapper(request);
                    reqNode.set("body", mapper.valueToTree(request.getParameterMap()));
                    reqNode.put("bodyIsJson", false);
                    StringBuilder stringBuilder = new StringBuilder();
                    Enumeration<String> parameterNames = request.getParameterNames();
                    while (parameterNames.hasMoreElements()) {
                        String name = parameterNames.nextElement();
                        String[] values = request.getParameterValues(name);
                        for (String value : values) {
                            stringBuilder.append(name).append("=").append(value).append("&");
                        }
                    }
                    curlItemList.add(MessageFormat.format("--data-raw ''{0}''", stringBuilder.toString()));
                    super.doDispatch(bufferedServletRequestWrapper, responseWrapper);
                } else if (isJsonPost(request)) {
                    BufferedServletRequestWrapper bufferedServletRequestWrapper = new BufferedServletRequestWrapper(request);
                    ServletInputStream inputStream = bufferedServletRequestWrapper.getInputStream();
                    byte[] contentAsByteArray = IOUtils.toByteArray(inputStream);
                    reqNode.set("body", mapper.readTree(contentAsByteArray));
                    reqNode.put("bodyIsJson", true);
                    curlItemList.add(MessageFormat.format("--data-binary ''{0}''", mapper.readTree(contentAsByteArray)));
                    super.doDispatch(bufferedServletRequestWrapper, responseWrapper);
                } else if (isTextPost(request) || isXmlPost(request)) {
                    BufferedServletRequestWrapper bufferedServletRequestWrapper = new BufferedServletRequestWrapper(request);
                    ServletInputStream inputStream = bufferedServletRequestWrapper.getInputStream();
                    byte[] contentAsByteArray = IOUtils.toByteArray(inputStream);
                    reqNode.put("body", new String(contentAsByteArray));
                    reqNode.put("bodyIsJson", false);
                    curlItemList.add(MessageFormat.format("--data-binary ''{0}''", new String(contentAsByteArray)));
                    super.doDispatch(bufferedServletRequestWrapper, responseWrapper);
                } else if (isMediaPost(request)) {
                    reqNode.put("body", "Media Request Body ContentLength = " + request.getContentLengthLong());
                    reqNode.put("bodyIsJson", false);
                    super.doDispatch(request, responseWrapper);
                } else {
                    reqNode.put("body", "Unknown Request Body ContentLength = " + request.getContentLengthLong());
                    reqNode.put("bodyIsJson", false);
                    super.doDispatch(request, responseWrapper);
                }
            }
            HandlerExecutionChain handlerExecutionChain = getHandler(request);
            if (handlerExecutionChain == null) {
                //手动判断是不是404 不走系统流程 直接处理 因为会重定向/error
                resNode.put("status", HttpStatus.NOT_FOUND.value());
                logger.info(rootNode.toString());
                response.setStatus(HttpStatus.NOT_FOUND.value());
                PrintWriter writer = response.getWriter();
                writer.write("Request path not found");
                writer.flush();
                writer.close();
                return;
            }
            System.out.println(handlerExecutionChain);
        } finally {
            byte[] responseWrapperContentAsByteArray = responseWrapper.getContentAsByteArray();
            responseWrapper.copyBodyToResponse();//这里有顺序 必须先读body 然后再调用这个方法 才能继续读
            resNode.put("status", response.getStatus());
            Map<String, Object> responseHeaders = getResponseHeaders(response);

            //这里判断错误拦截是不是吧url改成error了 如果是就做一下替换 替换的值是错误拦截器写到header里面的
            String url = rootNode.get("url").asText();
            if (url.endsWith("/error")) {
                String path = (String) responseHeaders.get("x-error-path");
                if (!ObjectUtils.isEmpty(path)) {
                    rootNode.put("url", url.replace("/error", path));
                }
            }
            resNode.set("headers", mapper.valueToTree(responseHeaders));
            if (isProtoBufPost(responseWrapper) || "GET".equals(request.getMethod())) {
            } else {
                try {
                    resNode.set("body", mapper.readTree(responseWrapperContentAsByteArray));
                    resNode.put("bodyIsJson", true);
                } catch (Exception e) {
                    resNode.put("body", new String(responseWrapperContentAsByteArray));
                    resNode.put("bodyIsJson", false);
                }
            }
            logger.info(rootNode.toPrettyString());
            getCurlRequestHeaders(request, curlItemList);
            logger.info("curl " + CollectionUtil.join(curlItemList, "\n   "));
        }
    }

    private Map<String, Object> getRequestHeaders(HttpServletRequest request) {
        Map<String, Object> headers = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            headers.put(headerName, request.getHeader(headerName));
        }
        return headers;

    }

    private void getCurlRequestHeaders(HttpServletRequest request, List<String> list) {
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            if (!headerValue.contains("multipart/form-data")) {
                list.add(MessageFormat.format("-H ''{0}: {1}''", headerName, headerValue));
            }
        }
    }

    private Map<String, Object> getResponseHeaders(HttpServletResponse response) {
        Map<String, Object> headers = new HashMap<>();
        Collection<String> headerNames = response.getHeaderNames();
        for (String headerName : headerNames) {
            headers.put(headerName, response.getHeader(headerName));
        }
        return headers;
    }

    private boolean isFormPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        return (contentType != null && contentType.contains("x-www-form"));
    }

    private boolean isMediaPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return contentType.contains("stream") || contentType.contains("image") || contentType.contains("video") || contentType.contains("audio");
        }
        return false;
    }

    private boolean isTextPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return contentType.contains("text/plain") || contentType.contains("text/xml") || contentType.contains("text/html");
        }
        return false;
    }

    private boolean isJsonPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return contentType.contains("application/json");
        }
        return false;
    }

    private boolean isXmlPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return contentType.contains("application/xml");
        }
        return false;
    }

    private boolean isProtoBufPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return contentType.contains("application") && contentType.contains("protobuf");
        }
        return false;
    }

    private boolean isProtoBufPost(HttpServletResponse response) {
        String contentType = response.getContentType();
        if (contentType != null) {
            return contentType.contains("application") && contentType.contains("protobuf");
        }
        return false;
    }

    private boolean isMultipartFormDataPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return contentType.contains("multipart/form-data");
        }
        return false;
    }

    class BufferedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream inputStream;
        private ServletInputStream is;

        public BufferedServletInputStream(byte[] buffer, ServletInputStream is) {
            this.is = is;
            this.inputStream = new ByteArrayInputStream(buffer);
        }

        @Override
        public int available() {
            return inputStream.available();
        }

        @Override
        public int read() {
            return inputStream.read();
        }

        @Override
        public int read(byte[] b, int off, int len) {
            return inputStream.read(b, off, len);
        }

        @Override
        public boolean isFinished() {
            return is.isFinished();
        }

        @Override
        public boolean isReady() {
            return is.isReady();
        }

        @Override
        public void setReadListener(ReadListener listener) {
            is.setReadListener(listener);
        }
    }

    class BufferedServletRequestWrapper extends HttpServletRequestWrapper {
        private byte[] buffer;
        private ServletInputStream is;

        public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            this.is = request.getInputStream();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(IOUtils.toByteArray(is));
            this.buffer = byteArrayOutputStream.toByteArray();
        }

        @Override
        public ServletInputStream getInputStream() {
            return new BufferedServletInputStream(this.buffer, this.is);
        }
    }
}

新建DispatchConfig类,这个配置类是替换spring boot的默认配置,

不同版本的配置有所不同,可直接复制DispatcherServletAutoConfiguration类中dispatcherServletRegistration方法和dispatcherServlet方法的内容,并替换DispatcherServlet类为上文的LoggableDispatcherServlet类

package com.hsh.common.config;

import com.hsh.common.dispatch.LoggableDispatcherServlet;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.MultipartConfigElement;

/**
 * @author lmx 2020/12/30 15:49
 */
@Configuration
public class DispatchConfig {

    @Bean
    @Primary
    public ServletRegistrationBean dispatcherRegistration(DispatcherServlet dispatcherServlet,
                                                          WebMvcProperties webMvcProperties,
                                                          ObjectProvider<MultipartConfigElement> multipartConfig) {
        DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
        registration.setName("dispatcherServlet");
        registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
        multipartConfig.ifAvailable(registration::setMultipartConfig);
        return registration;
    }

    @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
        LoggableDispatcherServlet dispatcherServlet = new LoggableDispatcherServlet();
        dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
        dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
        dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
        dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
        return dispatcherServlet;
    }

}

日志示例:

2021-01-07 11:44:19.817  INFO 23752 --- [nio-9083-exec-3] HttpLogger                               : {
  "method" : "GET",
  "url" : "http://172.16.3.33:9083/api/admin",
  "remoteAddr" : "172.16.3.33",
  "x-forwarded-for" : null,
  "request" : {
    "headers" : {
      "referer" : "http://172.16.3.33:9410/",
      "accept-language" : "zh-CN,zh;q=0.9",
      "origin" : "http://172.16.3.33:9410",
      "host" : "172.16.3.33:9083",
      "connection" : "keep-alive",
      "x-auth-token" : "2fd100d9-d46a-4a18-8737-c32421cfb491",
      "accept-encoding" : "gzip, deflate",
      "accept" : "application/json, text/plain, */*",
      "user-agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
    },
    "query" : {
      "current" : [ "1" ],
      "size" : [ "10" ]
    }
  },
  "response" : {
    "status" : 200,
    "headers" : {
      "Keep-Alive" : "timeout=60",
      "Access-Control-Allow-Origin" : "http://172.16.3.33:9410",
      "X-Content-Type-Options" : "nosniff",
      "Connection" : "keep-alive",
      "Pragma" : "no-cache",
      "Date" : "Thu, 07 Jan 2021 03:44:19 GMT",
      "X-Frame-Options" : "DENY",
      "Access-Control-Expose-Headers" : "X-Auth-Token",
      "Cache-Control" : "no-cache, no-store, max-age=0, must-revalidate",
      "Access-Control-Allow-Credentials" : "true",
      "Vary" : "Origin",
      "Expires" : "0",
      "X-XSS-Protection" : "1; mode=block",
      "Content-Length" : "907",
      "Content-Type" : "application/json"
    }
  }
}
2021-01-07 11:44:19.818  INFO 23752 --- [nio-9083-exec-3] HttpLogger                               : curl -X 'GET'
   'http://172.16.3.33:9083/api/admin?current=1&size=10&'
   -H 'host: 172.16.3.33:9083'
   -H 'connection: keep-alive'
   -H 'accept: application/json, text/plain, */*'
   -H 'x-auth-token: 2fd100d9-d46a-4a18-8737-c32421cfb491'
   -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
   -H 'origin: http://172.16.3.33:9410'
   -H 'referer: http://172.16.3.33:9410/'
   -H 'accept-encoding: gzip, deflate'
   -H 'accept-language: zh-CN,zh;q=0.9'