本文转载自微信公众号「Java大厂面试官」,作者laker。转载本文请联系Java大厂面试官公众号。
成都创新互联公司是专业的馆陶网站建设公司,馆陶接单;提供网站设计制作、成都网站建设,网页设计,网站设计,建网站,PHP网站建设等专业做网站服务;采用PHP框架,可快速的进行馆陶网站开发网页制作和功能扩展;专业做搜索引擎喜爱的网站,专业的做网站团队,希望更多企业前来合作!
背景
在过滤器或者Controller中多次调用HttpServletRequest.getReader()或getInputStream()方法,会导致异常。
给出示例代码如下:
- @RequestMapping(value = "/param")
- private ResponseEntity
param(HttpServletRequest request, @RequestBody Map body){ - // ...
- String string = IOUtils.toString(request.getInputStream());
- // ...
- }
Postman请求如下:
错误如下:
- 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数据是放在Http协议的Body中的,我们需要通过request.getInputStream()或者@RequestBody(本质也是调用request.getInputStream())获取请求体内容。
当我们调用request.getInputStream()时,可以查看其Api,其返回的是ServletInputStream继承于InputStream。
- public ServletInputStream getInputStream() throws IOException;
- public abstract class ServletInputStream extends InputStream {
- // ...
- }
下面我们来复习下流的知识:
InputStream的read方法内部有一个position,标志当前读取到的位置,读取到最后会返回-1,表示读取完毕。如果想要重新读取则需要使用mark和reset方法配合使用,把position移动到起始位置,就能从头读取实现多次读取,但是InputStream和ServletInputStream都未重写mark和reset方法。
所以就导致HttpServletRequest.getReader()或getInputStream()方法不能多次读取。
解决办法
使用HttpServletRequestWrapper,此类是HttpServletRequest的包装类,基于装饰器模式实现HttpServletRequest功能扩展。我们可以通过继承包装类HttpServletRequestWrapper来实现自定义扩展功能。
代码如下:
- 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();
- }
- // ...
- }
- }
- @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);
- }
- }
扩展思考
1.是否存在线程安全问题?
实测结果如下图,非单例,不存在线程安全问题。
2.加载顺序问题?
ContentCachingFilter必须在Filter链中的第一个,否则后面使用的是非自定义包装类而是默认的HttpServletRequest,将无法起作用。
3.OncePerRequestFilter和Filter的区别
OncePerRequestFilter 实现了 Filter 接口。
- OncePerRequestFilter extends GenericFilterBean implements Filter{
- }
在Spring中,Filter默认继承OncePerRequestFilter。
OncePerRequestFilter:顾名思义,它能够确保在一次请求中只通过一次filter,而需要重复的执行。大家常识上都认为,一次请求本来就只filter一次,为什么还要由此特别限定呢。
往往我们的常识和实际的实现并不真的一样,经过一番资料的查阅,此方法是为了兼容不同的web container,也就是说并不是所有的container都入我们期望的只过滤一次,servlet版本不同,执行过程也不同,我们可以看看Spring的javadoc怎么说:
- *
- *
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容器,以及对异步请求,也只过滤一次的需求。另外打个比方:如:servlet2.3与servlet2.4也有一定差异:
在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况 servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤,因此此处我有个建议:我们若是在Spring环境下使用Filter的话,个人建议继承OncePerRequestFilter吧,而不是直接实现Filter接口。这是一个比较稳妥的选择
参考:
https://cloud.tencent.com/developer/article/1497822
分享文章:从零搭建开发脚手架之HttpServletRequest多次读取异常问题的因和果
网站URL:http://www.36103.cn/qtweb/news25/3125.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联