当前位置: 首页 > 科技观察

HttpServletRequest多次读取异常问题的前因后果从头搭建开发脚手架

时间:2023-03-23 10:51:48 科技观察

本文转载自微信公众号《Java大厂面试官》,作者laker。转载本文请联系Java大厂面试官公众号。后台在filter或Controller中多次调用HttpServletRequest.getReader()或getInputStream()方法会引发异常。示例代码如下:@RequestMapping(value="/param")privateResponseEntityparam(HttpServletRequestrequest,@RequestBodyMapbody){//...Stringstring=IOUtils.toString(request.getInputStream());//...}Postman请求如下:报错如下:java.lang.IllegalStateException:getInputStream()hasalreadybeencalledforthisrequestorg.apache.catalina.connector.Request.getReader(Request.java:1222)~[tomcat-embed-core-9.0.41.jar:9.0.41]atorg.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504)~[tomcat-embed-core-9.0.41.jar:9.0.41]atcom.laker.notes.easy.http。HttpController.param(HttpController.java:64)~[classes/:na]...原因是Json数据放在Http协议的Body中,我们需要通过request.getInputStream()或者@RequestBody(本质就是调用request.getInputStream())获取请求体的内容。当我们调用request.getInputStream()时,可以查看它的Api,返回的是继承自InputStream的ServletInputStream。publicServletInputStreamgetInputStream()throwsIOException;publicabstractclassServletInputStreamextendsInputStream{//...}再回顾一下猥琐知识:InputStream的read方法里面有个position,标记当前读取位置,读取结束会返回-1,表示读取完成。如果要重新读取,需要同时使用mark和reset方法,将位置移动到起始位置,可以从头开始读取,实现多次读取,但是InputStream和ServletInputStream都没有重写mark和reset方法.因此,HttpServletRequest.getReader()或getInputStream()方法不能被多次读取。解决方案是使用HttpServletRequestWrapper,它是HttpServletRequest的包装类,基于装饰器模式实现对HttpServletRequest的功能扩展。我们可以通过继承包装类HttpServletRequestWrapper来实现自定义扩展功能。我们重新定义了一个容器(字节数组),将读取到的流数据存储在里面,方便以后多次使用。重写getReader()和getInputStream()方法,每次都从自定义容器中获取内容。然后配合Filter将原来的HttpServletRequest替换为我们自定义的包装类xxxHttpServletRequestWrapper。代码如下:CachedBodyHttpServletRequestWrapper.javapublicclassCachedBodyHttpServletRequestWrapperextendsHttpServletRequestWrapper{privatebyte[]cachedBody;publicCachedBodyHttpServletRequestWrapper(HttpServletRequestrequest)throwsIOException{super(request);InputStreamrequestInputStream=request.getInputStream();this.cachedBody=StreamUtils.copyToByteArray(requestInputStream);}@OverridepublicServletInputStreamgetInputStream()throwsIOException{returnnewCachedBodyServletInputStream(this.cachedBody);}@OverridepublicBufferedReadergetReader()throwsIOException{ByteArrayInputStreambyteArrayInputStream=newByteArrayInputStream(this.cachedBody);returnnewBufferedReader(newInputStreamReader(byteArrayInputStream));}publicclassCachedBodyServletInputStreamextendsServletInputStream{privateInputStreamcachedBodyInputStream;publicCachedBodyServletInputStream(byte[]cachedBody){this.cachedBodyInputStream=newByteArrayInputStream(cachedBody);}@Overridepublicintread()throwsIOException{returncachedBodyInputStream.read();}//...}}ContentCachingFilter.java@Order(value=Ordered.HIGHEST_PRECEDENCE)@Component@WebFilter(filterName="ContentCachingFilter",urlPatterns="/*")publicclassContentCachingFilterextendsOncePerRequestInterquestFilter{@edoverrid(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,FilterChainfilterChain)throwsServletException,IOException{System.out.println("INContentCachingFilter");CachedBodyHttpServletRequestcachedBodyHttpServletRequest=newCachedBodyHttpServletRequest(httpServletRequest);filterChain.doFilter(cachedBodyHttpServletRequest,httpServletResponse);}}扩展思考1.是否存在线程安全问题?实际测量结果如下图所示。不是单例,不存在线程安全问题。2.加载顺序问题?ContentCachingFilter必须是Filter链中的第一个,否则不是自定义包装类的默认HttpServletRequest将无法工作。.3、OncePerRequestFilter和Filter的区别OncePerRequestFilter实现了Filter接口。OncePerRequestFilterextendsGenericFilterBeanimplementsFilter{}Spring中Filter默认继承OncePerRequestFilter。OncePerRequestFilter:顾名思义,可以保证一次请求只传入一个过滤器,需要重复执行。大家按常理想,一个请求只能过滤一次,为什么要专门受这个限制。通常我们的常识和实际的执行并不完全相同。经过一些资料查阅,这种方式是为了兼容不同的web容器,也就是说,并不是所有的容器都包括在内。我们只过滤一次,servlet版本不同,执行过程也不同,我们可以看看Spring的javadoc是怎么说的:**

AsofServlet3.0,afiltermaybeinvokedaspartofa*{@linkjavax.servlet.DispatcherType#REQUESTREQUEST}or*{@linkjavax.servlet.DispatcherType#ASYNCASYNC}dispatchesthatoccurin*separatethreads.Afiltercanbeconfiguredin{@codeweb.xml}whetherit*shouldbeinvolvedinasyncdispatches.However,insomecasesesservlet*containersassumeddifferentdefaultconfiguration.简单的说就是适配不同的web容器,对异步请求只过滤一次。再打个比方:比如servlet2.3和servlet2.4有一定的区别:在servlet2.3中,Filter会遍历所有的请求,包括服务器内部使用的forward转发请求和<%@includefile="/login.jsp中"%>的情况下,servlet2.4中的Filter默认只过滤外部提交的请求,不会过滤forward、include等内部转发,所以我这里有一个建议:如果我们在Spring中使用Filterenvironment,我个人建议继承OncePerRequestFilter,而不是直接实现Filter接口。这是一个比较安全的选择参考:https://cloud.tencent.com/developer/article/1497822