我們可以自己定義一個類CustomHttpRequestWrapper?,繼承自HttpServletRequestWrapper?,定義一個成員變量bodyInStringFormat?,存儲body中獲取到的數據,其實字符串底層是字節數組,然后重寫getInputStream?方法,構造一個ByteArrayInputStream?輸入流,而ByteArrayInputStream?實現了ma
前言
最近收到一個需求,出于審計的目的,希望可以通過日志記錄下對應用程序發起的post、put請求的body內容,面對這樣的一個需求,大家是不是覺得很簡單,但是我在開發過程中還是遇到了問題,在本文中做一個分享。
輸入流只能讀取一次
既然要記錄所有的請求,我們可以創建一個過濾器LogRequestFilter, 統一攔截所有的請求,讀取里面的輸入流InputStream,我想大家都能想到把,具體代碼如下:
@Component
public class LogRequestFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(LogRequestFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
// 記錄post和put請求體內容
logPostOrPutRequestBody((HttpServletRequest) servletRequest);
filterChain.doFilter(servletRequest, servletResponse);
}
private void logPostOrPutRequestBody(HttpServletRequest httpRequest) throws IOException {
if(Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
String characterEncoding = httpRequest.getCharacterEncoding();
Charset charset = Charset.forName(characterEncoding);
// 讀取輸入流轉為字符串
String bodyInStringFormat = readInputStreamInStringFormat(httpRequest.getInputStream(), charset);
logger.info("Request body: {}", bodyInStringFormat);
}
}
private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
final int MAX_BODY_SIZE = 1024;
final StringBuilder bodyStringBuilder = new StringBuilder();
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(MAX_BODY_SIZE + 1);
final byte[] entity = new byte[MAX_BODY_SIZE + 1];
// 讀取流
final int bytesRead = stream.read(entity);
if (bytesRead != -1) {
bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
if (bytesRead > MAX_BODY_SIZE) {
bodyStringBuilder.append("...");
}
}
stream.reset();
return bodyStringBuilder.toString();
}
}
但是事情往往不是按照你預期的方向發展的, 但你按照上面的設計寫好代碼后,發一個post請求,卻返回下面的報錯:
DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
Required request body is missing
為什么會報錯呢?
原因就是輸入流只能讀取一次。 當我們調用getInputStream()方法獲取輸入流時得到的是一個InputStream對象,而實際類型是ServletInputStream,它繼承于InputStream。
InputStream的read()方法內部有一個postion,標志當前流被讀取到的位置,每讀取一次,該標志就會移動一次,如果讀到最后,read()會返回-1,表示已經讀取完了。如果想要重新讀取則需要調用reset()方法,position就會移動到上次調用mark的位置,mark默認是0,所以就能從頭再讀了。調用reset()方法的前提是已經重寫了reset()方法,當然能否reset也是有條件的,它取決于markSupported()方法是否返回true。
InputStream默認不實現reset(),并且markSupported()默認也是返回false,這一點查看InputStream源碼便知:

我們再來看看ServletInputStream,可以看到該類沒有重寫mark(),reset()以及markSupported()方法:

所以InputStream默認不實現reset的相關方法,而ServletInputStream也沒有重寫reset的相關方法,這樣就無法重復讀取流,這就是我們從request對象中獲取的輸入流就只能讀取一次的原因,最后導致再次讀取流的時候報錯。
那該如何解決呢?
改寫ServeltRequest
既然ServletInputStream不支持重新讀寫,那么為什么不把流讀出來后用容器存儲起來,后面就可以多次利用了。那么問題就來了,要如何存儲這個流呢?
所幸JavaEE提供了一個 HttpServletRequestWrapper類,從類名也可以知道它是一個http請求包裝器,其基于裝飾者模式實現了HttpServletRequest界面,部分源碼如下:

從上圖中的部分源碼可以看到,該類并沒有真正去實現HttpServletRequest的方法,而只是在方法內又去調用HttpServletRequest的方法,所以我們可以通過繼承該類并實現想要重新定義的方法以達到包裝原生HttpServletRequest對象的目的。
我們可以自己定義一個類CustomHttpRequestWrapper,繼承自HttpServletRequestWrapper,定義一個成員變量bodyInStringFormat,存儲body中獲取到的數據,其實字符串底層是字節數組,然后重寫getInputStream方法,構造一個ByteArrayInputStream輸入流,而ByteArrayInputStream實現了mark(),reset()以及markSupported()方法,然后讓ByteArrayInputStream去讀取前面保存的字符串bodyInStringFormat中的數組,從而達到重復使用的目的。
package com.filters;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = LoggerFactory.getLogger(CustomHttpRequestWrapper.class);
private final String bodyInStringFormat;
public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
logger.info("Body: {}", bodyInStringFormat);
}
private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
final int MAX_BODY_SIZE = 1024;
final StringBuilder bodyStringBuilder = new StringBuilder();
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(MAX_BODY_SIZE + 1);
final byte[] entity = new byte[MAX_BODY_SIZE + 1];
final int bytesRead = stream.read(entity);
if (bytesRead != -1) {
bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
if (bytesRead > MAX_BODY_SIZE) {
bodyStringBuilder.append("...");
}
}
stream.reset();
return bodyStringBuilder.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());
return new ServletInputStream() {
private boolean finished = false;
@Override
public boolean isFinished() {
return finished;
}
@Override
public int available() throws IOException {
return byteArrayInputStream.available();
}
@Override
public void close() throws IOException {
super.close();
byteArrayInputStream.close();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
public int read () throws IOException {
int data = byteArrayInputStream.read();
if (data == -1) {
finished = true;
}
return data;
}
};
}
}
編寫玩上面的代碼以后,還需要再過濾器中使用,那么后續過濾器中的ServletRequest實現類都是CustomHttpRequestWrapper , 就可以再次讀取body的內容了,具體代碼如下:
@Component
public class LogRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
if(Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
// 設置自定義的ServletRequest
CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
filterChain.doFilter(requestWrapper, servletResponse);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
這一下你再次向應用程序發出POST或GET請求時,就不會看到任何報錯了。