Spring在Filter中記錄Web請求Request和返回Response的內容及時長

2023-02-06 21:00:20

1 簡介

Spring MVC中,我們有時需要記錄一下請求和返回的內容,方便出現問題時排查。比較Header、Request Body等。這些在Controller也可以記錄,但在Filter中會更方便。而我們使用的是OncePerRequestFilter

2 記錄請求

2.1 流重複讀的問題

可以通過下面的程式碼來讀取請求Body:

byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
log.info("request body = {}", new String(requestBody, StandardCharsets.UTF_8));

但是這裡從流讀取了一次內容後,後續不可再讀了。這就造成了真正處理請求的時候,報錯失敗,我們需要把Request物件改造成可重複讀的類。

2.2 通過Wrapper解決流重複讀的問題

為了可以讓流重複讀,加了以下Wrapper:

public class PkslowRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;
    public PkslowRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }

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

這裡主要在構造時讀了流,然後存在變數body裡,每次返回流的時候從body構造回去即可。

在Filter中使用這個Wrapper如下:

PkslowRequestWrapper request = new PkslowRequestWrapper(req);
ServletInputStream servletInputStream = request.getInputStream();
String body = StreamUtils.copyToString(servletInputStream, Charset.defaultCharset());
log.info("Request Body(PkslowRequestWrapper): {}", body);

2.3 內建Filter

其實,針對Request,Spring Boot提供了內建的Filter可以直接記錄請求,使用如下:

package com.pkslow.springboot.common.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

@Configuration
public class PkslowConfig {
    @Bean
    public CommonsRequestLoggingFilter loggingFilter() {
        CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
        filter.setIncludeHeaders(true);
        filter.setIncludeClientInfo(true);
        filter.setIncludePayload(true);
        filter.setIncludeQueryString(true);

        filter.setAfterMessagePrefix("CommonsRequestLoggingFilter Request: ");

        return filter;
    }
}

但要開debug級別的紀錄檔才會打出來。

logging:
  level:
    root: debug

紀錄檔如下:

DEBUG 20356 --- [nio-8080-exec-1] o.s.w.f.CommonsRequestLoggingFilter      : Before request [POST /hello/pkslow, client=127.0.0.1, headers=[authorization:"Basic xxxxxx", content-length:"37", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/17.0.5)", accept-encoding:"gzip,deflate", Content-Type:"application/json;charset=UTF-8"]]

3 記錄返回

返回也是一樣,有流不可重複讀的問題,使用Spring自帶的ContentCachingResponseWrapper即可。

ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(res);
log.info("Response Code: {}", response.getStatus());
String responseBody = new String(response.getContentAsByteArray(), response.getCharacterEncoding());
log.info("Response Body: {}", responseBody);
response.copyBodyToResponse();

特別注意一定要呼叫copyBodyToResponse()這個方法,不然無法返回body給請求端了。

4 記錄時間

記錄整個請求的處理時間請參考: Java如何測量方法執行時間

5 測試

測試一下:

POST http://localhost:8080/hello/pkslow
Content-Type: application/json
Authorization: Basic xxxxxx

{
  "id": 999,
  "value": "content"
}

執行紀錄檔結果如下:

6 總結

也可使用ContentCachingRequestWrapper來解決請求流不可重複讀的問題,但這個Wrapper是有限制的,具體可以看它原始碼。也有人提了Issue

程式碼請看GitHub: https://github.com/LarryDpk/pkslow-samples