從零搭建開發(fā)腳手架之 HttpServletRequest多次讀取異常問題的因和果
本文轉(zhuǎn)載自微信公眾號(hào)「Java大廠面試官」,作者laker。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java大廠面試官公眾號(hào)。
背景
在過濾器或者Controller中多次調(diào)用HttpServletRequest.getReader()或getInputStream()方法,會(huì)導(dǎo)致異常。
給出示例代碼如下:
- @RequestMapping(value = "/param")
- private ResponseEntity<String> param(HttpServletRequest request, @RequestBody Map body){
- // ...
- String string = IOUtils.toString(request.getInputStream());
- // ...
- }
Postman請(qǐng)求如下:
錯(cuò)誤如下:
- java.lang.IllegalStateException: getInputStream() has already been called for this request
- at org.apache.catalina.connector.Request.getReader(Request.java:1222) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
- at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
- at com.laker.notes.easy.http.HttpController.param(HttpController.java:64) ~[classes/:na]
- ...
原因
Json數(shù)據(jù)是放在Http協(xié)議的Body中的,我們需要通過request.getInputStream()或者@RequestBody(本質(zhì)也是調(diào)用request.getInputStream())獲取請(qǐng)求體內(nèi)容。
當(dāng)我們調(diào)用request.getInputStream()時(shí),可以查看其Api,其返回的是ServletInputStream繼承于InputStream。
- public ServletInputStream getInputStream() throws IOException;
- public abstract class ServletInputStream extends InputStream {
- // ...
- }
下面我們來復(fù)習(xí)下流的知識(shí):
InputStream的read方法內(nèi)部有一個(gè)position,標(biāo)志當(dāng)前讀取到的位置,讀取到最后會(huì)返回-1,表示讀取完畢。如果想要重新讀取則需要使用mark和reset方法配合使用,把position移動(dòng)到起始位置,就能從頭讀取實(shí)現(xiàn)多次讀取,但是InputStream和ServletInputStream都未重寫mark和reset方法。
所以就導(dǎo)致HttpServletRequest.getReader()或getInputStream()方法不能多次讀取。
解決辦法
使用HttpServletRequestWrapper,此類是HttpServletRequest的包裝類,基于裝飾器模式實(shí)現(xiàn)HttpServletRequest功能擴(kuò)展。我們可以通過繼承包裝類HttpServletRequestWrapper來實(shí)現(xiàn)自定義擴(kuò)展功能。
- 我們重新定義一個(gè)容器(字節(jié)數(shù)組),把讀取到的流數(shù)據(jù)存儲(chǔ)其中供以后多次使用。
- 重寫getReader()和getInputStream()方法,改為每次從自定義容器中獲取內(nèi)容。
- 再配合Filter把原始的HttpServletRequest替換為我們自定義的包裝類xxxHttpServletRequestWrapper。
代碼如下:
- CachedBodyHttpServletRequestWrapper.java
- public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
- private byte[] cachedBody;
- public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
- super(request);
- InputStream requestInputStream = request.getInputStream();
- this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
- }
- @Override
- public ServletInputStream getInputStream() throws IOException {
- return new CachedBodyServletInputStream(this.cachedBody);
- }
- @Override
- public BufferedReader getReader() throws IOException {
- ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
- return new BufferedReader(new InputStreamReader(byteArrayInputStream));
- }
- public class CachedBodyServletInputStream extends ServletInputStream {
- private InputStream cachedBodyInputStream;
- public CachedBodyServletInputStream(byte[] cachedBody) {
- this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
- }
- @Override
- public int read() throws IOException {
- return cachedBodyInputStream.read();
- }
- // ...
- }
- }
- ContentCachingFilter.java
- @Order(value = Ordered.HIGHEST_PRECEDENCE)
- @Component
- @WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*")
- public class ContentCachingFilter extends OncePerRequestFilter {
- @Override
- protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
- System.out.println("IN ContentCachingFilter ");
- CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
- filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
- }
- }
擴(kuò)展思考
1.是否存在線程安全問題?
實(shí)測(cè)結(jié)果如下圖,非單例,不存在線程安全問題。
2.加載順序問題?
ContentCachingFilter必須在Filter鏈中的第一個(gè),否則后面使用的是非自定義包裝類而是默認(rèn)的HttpServletRequest,將無法起作用。
3.OncePerRequestFilter和Filter的區(qū)別
OncePerRequestFilter 實(shí)現(xiàn)了 Filter 接口。
- OncePerRequestFilter extends GenericFilterBean implements Filter{
- }
在Spring中,F(xiàn)ilter默認(rèn)繼承OncePerRequestFilter。
OncePerRequestFilter:顧名思義,它能夠確保在一次請(qǐng)求中只通過一次filter,而需要重復(fù)的執(zhí)行。大家常識(shí)上都認(rèn)為,一次請(qǐng)求本來就只filter一次,為什么還要由此特別限定呢。
往往我們的常識(shí)和實(shí)際的實(shí)現(xiàn)并不真的一樣,經(jīng)過一番資料的查閱,此方法是為了兼容不同的web container,也就是說并不是所有的container都入我們期望的只過濾一次,servlet版本不同,執(zhí)行過程也不同,我們可以看看Spring的javadoc怎么說:
- *
- * <p>As of Servlet 3.0, a filter may be invoked as part of a
- * {@link javax.servlet.DispatcherType#REQUEST REQUEST} or
- * {@link javax.servlet.DispatcherType#ASYNC ASYNC} dispatches that occur in
- * separate threads. A filter can be configured in {@code web.xml} whether it
- * should be involved in async dispatches. However, in some cases servlet
- * containers assume different default configuration.
簡單的說就是去適配了不同的web容器,以及對(duì)異步請(qǐng)求,也只過濾一次的需求。另外打個(gè)比方:如:servlet2.3與servlet2.4也有一定差異:
在servlet2.3中,F(xiàn)ilter會(huì)經(jīng)過一切請(qǐng)求,包括服務(wù)器內(nèi)部使用的forward轉(zhuǎn)發(fā)請(qǐng)求和<%@ include file=”/login.jsp”%>的情況 servlet2.4中的Filter默認(rèn)情況下只過濾外部提交的請(qǐng)求,forward和include這些內(nèi)部轉(zhuǎn)發(fā)都不會(huì)被過濾,因此此處我有個(gè)建議:我們?nèi)羰窃赟pring環(huán)境下使用Filter的話,個(gè)人建議繼承OncePerRequestFilter吧,而不是直接實(shí)現(xiàn)Filter接口。這是一個(gè)比較穩(wěn)妥的選擇
參考:
https://cloud.tencent.com/developer/article/1497822