簡介
- HTTP 請求 RequestBody 只能被讀取一次:
HttpServletRequest
的輸入流 (InputStream
) 在被讀取后會被關閉,導致后續無法再次讀取。 - 本文將介紹如何通過 請求包裝類 (RequestWrapper) 來解決這個問題。
問題背景
當我們需要在以下場景中多次讀取 RequestBody
時,就會遇到這個問題:
- 日志記錄:需要在攔截器或過濾器中記錄請求體
- 參數校驗:需要在多個地方校驗請求體數據
- 數據解析:需要在不同的組件中解析請求體(如 Spring 的
@RequestBody
和手動解析)
直接嘗試多次讀取 InputStream
會導致異常:
// 第一次讀取(可以成功)
String body1 = IOUtils.toString(request.getInputStream(), "UTF-8");// 第二次讀取
// 場景一:(拋出異常:Stream closed)
String body2 = IOUtils.toString(request.getInputStream(), "UTF-8");
// 場景二:Controller層的@RequestBody直接報錯 org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: ...
解決方案:緩存請求體
我們可以通過自定義 HttpServletRequestWrapper
來緩存請求體,使得它可以被多次讀取。
1. 實現 RequestWrapper
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;public class RequestWrapper extends HttpServletRequestWrapper {//參數字節數組@Getterprivate byte[] requestBody;//Http請求對象private final HttpServletRequest request;public RequestWrapper(HttpServletRequest request) throws IOException {super(request);this.request = request;}@Overridepublic ServletInputStream getInputStream() throws IOException {/*每次調用此方法時將數據流中的數據讀取出來,然后再回填到InputStream之中解決通過@RequestBody和@RequestParam(POST方式)讀取一次后控制器拿不到參數問題*/if (null == this.requestBody) {ByteArrayOutputStream baos = new ByteArrayOutputStream();IOUtils.copy(request.getInputStream(), baos);this.requestBody = baos.toByteArray();}final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() {return bais.read();}};}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(this.getInputStream()));}
}
2. 使用 RequestWrapper
在 Filter 中包裝原始請求:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高優先級
public class RequestCachingFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {// 包裝原始請求RequestWrapper wrappedRequest = new RequestWrapper((HttpServletRequest) request);// 注意這里傳的是request的裝飾類 RequestWrapper chain.doFilter(wrappedRequest, response);}
}
方案優勢
- 性能優化:只在構造時讀取一次請求體,后續從緩存讀取
- 線程安全:每個請求有自己的包裝實例,互不干擾
- 兼容性:完全兼容
HttpServletRequest
的所有方法 - 靈活性:可以隨時獲取原始請求體數據
注意事項
- 大文件處理:對于大文件上傳,緩存整個請求體可能消耗較多內存
- 流式處理:如果確實需要流式處理大數據,不宜使用此方案
- Filter 順序:確保此 Filter 在其他需要讀取請求體的 Filter 之前執行