title: Request Response 重复读取
date: 2020-03-16 22:43:21
tags:
- http
categories: - 编程
背景
通常为了方便定位问题,我们需要记录接口的入参和出参。但由于 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();
}
}