从零搭建开发脚手架 HttpServletRequest多次读取异常,仅能读取一次

501 阅读4分钟

文章目录

准备做个《从零搭建开发脚手架系列》,把遇到的问题和搭建的过程记录分享给大家。

背景

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

给出示例代码如下

  @RequestMapping(value = "/param")
  private ResponseEntity<String> 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 {
    // ...
}

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

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

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

解决办法

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

  • 我们重新定义一个容器(字节数组),把读取到的流数据存储其中供以后多次使用。
  • 重写getReader()和getInputStream()方法,改为每次从自定义容器中获取内容。
  • 再配合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 {
        // 不能拦截 application/x-www-form-urlencoded 和 multipart/form-data请求,否则会出现参数丢失,因为都是是http body中的 它们2个只能从流读取一次,后面解析的时候会出现问题。
        if (StrUtil.contains(httpServletRequest.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            CachedBodyHttpServletRequestWrapper cachedBodyHttpServletRequest = new CachedBodyHttpServletRequestWrapper(httpServletRequest);
            filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
        } else {
            filterChain.doFilter(httpServletRequest, 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怎么说:

 *
 * <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容器,以及对异步请求,也只过滤一次的需求。另外打个比方:如:servlet2.3与servlet2.4也有一定差异:

在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况 servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤,

因此此处我有个建议:我们若是在Spring环境下使用Filter的话,个人建议继承OncePerRequestFilter吧,而不是直接实现Filter接口。这是一个比较稳妥的选择

参考:


🍎QQ群【837324215
🍎关注我的公众号【Java大厂面试官】,一起学习呗🍎🍎🍎

img