通常为了方便定位问题,我们需要记录接口的入参和出参。但由于 stream 不可重复读的特性,会导致无法预期的各种问题。
作为 request、response 的包装类,我们可以通过重写 getInputStream 和 getOutputStream 控制数据的流转,从而达到数据的可重复读取。
package cn.caojiantao.spider; import org.springframework.util.StreamUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.ByteArrayInputStream; import java.io.IOException; /** * @author caojiantao */ public class RequestWrapper extends HttpServletRequestWrapper { private byte[] data; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); data = StreamUtils.copyToByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener listener) { } @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; } public byte[] toByteArray() throws IOException { return data; } } 复制代码
package cn.caojiantao.spider; 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; /** * @author caojiantao */ public class ResponseWrapper extends HttpServletResponseWrapper { private ByteArrayOutputStream byteArrayOutputStream; private ServletOutputStream servletOutputStream; public ResponseWrapper(HttpServletResponse response) { super(response); byteArrayOutputStream = new ByteArrayOutputStream(); servletOutputStream = new ServletOutputStream() { @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { } @Override public void write(int b) throws IOException { response.getOutputStream().write(b); // 同时写入字节数组 byteArrayOutputStream.write(b); } }; } @Override public ServletOutputStream getOutputStream() throws IOException { return servletOutputStream; } public byte[] toByteArray() { return byteArrayOutputStream.toByteArray(); } } 复制代码
response.getOutputStream() 和 response.getWriter() 互斥,不能同时使用。
package cn.caojiantao.spider.configuration; import cn.caojiantao.spider.RequestWrapper; import cn.caojiantao.spider.ResponseWrapper; import cn.caojiantao.spider.util.LogContext; import cn.caojiantao.spider.util.NetUtils; import lombok.extern.slf4j.Slf4j; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; import java.util.List; /** * @author caojiantao */ @Slf4j @WebFilter(urlPatterns = {"/*"}) public class SpiderFilter implements Filter { private List<String> excludePathList = Arrays.asList("/", "/favicon.ico", "/index.html", "/css/*", "/js/*"); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (excludePathList.contains(request.getRequestURI())) { filterChain.doFilter(request, response); return; } // 追踪日志 LogContext.setTraceId(); // 包装流,可重复读取 RequestWrapper requestWrapper = new RequestWrapper(request); ResponseWrapper responseWrapper = new ResponseWrapper(response); // 请求参数 String traceId = LogContext.getTraceId(); String method = request.getMethod(); String uri = request.getRequestURI(); String data = new String(requestWrapper.toByteArray()); String query = request.getQueryString(); String ip = NetUtils.getIpAddress(request); log.info("request traceId:{} method:{} uri:{} data:{} query:{} ip:{}", traceId, method, uri, data, query, ip); long t = System.currentTimeMillis(); filterChain.doFilter(requestWrapper, responseWrapper); // 响应参数 String resp = new String(responseWrapper.toByteArray()); long cost = System.currentTimeMillis() - t; log.info("response traceId:{} method:{} uri:{} data:{} query:{} ip:{} response:{} cost:{}", traceId, method, uri, data, query, ip, resp, cost); LogContext.clear(); } } 复制代码
这里 LogContext 为日志跟踪 traceId 管理,通过 ThreadLocal 来实现,方便问题定位。
package cn.caojiantao.spider.util; import java.util.UUID; /** * @author caojiantao */ public class LogContext { private static ThreadLocal<String> traceIdLocal = new ThreadLocal<>(); public static void setTraceId() { String traceId = UUID.randomUUID().toString().replaceAll("-", ""); setTraceId(traceId); } public static void setTraceId(String traceId) { traceIdLocal.set(traceId); } public static String getTraceId() { return traceIdLocal.get(); } public static void clear() { traceIdLocal.remove(); } } 复制代码