新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「Java大廠面試官」,作者laker。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java大廠面試官公眾號(hào)。

成都創(chuàng)新互聯(lián)公司是專業(yè)的館陶網(wǎng)站建設(shè)公司,館陶接單;提供網(wǎng)站設(shè)計(jì)制作、成都網(wǎng)站建設(shè),網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行館陶網(wǎng)站開(kāi)發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛(ài)的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來(lái)合作!
背景
在過(guò)濾器或者Controller中多次調(diào)用HttpServletRequest.getReader()或getInputStream()方法,會(huì)導(dǎo)致異常。
給出示例代碼如下:
- @RequestMapping(value = "/param")
- private ResponseEntity
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中的,我們需要通過(guò)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 {
- // ...
- }
下面我們來(lái)復(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都未重寫(xiě)mark和reset方法。
所以就導(dǎo)致HttpServletRequest.getReader()或getInputStream()方法不能多次讀取。
解決辦法
使用HttpServletRequestWrapper,此類是HttpServletRequest的包裝類,基于裝飾器模式實(shí)現(xiàn)HttpServletRequest功能擴(kuò)展。我們可以通過(guò)繼承包裝類HttpServletRequestWrapper來(lái)實(shí)現(xiàn)自定義擴(kuò)展功能。
- 我們重新定義一個(gè)容器(字節(jié)數(shù)組),把讀取到的流數(shù)據(jù)存儲(chǔ)其中供以后多次使用。
- 重寫(xiě)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.是否存在線程安全問(wèn)題?
實(shí)測(cè)結(jié)果如下圖,非單例,不存在線程安全問(wèn)題。
2.加載順序問(wèn)題?
ContentCachingFilter必須在Filter鏈中的第一個(gè),否則后面使用的是非自定義包裝類而是默認(rèn)的HttpServletRequest,將無(wú)法起作用。
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)求中只通過(guò)一次filter,而需要重復(fù)的執(zhí)行。大家常識(shí)上都認(rèn)為,一次請(qǐng)求本來(lái)就只filter一次,為什么還要由此特別限定呢。
往往我們的常識(shí)和實(shí)際的實(shí)現(xiàn)并不真的一樣,經(jīng)過(guò)一番資料的查閱,此方法是為了兼容不同的web container,也就是說(shuō)并不是所有的container都入我們期望的只過(guò)濾一次,servlet版本不同,執(zhí)行過(guò)程也不同,我們可以看看Spring的javadoc怎么說(shuō):
- *
- *
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.
簡(jiǎn)單的說(shuō)就是去適配了不同的web容器,以及對(duì)異步請(qǐng)求,也只過(guò)濾一次的需求。另外打個(gè)比方:如:servlet2.3與servlet2.4也有一定差異:
在servlet2.3中,F(xiàn)ilter會(huì)經(jīng)過(guò)一切請(qǐng)求,包括服務(wù)器內(nèi)部使用的forward轉(zhuǎn)發(fā)請(qǐng)求和<%@ include file=”/login.jsp”%>的情況 servlet2.4中的Filter默認(rèn)情況下只過(guò)濾外部提交的請(qǐng)求,forward和include這些內(nèi)部轉(zhuǎn)發(fā)都不會(huì)被過(guò)濾,因此此處我有個(gè)建議:我們?nèi)羰窃赟pring環(huán)境下使用Filter的話,個(gè)人建議繼承OncePerRequestFilter吧,而不是直接實(shí)現(xiàn)Filter接口。這是一個(gè)比較穩(wěn)妥的選擇
參考:
https://cloud.tencent.com/developer/article/1497822
名稱欄目:從零搭建開(kāi)發(fā)腳手架之HttpServletRequest多次讀取異常問(wèn)題的因和果
當(dāng)前地址:http://fisionsoft.com.cn/article/ccdesch.html


咨詢
建站咨詢
