新建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("'"); String url = stringBuilder.toString(); if (url.endsWith("&'")) { url = url.substring(0, url.length() - 2) + "'"; } curlItemList.add(url); 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("&"); } } String paramsStr = stringBuilder.toString(); if (paramsStr.endsWith("&")) { paramsStr = paramsStr.substring(0, paramsStr.length() - 1); } curlItemList.add(MessageFormat.format("--data-raw ''{0}''", paramsStr)); 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, " ")); } } 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'
参考文献: