在Spring MVC或Spring Boot中使用Filter打印請求參數問題

 使用Spring MVC或Spring Boot中打印或記錄日誌一般使用AOP記錄Request請求和Response響應參數,在不使用AOP的前提下,如果在Filter中打印日誌,在打印或消費請求類型爲Content-Type:application/json的請求時,會出現嚴重的問題。

在Spring體系中,過濾器的定義我們一般採用繼承OncePerRequestFilter的方式,當然也可以使用原始的Filter。

錯誤寫法一:

如果不對request和response進行處理,使用僞代碼採用如下寫法打印請求和響應參數(注:此時request請求類型爲Post,接收的是Json數據)

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse             
                  response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, response);
        printRequestLog(request);
        printResonseLog(response);
    }

       運行測試後你會發現拋出如下異常: 

java.io.IOException: Stream closed
	at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:359) ~[tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) ~[tomcat-embed-core-9.0.31.jar:9.0.31]
	at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284) ~[na:1.8.0_191]
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326) ~[na:1.8.0_191]
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) ~[na:1.8.0_191]
	at java.io.InputStreamReader.read(InputStreamReader.java:184) ~[na:1.8.0_191]
	at java.io.BufferedReader.fill(BufferedReader.java:161) ~[na:1.8.0_191]
	at java.io.BufferedReader.readLine(BufferedReader.java:324) ~[na:1.8.0_191]
	at java.io.BufferedReader.readLine(BufferedReader.java:389) ~[na:1.8.0_191]
	at com.micro.backend.filter.LoggingFilter.getBodyString(LoggingFilter.java:60) [classes/:na]
	at com.micro.backend.filter.LoggingFilter.doFilterInternal(LoggingFilter.java:49) [classes/:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:94) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.1.14.RELEASE.jar:5.1.14.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:367) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1639) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_191]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_191]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.31.jar:9.0.31]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_191]

錯誤寫法二:

如果不對request和response進行處理,使用僞代碼採用如下寫法打印請求和響應參數(注:此時request請求類型爲Post,接收的是Json數據)

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 
              response, FilterChain filterChain) throws ServletException, IOException {
        printRequestLog(request);
        printResonseLog(response);
        filterChain.doFilter(request, response);
    }

       運行測試後你會發現拋出如下異常:  

org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing

       遇到這樣的問題你是不是有坐立不安、心煩意亂、百爪撓心的痛楚,不要着急,下面我給出一個解決方案。

       首先我們使用裝飾器模式,創建request和response兩個包裝類,如下:

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
/**
* @Description:    請求包裝器
* @Author:         liuliya
* @CreateDate:     2020/4/29 10:00
*/
public class RequestWrapper extends HttpServletRequestWrapper {
    
    private final byte[] body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = getRequestBodyString(request).getBytes(Charset.defaultCharset());
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }
        };
    }

    public String getRequestBodyString(ServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }

}
package com.micro.backend.filter.support;

import org.apache.commons.io.output.TeeOutputStream;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @Description: 響應包裝器
 * @Author: liuliya
 * @CreateDate: 2020/4/29 10:00
 */
public class ResponseWrapper extends HttpServletResponseWrapper {

    private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    private PrintWriter writer = new PrintWriter(bos);

    public ResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletResponse getResponse() {
        return this;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStream() {
            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener listener) {

            }

            private TeeOutputStream tee = new TeeOutputStream(ResponseWrapper.super.getOutputStream(), bos);

            @Override
            public void write(int b) throws IOException {
                tee.write(b);
            }
        };
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new TeePrintWriter(super.getWriter(), writer);
    }

    public byte[] toByteArray() {
        return bos.toByteArray();
    }

}

package com.micro.backend.filter.support;

import java.io.PrintWriter;
//PrintWriter是一種寫入字符的一種操作類,可以寫入字符,TeePrintWriter繼承了他,主要功能是把原始的字符流copy到branch裏面。
public class TeePrintWriter extends PrintWriter {

    PrintWriter branch;

    public TeePrintWriter(PrintWriter main, PrintWriter branch) {
        super(main, true);
        this.branch = branch;
    }

    public void write(char buf[], int off, int len) {
        super.write(buf, off, len);
        super.flush();
        branch.write(buf, off, len);
        branch.flush();
    }

    public void write(String s, int off, int len) {
        super.write(s, off, len);
        super.flush();
        branch.write(s, off, len);
        branch.flush();
    }

    public void write(int c) {
        super.write(c);
        super.flush();
        branch.write(c);
        branch.flush();
    }

    public void flush() {
        super.flush();
        branch.flush();
    }
}

接下來創建最重要的LoggingFilter類,繼承OncePerRequestFilter,或者直接繼承Servlet中原始的Filter。

package com.micro.backend.filter;

import com.micro.backend.filter.support.RequestWrapper;
import com.micro.backend.filter.support.ResponseWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
* @Author:         liuliya
* @CreateDate:     2020/4/28 23:30
*/
@Slf4j
@Configuration
public class LoggingFilter extends OncePerRequestFilter {
   
    private static final String REQUEST_PREFIX_NAME = "Request請求: ";
    private static final String RESPONSE_PREFIX_NAME = "Response請求: ";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request = new RequestWrapper(request);
        response = new ResponseWrapper(response);
        filterChain.doFilter(request, response);
        printRequestLog(request);
        printResponseLog((ResponseWrapper) response);
    }

    private void printRequestLog(final HttpServletRequest request) {
        StringBuilder msg = new StringBuilder();
        msg.append(REQUEST_PREFIX_NAME);
        HttpSession session = request.getSession(false);
        if (session != null) {
            msg.append("sessionId = ").append(session.getId()).append("; ");
        }
        if (request.getMethod() != null) {
            msg.append("method = ").append(request.getMethod()).append("; ");
        }
        if (request.getContentType() != null) {
            msg.append("contentType = ").append(request.getContentType()).append("; ");
        }
        msg.append("uri = ").append(request.getRequestURI());
        if (request.getQueryString() != null) {
            msg.append('?').append(request.getQueryString());
        }

        if (request instanceof RequestWrapper && !isMultipart(request) && !isBinaryContent(request)) {
            RequestWrapper requestWrapper = (RequestWrapper) request;
            msg.append("; payload = ").append(requestWrapper.getRequestBodyString(request));
        }
        log.info(msg.toString());
    }

    private boolean isBinaryContent(final HttpServletRequest request) {
        if (request.getContentType() == null) {
            return false;
        }
        return request.getContentType().startsWith("image")
                || request.getContentType().startsWith("video")
                || request.getContentType().startsWith("audio");
    }

    private boolean isMultipart(final HttpServletRequest request) {
        return request.getContentType() != null
                && request.getContentType().startsWith("multipart/form-data");
    }

    private void printResponseLog(final ResponseWrapper response) {
        StringBuilder msg = new StringBuilder();
        msg.append(RESPONSE_PREFIX_NAME);
        try {
            msg.append("; payload = ")
                    .append(new String(response.toByteArray(), response.getCharacterEncoding()));
        } catch (UnsupportedEncodingException e) {
            log.warn("Failed to parse response payload", e);
        }
        log.info(msg.toString());
    }

}

參考以上我整理出的代碼,你就會發現奇蹟!!!

爲什麼要這麼寫呢,其本質是把請求流拷貝了一份,一個供filterChain向下傳遞,一個來做流的消費,再有一個就是運用裝飾器模式的精髓所在。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章