HttpServletRequest 流数据不可重复读问题以及解决方案

发布时间 2023-07-13 14:49:51作者: 话祥

1.HttpServletRequest 流数据不可重复读的原因

  HttpServletRequest 的request.getInputStream()只可以读取一次参数,由于 InputStream 这个流数据的特殊性,在 Java 中读取 InputStream 数据时,内部是通过一个指针的移动来读取一个一个的字节数据的,当读完一遍后,这个指针并不会 reset,因此第二遍读的时候就会出现问题了。(https://www.cnblogs.com/Sinte-Beuve/p/13260249.html

2.问题重现

  req.getParameter("name")会调用 parseParameters() 方法对参数进行封装,从 InputStream 中读取数据,并封装到 Map 中,所以再次获取参数值的时候是从map中获取,见图片1。将request.getInputStream()放到最前面后,后面通过request.getInputStream()和req.getParameter("name")就无法获取到了,见图片2。

 

 3.解决方案

  使用ContentCachingRequestWrapper,通过ContentCachingRequestWrapper#getContentAsByteArray()来读取数据,来实现可重复读的目的。

  @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String contentType = request.getContentType();
        if (request instanceof HttpServletRequest) {
            HttpServletRequest requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
            if (contentType != null && contentType.contains("multipart/form-data")) {
                chain.doFilter(request, response);
            } else {
                chain.doFilter(requestWrapper, response);
            }
            return;
        }
        chain.doFilter(request, response);
    }

  注:1.这里需要根据 contentType 做一下区分,遇到 multipart/form-data 数据时,不需要 wrapper,会直接通过 MultipartResolver 将参数封装成 Map,当然这也可以灵活的在拦截器中判断。

  2.wrapper 在具体使用中,我们可以使用 getContentAsByteArray() 来获取数据,并通过 IOUtils 或者JSONObject转换成 String。尽量不使用 request.getInputStream()。因为虽然经过了包装,但是 InputStream 仍然只能读一次,而参数进入 Controller 的方法前 HttpMessageConverter 的参数转换需要调用这个方法,所以把它保留就可以了。

public static Map<String, Object> getBodyParams1(HttpServletRequest request) throws UnsupportedEncodingException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        BufferedInputStream br = null;
        // 这里不能使用getInputStream读取,使用了后面再次读取会读取不到
            /*br = new BufferedInputStream(request.getInputStream());
            for(int c=0; (c=br.read())!=-1;){
                bos.write(c);
            }
            String ok = bos.toString();
            return JSONObject.parseObject(ok, Map.class);*/
        // 判断一下,排除 multipart/form-data数据时没有转为ContentCachingRequestWrapper
        if (request instanceof ContentCachingRequestWrapper) {
            // 使用缓存流读取数据
            byte[] contentData = ((ContentCachingRequestWrapper) request).getContentAsByteArray();
            // IOUtils.toString(contentData, "utf-8");
            return JSONObject.parseObject(new String(contentData, "utf-8"), Map.class);
        }
        return new HashMap<>();
    }

  3.也有重写ContentCachingRequestWrapper类替代的。如下:

//继承ContentCachingRequestWrapper 
public class ContentCachingRequestWrapperNew extends ContentCachingRequestWrapper {
 
    //原子变量,用来区分首次读取还是非首次
    private AtomicBoolean isFirst = new AtomicBoolean(true);
 
    public ContentCachingRequestWrapperNew(HttpServletRequest request) {
        super(request);
    }
 
    public ContentCachingRequestWrapperNew(HttpServletRequest request, int contentCacheLimit) {
        super(request, contentCacheLimit);
    }
 
    @Override
    public ServletInputStream getInputStream() throws IOException {
 
        if(isFirst.get()){
            //首次读取直接调父类的方法,这一次执行完之后 缓存流中有数据了
            //后续读取就读缓存流里的。
            isFirst.set(false);
            return super.getInputStream();
        }
 
        //用缓存流构建一个新的输入流
        return new ServletInputStreamNew(super.getContentAsByteArray());
    }
 
    //参考自 DelegatingServletInputStream
    class ServletInputStreamNew extends ServletInputStream{
 
        private InputStream sourceStream;
 
        private boolean finished = false;
 
 
 
        public ServletInputStreamNew(byte [] bytes) {
            //构建一个普通的输入流
            this.sourceStream = new ByteArrayInputStream(bytes);
        }
 
 
        @Override
        public int read() throws IOException {
            int data = this.sourceStream.read();
            if (data == -1) {
                this.finished = true;
            }
            return data;
        }
 
        @Override
        public int available() throws IOException {
            return this.sourceStream.available();
        }
 
        @Override
        public void close() throws IOException {
            super.close();
            this.sourceStream.close();
        }
 
        @Override
        public boolean isFinished() {
            return this.finished;
        }
 
        @Override
        public boolean isReady() {
            return true;
        }
 
        @Override
        public void setReadListener(ReadListener readListener) {
            throw new UnsupportedOperationException();
        }
    }
 
}

重写的原因(自己实现 Wrapper 的方案,也要注意,如果是直接在 Wrapper 的构造函数中读取流数据到 byte[] 数据中去,这样在遇到 multipart/form-data 这种数据类型的时候就会出现问题了,因为包装在调用 MultipartResolver 之前执行,再次调用的时候就读不到数据了。):

https://blog.csdn.net/b306533659/article/details/121289056?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1-121289056-blog-89710200.235^v38^pc_relevant_sort_base1&spm=1001.2101.3001.4242.2&utm_relevant_index=4)。