背景
通常爲了方便定位問題,我們需要記錄接口的入參和出參。但由於 stream 不可重複讀的特性,會導致無法預期的各種問題。
Wrapper
作爲 request、response 的包裝類,我們可以通過重寫 getInputStream 和 getOutputStream 控制數據的流轉,從而達到數據的可重複讀取。
HttpServletRequestWrapper
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;
}
}
HttpServletResponseWrapper
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();
}
}