問題描述
在基于Spring開發(fā)Java項目時,可能需要重復讀取HTTP請求體中的數(shù)據(jù),例如使用攔截器打印入?yún)⑿畔⒌?,但當我們重復調用getInputStream()或者getReader()時,通常會遇到類似以下的錯誤信息:

大體的意思是當前request的getInputStream()已經(jīng)被調用過了。那為什么會出現(xiàn)這個問題呢?
原因分析
主要原因有兩個,一是Java自身的設計中,InputStream作為數(shù)據(jù)管道本身只支持讀取一次,如果要支持重復讀取的話就需要重新初始化;二是Servlet容器中Request的實現(xiàn)問題,我們以默認的Tomcat為例,可以發(fā)現(xiàn)在Request有兩個boolean類型的屬性,分別是usingReader和usingInputStream,當調用getInputStream()或getReader()時會分別檢查兩個屬性的值,并在執(zhí)行后將對應的屬性設置為true,如果在檢查時變量的值已經(jīng)為true了,那么就會報出以上錯誤信息。

解決方案
不太可行的方案:簡單粗暴的反射機制
涉及到變量的修改,我們首先想到的就是有沒有提供方法進行修改,不過可惜的是usingReader和usingInputStream并未提供,所以想要在使用過程中修改這兩個屬性估計只能靠反射了,在使用過程中每次調用后通過反射將usingReader和usingInputStream設置為false,每次根據(jù)讀取出的內容把數(shù)據(jù)流初始化回去,理論上就可以再次讀取了。
首先說反射機制本身就是通過破壞類的封裝來實現(xiàn)動態(tài)修改的,有點過于粗暴了,其次也是主要原因,我們只能針對我們自己實現(xiàn)的代碼進行處理,框架本身如果調用getInputStream()和getReader()的話,我們就沒法通過這個辦法干預了,所以這個方案在給予Spring的Web項目中并不可行。
理論上可行的方案:HttpServletRequest接口
HttpServletRequest是一個接口,理論上我們只需要創(chuàng)建一個實現(xiàn)類就可以自定義getInputStream()和getReader()的行為,自然也就能解決RequestBody不能重復讀取的問題,但這個方案的問題在于HttpServletRequest有70個方法,而我們只需要修改其中兩個而已,通過這種方式去解決有點得不償失。
部分場景可行的方案:ContentCachingRequestWrapper
Spring本身提供了一個Request包裝類來處理重復讀取的問題,即ContentCachingRequestWrapper,其實現(xiàn)思路就是在讀取RequestBody時將內存緩存到它內部的一個字節(jié)流中,后續(xù)讀取可以通過調用getContentAsString()或getContentAsByteArray()獲取到緩存下來的內容。
之所以說這個方案是部分場景可行主要是兩個方面,一是ContentCachingRequestWrapper沒有重寫getInputStream()和getReader()方法,所以框架中使用這兩個方法的地方依然獲取不到緩存下來的內容,僅支持自定義的業(yè)務邏輯;第二點和第一點有所關聯(lián),因為其沒有修改getInputStream()和getReader()方法,所以我們在使用時只能在使用RequestBody注解后使用ContentCachingRequestWrapper,否則就會出現(xiàn)RequestBody注解修飾的參數(shù)無法正常讀取請求體的問題,也就限定了它的使用范圍如下圖所示:

如果僅需要在業(yè)務代碼后再次讀取請求體內容,那么使用ContentCachingRequestWrapper也足以滿足需求,具體使用方法請參考下一節(jié)的說明。
目前的最佳實踐:繼承HttpServletRequestWrapper
之前我們提到實現(xiàn)HttpServletRequest需要實現(xiàn)70個方法,所以不太可能自行實現(xiàn),這個方案算是進階版本,繼承HttpServletRequest的實現(xiàn)類,之后再自定義我們需要修改的兩個方法。
HttpServletRequest作為一個接口,肯定會有其實現(xiàn)去支撐它的業(yè)務功能,因為Servlet容器的選擇較多,我們也不能使用某一方提供的實現(xiàn),所以選擇的范圍也就被限制到了Java EE(現(xiàn)在叫Jakarta EE)標準范圍內,通過查看HttpServletRequest的實現(xiàn),可以發(fā)現(xiàn)在標準內提供了一個包裝類:HttpServletRequestWrapper,我們的方案也是圍繞它展開。
思路簡述
- 自定義子類,繼承HttpServletRequestWrapper,在子類的構造方法中將RequestBody緩存到自定義的屬性中。
- 自定義getInputStream()和getReader()的業(yè)務邏輯,不再校驗usingReader和usingInputStream,且在調用時讀取緩存下來的內容。
- 自定義Filter,將默認的HttpServletRequest替換為自定義的包裝類。
代碼展示
- 繼承HttpServletRequestWrapper,實現(xiàn)子類CustomRequestWrapper,并自定義getInputStream()和getReader()的業(yè)務邏輯
// 1.繼承HttpServletRequestWrapper
public class CustomRequestWrapper extends HttpServletRequestWrapper {
// 2.定義final屬性,用于緩存請求體內容
private final byte[] content;
public CustomRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 3.構造方法中將請求體內容緩存到內部屬性中
this.content = StreamUtils.copyToByteArray(request.getInputStream());
}
// 4.重新getInputStream()
@Override
public ServletInputStream getInputStream() {
// 5.將緩存下來的內容轉換為字節(jié)流
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
// 6.讀取時讀取第5步初始化的字節(jié)流
return byteArrayInputStream.read();
}
};
}
// 7.重寫getReader()方法,這里復用getInputStream()的邏輯
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
- 自定義Filter將默認的HttpServletRequest替換為自定義的CustomRequestWrapper
// 1.實現(xiàn)Filter接口,此處也可以選擇繼承HttpFilter
public class RequestWrapperFilter implements Filter {
// 2. 重寫或實現(xiàn)doFilter方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 3.此處判斷是為了縮小影響范圍,本身CustomRequestWrapper只是針對HttpServletRequest,不進行判斷可能會影響其他類型的請求
if (request instanceof HttpServletRequest) {
// 4.將默認的HttpServletRequest轉換為自定義的CustomRequestWrapper
CustomRequestWrapper requestWrapper = new CustomRequestWrapper((HttpServletRequest) request);
// 5.將轉換后的request傳遞至調用鏈中
chain.doFilter(requestWrapper, response);
} else {
chain.doFilter(request, response);
}
}
}
- 將Filter注冊到Spring容器,這一步可以通過多種方式執(zhí)行,這里采用比較傳統(tǒng)但比較靈活的Bean方式注冊,如果圖方便可以通過ServletComponentScan注解+ WebFilter注解的方式。
/**
* 過濾器配置,支持第三方過濾器
*/
@Configuration
public class FilterConfigure {
/**
* 請求體封裝
* @return
*/
@Bean
public FilterRegistrationBean<RequestWrapperFilter> filterRegistrationBean(){
FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new RequestWrapperFilter());
bean.addUrlPatterns("/*");
return bean;
}
}
至此我們就可以在項目中重復讀取請求體了,如果選擇使用Spring提供的ContentCachingRequestWrapper,那么在Filter中將CustomRequestWrapper替換為ContentCachingRequestWrapper即可,不過需要注意在上一節(jié)提到的可用范圍較小的問題。
文章內的代碼可以參考 https://gitee.com/itartisans/itartisans-framework,這是我開源的一個SpringBoot項目腳手架,我會不定期加入一些通用功能,歡迎關注。