在一个过滤器中打印一次完整的请求响应,解决RequestBody和ResponseBody只能获取一次的问题

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.实现

  1. 首先我们新建两个请求和响应类,这两个类直接继承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){

        }
    }
}

  1. 准备好这两个类后我们就可以,准备过滤器中的内容了
	@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 "";
    }

版权声明:本文为Special_shooting原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。