1.需求描述
首先说一下我的需求,在前后端分离的项目中,由于后台不需要渲染页面,只返回Json串,所以可以在一个过滤器中将一次完整的请求和响应都打印在一条log日志里面,并且使用json的格式,这样在之后查找日志时可以通过请求的Id很方便的在一条日志中看到完整的请求和响应
2.问题
这里主要涉及两个问题,就是如果想在过滤器中就获取完整的请求和响应,就必须获取RequestBoby和ResponseBody,但是由于这两个是流的形式,只能够获取一次,这就相当于如果在过滤器中读取这两个流,那么原来应该给后续业务代码获取请求体就获取不到了,在Controller就获取不到前端的参数了,原来应该响应给页面的响应体的内容也由于在过滤器中被读取,页面只能拿到空的响应,所以解决请求体和响应体只能获取一次的问题,就是关键.
3.思路
关于多次获取请求体和响应体的问题网上有很多讨论,大体都差不多,这里采用将流重新写入的方式解决,简单来说就是:
1.既然请求体只能获取一次,那么我获取之后将这个请求体在写回去就好了.
2.我们从在过滤器方法中原生的ServletRequest 这个对象中获取RequestBody的流,将这个流转为字符串保存并使用打印日志
3.再新建一个ServletRequest ,将保存的RequestBody在转为流重新写回新建的ServletRequest 对象,然后在chain.doFilter()这个方法中,传入我们新建的重写写回流的ServletRequest 对象,这样后面的业务就可以继续获取请求体中内容.
对于响应体而言:
1.我们仍是将流复制到一个新建响应对象中
2.由这个新建的对象chain.doFilter()去执行和获取响应的内容
3.当他执行完业务代码在回到过滤器中的时候,获取这个新建对象的响应体并保存,打印完日志
4.再使用原生的那个ServletResponse对象,用他的通道手动的将数据写回页面.
4.实现
- 首先我们新建两个请求和响应类,这两个类直接继承HttpServletRequestWrapper和HttpServletResponseWrapper,然后在里面封装一些需要的方法,下面的代码直接复制就好
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* <http request封装获取请求body里的参数,获取header,获取paramMap>
*/
public class RequestWrapper extends HttpServletRequestWrapper {
/**
* 请求body
*/
private String body;
HttpServletRequest req = null;
/**
* 请求header
*/
private Map<String, String> headerMap = new HashMap<>();
/**
* 请求paramMap
*/
private Map<String, String> parameterMap = new HashMap<>();
/**
* Constructs a request object wrapping the given request.
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request)
throws IOException {
super(request);
}
public RequestWrapper(HttpServletRequest request, String requestBody) {
super(request);
this.body = requestBody;
this.req = request;
Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) {
String headerKey = headers.nextElement();
headerMap.put(headerKey, request.getHeader(headerKey));
}
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
parameterMap.put(name, request.getParameter(name));
}
}
@Override
public ServletInputStream getInputStream()
throws IOException {
return new ServletInputStream() {
private InputStream in = new ByteArrayInputStream(body.getBytes(req.getCharacterEncoding()));
@Override
public int read()
throws IOException {
return in.read();
}
@Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
// TODO Auto-generated method stub
}
};
}
@Override
public BufferedReader getReader()
throws IOException {
return new BufferedReader(new StringReader(body));
}
public String getBody() {
// 请求中的数据
return this.body;
}
public Map<String, String> getHeaderMap() {
return this.headerMap;
}
public Map<String, String> getParameterMaps() {
return this.parameterMap;
}
}
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
/**
* <http response封装,获取响应报文>
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = null;
private ServletOutputStream out = null;
private PrintWriter writer = null;
public ResponseWrapper(HttpServletResponse resp) throws IOException {
super(resp);
/**
* 替换默认的输出端,作为response输出数据的存储空间(即真正存储数据的流)
*/
buffer = new ByteArrayOutputStream();
/**
* response输出数据时是调用getOutputStream()和getWriter()方法获取输出流,再将数据输出到输出流对应的输出端的。
* 此处指定getOutputStream()和getWriter()返回的输出流的输出端为buffer,即将数据保存到buffer中。
*/
out = new WapperedOutputStream(buffer);
writer = new PrintWriter(new OutputStreamWriter(buffer, this.getCharacterEncoding()));
}
//重载父类获取outputstream的方法
@Override
public ServletOutputStream getOutputStream()
throws IOException {
return out;
}
//重载父类获取writer的方法
@Override
public PrintWriter getWriter()
throws UnsupportedEncodingException {
return writer;
}
/**
* 这是将数据输出的最后步骤
* @throws IOException
*/
@Override
public void flushBuffer()
throws IOException {
if (out != null) {
out.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void reset() {
buffer.reset();
}
public byte[] getResponseData()
throws IOException {
flushBuffer();//将out、writer中的数据强制输出到WapperedResponse的buffer里面,否则取不到数据
return buffer.toByteArray();
}
//内部类,对ServletOutputStream进行包装,指定输出流的输出端
private class WapperedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = null;
public WapperedOutputStream(ByteArrayOutputStream stream)
throws IOException {
bos = stream;
}
//将指定字节写入输出流bos
@Override
public void write(int b)
throws IOException {
bos.write(b);
}
@Override
public boolean isReady(){
return false;
}
@Override
public void setWriteListener(WriteListener writeListener){
}
}
}
- 准备好这两个类后我们就可以,准备过滤器中的内容了
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
// 请求url,这是打印日志所需,可以不获取
String url = request.getRequestURI();
// 获取请求报文,注意这里就已经将请求体内容读取了,保存在requestBody 这个变量中
String requestBody = this.getRequestBody(request);
// 注意这里新建自定义的RequestWrapper对象并将获取的requestBody传入,就是将流重新写入,具体代码在RequestWrapper构造方法中
RequestWrapper requestWrapper = new RequestWrapper(request, requestBody);
// 获取header,这里都是通过自定义的方法获取日志信息,可以不获取
Map<String, String> headerMap = requestWrapper.getHeaderMap();
// 获取paramMap,这里都是通过自定义的方法获取日志信息,可以不获取
Map<String, String> parameterMap = requestWrapper.getParameterMaps();
//响应处理 包装响应对象并缓存响应数据,这里相当于直接将原生的servletResponse复制了一份
ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse)servletResponse);
long start = System.currentTimeMillis();
// 执行后续业务代码,注意是将我们自定义的两个请求和响应对象传进去,请求体的内容已经被重新写入
filterChain.doFilter(requestWrapper, responseWrapper);
long end = System.currentTimeMillis();
// 注意这里是从自定义的响应对象中取响应体
String responseBody = new String(responseWrapper.getResponseData(), "UTF-8");
// 打印完整的请求响应日志
logger.warn("{}|{}|{}|{}|{}|{}", url, headerMap, parameterMap, requestBody, responseBody, end-start);
// 注意由于响应体内容已经被我们获取,页面这时候是拿不到响应数据的,要手动写回去
servletResponse.setContentLength(-1);
// 需要注意的是,这里是从原生的方法参数中定义的ServletResponse 对象中获取输出流,从自定义的响应对象responseWrapper获取的输出流是写不出东西的!!!
ServletOutputStream output = servletResponse.getOutputStream();
// 写出响应体
output.write(responseBody.getBytes());
output.flush();
}
/**
* 获取requestBody的参数
* @param req
* @return
*/
private String getRequestBody(HttpServletRequest req) {
try {
BufferedReader reader = req.getReader();
StringBuffer sb = new StringBuffer();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String json = sb.toString();
return json;
}
catch (IOException e) {
logger.error("验签时请求体读取失败", e);
}
return "";
}