【實踐】切面打印請求參數

加打印語句,將請求參數打印出來。後面想一想,之後可能還會遇到這樣的狀況,若是每次遇到,我都去對應的方法中加日誌打印,就變成重複工做。而且日誌打印跟咱們的業務自己沒有任何關係。json

記錄日誌網上主要有三種方法:數組

  1. aop
  2. filter
  3. interceptor

我選擇了filter。爲何選擇它,由於我以爲它相對於定義切點,而後切點先後處理來講,更加方便;相對於 interceptor, 我更加熟悉這種方式。緩存

 

定義Filter

定義一個 LogFilter  。 裏面對 HttpServletRequest  進行攔截,根據對應的 content-type 解析請求參數。主要代碼以下app

/**
 * 功能描述: 打印請求參數
 * @author lkb
 * @date 2020/5/6
 * @param
 * @return
 */
@Slf4j
public class LogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        //日誌
        doLog(request, response);

        // 將request 傳到下一個Filter
        filterChain.doFilter(request, response);
    }


    private void doLog(HttpServletRequest request,  HttpServletResponse response){
        // 輸出請求體
        log.info("request. uri = {}, method = {}, requestParam = {}", request.getRequestURI(), request.getMethod(), getRequestParam(request));
        //todo 返回結果也能夠進行處理
    }


    private String getRequestParam(HttpServletRequest request){
        String requestParam = "";

        String requestContentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
        try {
            if(StringUtils.isNotEmpty(requestContentType)){
                if (requestContentType.startsWith(MediaType.APPLICATION_JSON_VALUE)
                        || requestContentType.startsWith(MediaType.APPLICATION_XML_VALUE)) {
                    // xml json
                    requestParam = getRequestBody(request);
                }
                else if (requestContentType.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) {
                    // 文件表單提交
                    requestParam = getFormParam(request);
                }else if(requestContentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE)){
                    // 普通表單提交
                    requestParam = toJson(request.getParameterMap());
                }
            }else{
                // 默認普通表單提交
                requestParam = toJson(request.getParameterMap());
            }
        }catch (Exception e){
            log.error("getRequestParam error");
            log.error(e.getMessage(),e);
        }
        return requestParam;
    }
    
    ...
}

 

而後,註冊這個filteride

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        final FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        final LogFilter logFilter = new LogFilter();
        filterRegistrationBean.setFilter(logFilter);
        return filterRegistrationBean;
    }

}

 

上面兩步以後,重啓項目,能夠看到請求過來後會打印出請求的uri、method、param 。this

 

InputStream 只能讀取一次

原本覺得這樣就萬事大吉了。可是事實並非如此。加上上面代碼後會發現,再controller 加上的 @requestBody 沒有效果,取不到任何數據,並拋出異常,告訴咱們請求已經被讀取過。 爲何呢?spa

 

緣由很簡單。由於在 doLog 中獲取請求參數的時候,咱們已經將請求的 inputStream 給讀取了。讀取inputStream 時有一個offset,它表示你從哪裏開始讀取輸入流。由於咱們讀取了一遍 inputStream,因此offset已經在流的最末端了。咱們再去讀取,就會發現沒有東西能夠讀了。若是想重複讀取 inputStream 就須要每次讀取後重置 offset 的值。日誌

 

固然爲了方便,我並無去從新inputStream 中的reset 方法。而是選擇,在讀取請求後,將請求緩存起來。code

 

首先,BufferedServletInputStream 繼承自 ServletInputStream。orm

public class BufferedServletInputStream extends ServletInputStream {

    private ByteArrayInputStream inputStream;

    public BufferedServletInputStream(byte[] buffer) {
        this.inputStream = new ByteArrayInputStream( buffer );
    }

    @Override
    public int available(){
        return inputStream.available();
    }

    @Override
    public int read(){
        return inputStream.read();
    }

    @Override
    public int readLine(byte[] b, int off, int len){
        return inputStream.read( b, off, len );
    }

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

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

    @Override
    public void setReadListener(ReadListener readListener) {

    }
}

 

而後,BufferedServletRequestWrapper 繼承 HttpServletRequestWrapper。

@Slf4j
public class BufferedServletRequestWrapper extends HttpServletRequestWrapper {

    private byte[] buffer;

    public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream is = request.getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte buff[] = new byte[1024];
        int read;
        while ((read = is.read(buff)) > 0) {
            baos.write(buff, 0, read);
        }
        this.buffer = baos.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() {
        return new BufferedServletInputStream(this.buffer);
    }
}

裏面使用一個 byte[] buffer 數組將請求緩存起來。

 

最後,在 LogFilter 中 doLog 前,對請求進行包裝。

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
    // 備份HttpServletRequest
    request = new BufferedServletRequestWrapper(request);

    //日誌
    doLog(request, response);

    // 將request 傳到下一個Filter
    filterChain.doFilter(request, response);
}

通過上訴處理,咱們就能夠愉快地用日誌記錄請求參數了。

 

總結

最後總結一下:

1. 記錄請求參很多天志的方式最好採用切面的思想

2. inputStream 默認只能讀取一次,屢次讀取要從新處理inputStream

3. @requestBody 的原理能夠了解一下

 

 

相關文章
相關標籤/搜索