在Spring MVC
中,我們有時需要記錄一下請求和返回的內容,方便出現問題時排查。比較Header、Request Body等。這些在Controller
也可以記錄,但在Filter
中會更方便。而我們使用的是OncePerRequestFilter
。
可以通過下面的程式碼來讀取請求Body:
byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
log.info("request body = {}", new String(requestBody, StandardCharsets.UTF_8));
但是這裡從流讀取了一次內容後,後續不可再讀了。這就造成了真正處理請求的時候,報錯失敗,我們需要把Request物件改造成可重複讀的類。
為了可以讓流重複讀,加了以下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);
其實,針對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"]]
返回也是一樣,有流不可重複讀的問題,使用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給請求端了。
記錄整個請求的處理時間請參考: Java如何測量方法執行時間
測試一下:
POST http://localhost:8080/hello/pkslow
Content-Type: application/json
Authorization: Basic xxxxxx
{
"id": 999,
"value": "content"
}
執行紀錄檔結果如下:
也可使用ContentCachingRequestWrapper
來解決請求流不可重複讀的問題,但這個Wrapper是有限制的,具體可以看它原始碼。也有人提了Issue。
程式碼請看GitHub: https://github.com/LarryDpk/pkslow-samples