从零搭建开发脚手架之 HttpServletRequest多次读取异常问题的因和果

开发 前端
在过滤器或者Controller中多次调用HttpServletRequest.getReader()或getInputStream()方法,会导致异常。

[[382286]]

本文转载自微信公众号「Java大厂面试官」,作者laker。转载本文请联系Java大厂面试官公众号。

背景

在过滤器或者Controller中多次调用HttpServletRequest.getReader()或getInputStream()方法,会导致异常。

给出示例代码如下:

  1. @RequestMapping(value = "/param"
  2. private ResponseEntity<String> param(HttpServletRequest request, @RequestBody Map body){ 
  3.       // ... 
  4.       String string = IOUtils.toString(request.getInputStream()); 
  5.       // ... 

Postman请求如下:

 

错误如下:

  1. java.lang.IllegalStateException: getInputStream() has already been called for this request 
  2.  at org.apache.catalina.connector.Request.getReader(Request.java:1222) ~[tomcat-embed-core-9.0.41.jar:9.0.41] 
  3.  at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504) ~[tomcat-embed-core-9.0.41.jar:9.0.41] 
  4.  at com.laker.notes.easy.http.HttpController.param(HttpController.java:64) ~[classes/:na] 
  5.     ... 

原因

Json数据是放在Http协议的Body中的,我们需要通过request.getInputStream()或者@RequestBody(本质也是调用request.getInputStream())获取请求体内容。

当我们调用request.getInputStream()时,可以查看其Api,其返回的是ServletInputStream继承于InputStream。

  1. public ServletInputStream getInputStream() throws IOException; 
  2.  
  3. public abstract class ServletInputStream extends InputStream { 
  4.     // ... 

下面我们来复习下流的知识:

InputStream的read方法内部有一个position,标志当前读取到的位置,读取到最后会返回-1,表示读取完毕。如果想要重新读取则需要使用mark和reset方法配合使用,把position移动到起始位置,就能从头读取实现多次读取,但是InputStream和ServletInputStream都未重写mark和reset方法。

所以就导致HttpServletRequest.getReader()或getInputStream()方法不能多次读取。

解决办法

使用HttpServletRequestWrapper,此类是HttpServletRequest的包装类,基于装饰器模式实现HttpServletRequest功能扩展。我们可以通过继承包装类HttpServletRequestWrapper来实现自定义扩展功能。

  • 我们重新定义一个容器(字节数组),把读取到的流数据存储其中供以后多次使用。
  • 重写getReader()和getInputStream()方法,改为每次从自定义容器中获取内容。
  • 再配合Filter把原始的HttpServletRequest替换为我们自定义的包装类xxxHttpServletRequestWrapper。

代码如下:

  • CachedBodyHttpServletRequestWrapper.java
  1. public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper { 
  2.     private byte[] cachedBody; 
  3.     public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException { 
  4.         super(request); 
  5.         InputStream requestInputStream = request.getInputStream(); 
  6.         this.cachedBody = StreamUtils.copyToByteArray(requestInputStream); 
  7.     } 
  8.     @Override 
  9.     public ServletInputStream getInputStream() throws IOException { 
  10.         return new CachedBodyServletInputStream(this.cachedBody); 
  11.     } 
  12.     @Override 
  13.     public BufferedReader getReader() throws IOException { 
  14.         ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody); 
  15.         return new BufferedReader(new InputStreamReader(byteArrayInputStream)); 
  16.     } 
  17.     public class CachedBodyServletInputStream extends ServletInputStream { 
  18.         private InputStream cachedBodyInputStream; 
  19.         public CachedBodyServletInputStream(byte[] cachedBody) { 
  20.             this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody); 
  21.         } 
  22.         @Override 
  23.         public int read() throws IOException { 
  24.             return cachedBodyInputStream.read(); 
  25.         } 
  26.         // ... 
  27.     } 
  • ContentCachingFilter.java
  1. @Order(value = Ordered.HIGHEST_PRECEDENCE) 
  2. @Component 
  3. @WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*"
  4. public class ContentCachingFilter extends OncePerRequestFilter { 
  5.  
  6.     @Override 
  7.     protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { 
  8.         System.out.println("IN  ContentCachingFilter "); 
  9.         CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest); 
  10.         filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse); 
  11.     } 

扩展思考

1.是否存在线程安全问题?

实测结果如下图,非单例,不存在线程安全问题。

 

2.加载顺序问题?

ContentCachingFilter必须在Filter链中的第一个,否则后面使用的是非自定义包装类而是默认的HttpServletRequest,将无法起作用。

3.OncePerRequestFilter和Filter的区别

OncePerRequestFilter 实现了 Filter 接口。

  1. OncePerRequestFilter extends GenericFilterBean implements Filter{ 

在Spring中,Filter默认继承OncePerRequestFilter。

 

OncePerRequestFilter:顾名思义,它能够确保在一次请求中只通过一次filter,而需要重复的执行。大家常识上都认为,一次请求本来就只filter一次,为什么还要由此特别限定呢。

往往我们的常识和实际的实现并不真的一样,经过一番资料的查阅,此方法是为了兼容不同的web container,也就是说并不是所有的container都入我们期望的只过滤一次,servlet版本不同,执行过程也不同,我们可以看看Spring的javadoc怎么说:

  1. * <p>As of Servlet 3.0, a filter may be invoked as part of a 
  2. * {@link javax.servlet.DispatcherType#REQUEST REQUEST} or 
  3. * {@link javax.servlet.DispatcherType#ASYNC ASYNC} dispatches that occur in 
  4. * separate threads. A filter can be configured in {@code web.xml} whether it 
  5. * should be involved in async dispatches. However, in some cases servlet 
  6. * 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

 

责任编辑:武晓燕 来源: Java大厂面试官
相关推荐

2021-04-28 16:10:48

开发脚手架 Spring

2021-06-02 17:58:49

脚手架 幂等性前端

2021-05-13 17:02:38

MDC脚手架日志

2021-07-13 18:42:38

Spring Boot脚手架开发

2021-04-13 14:47:53

认证授权Java

2020-08-19 08:55:47

Redis缓存数据库

2021-07-29 18:49:49

Spring开发脚手架

2021-03-11 14:16:47

Spring Boo开发脚手架

2021-04-20 19:24:16

脚手架 Java微信

2021-09-01 10:07:43

开发零搭建Groovy

2021-03-09 17:11:09

数据库脚手架开发

2016-08-10 14:59:41

前端Javascript工具

2023-11-21 17:36:04

OpenFeignSentinel

2021-01-07 05:34:07

脚手架JDK缓存

2018-06-11 14:39:57

前端脚手架工具node.js

2018-08-30 16:08:37

Node.js脚手架工具

2014-08-15 09:36:06

2022-07-18 07:58:46

Spring工具工具类

2021-12-23 10:35:32

SpringCloud脚手架架构

2016-09-07 15:35:06

VueReact脚手架
点赞
收藏

51CTO技术栈公众号