前言
最近收到一個(gè)需求,出于審計(jì)的目的,希望可以通過日志記錄下對(duì)應(yīng)用程序發(fā)起的post、put請(qǐng)求的body內(nèi)容,面對(duì)這樣的一個(gè)需求,大家是不是覺得很簡(jiǎn)單,但是我在開發(fā)過程中還是遇到了問題,在本文中做一個(gè)分享。
輸入流只能讀取一次
既然要記錄所有的請(qǐng)求,我們可以創(chuàng)建一個(gè)過濾器LogRequestFilter, 統(tǒng)一攔截所有的請(qǐng)求,讀取里面的輸入流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請(qǐng)求體內(nèi)容
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);
// 讀取輸入流轉(zhuǎn)為字符串
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();
}
}
但是事情往往不是按照你預(yù)期的方向發(fā)展的, 但你按照上面的設(shè)計(jì)寫好代碼后,發(fā)一個(gè)post請(qǐng)求,卻返回下面的報(bào)錯(cuò):
DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
Required request body is missing
為什么會(huì)報(bào)錯(cuò)呢?
原因就是輸入流只能讀取一次。 當(dāng)我們調(diào)用getInputStream()方法獲取輸入流時(shí)得到的是一個(gè)InputStream對(duì)象,而實(shí)際類型是ServletInputStream,它繼承于InputStream。
InputStream的read()方法內(nèi)部有一個(gè)postion,標(biāo)志當(dāng)前流被讀取到的位置,每讀取一次,該標(biāo)志就會(huì)移動(dòng)一次,如果讀到最后,read()會(huì)返回-1,表示已經(jīng)讀取完了。如果想要重新讀取則需要調(diào)用reset()方法,position就會(huì)移動(dòng)到上次調(diào)用mark的位置,mark默認(rèn)是0,所以就能從頭再讀了。調(diào)用reset()方法的前提是已經(jīng)重寫了reset()方法,當(dāng)然能否reset也是有條件的,它取決于markSupported()方法是否返回true。
InputStream默認(rèn)不實(shí)現(xiàn)reset(),并且markSupported()默認(rèn)也是返回false,這一點(diǎn)查看InputStream源碼便知:

我們?cè)賮砜纯碨ervletInputStream,可以看到該類沒有重寫mark(),reset()以及markSupported()方法:

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

從上圖中的部分源碼可以看到,該類并沒有真正去實(shí)現(xiàn)HttpServletRequest的方法,而只是在方法內(nèi)又去調(diào)用HttpServletRequest的方法,所以我們可以通過繼承該類并實(shí)現(xiàn)想要重新定義的方法以達(dá)到包裝原生HttpServletRequest對(duì)象的目的。
我們可以自己定義一個(gè)類CustomHttpRequestWrapper,繼承自HttpServletRequestWrapper,定義一個(gè)成員變量bodyInStringFormat,存儲(chǔ)body中獲取到的數(shù)據(jù),其實(shí)字符串底層是字節(jié)數(shù)組,然后重寫getInputStream方法,構(gòu)造一個(gè)ByteArrayInputStream輸入流,而ByteArrayInputStream實(shí)現(xiàn)了mark(),reset()以及markSupported()方法,然后讓ByteArrayInputStream去讀取前面保存的字符串bodyInStringFormat中的數(shù)組,從而達(dá)到重復(fù)使用的目的。
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;
}
};
}
}
編寫玩上面的代碼以后,還需要再過濾器中使用,那么后續(xù)過濾器中的ServletRequest實(shí)現(xiàn)類都是CustomHttpRequestWrapper , 就可以再次讀取body的內(nèi)容了,具體代碼如下:
@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())) {
// 設(shè)置自定義的ServletRequest
CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
filterChain.doFilter(requestWrapper, servletResponse);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
這一下你再次向應(yīng)用程序發(fā)出POST或GET請(qǐng)求時(shí),就不會(huì)看到任何報(bào)錯(cuò)了。