WANG LH , Research & Development

I/O error while reading input message

2021.08.19 21:54

异常信息

I/O error while reading input message; nested exception is java.io.IOException: Stream closed

异常原因

首先request.getInputSteam(),只能获取一次,如果重复读就会出现上面异常信息。为什么会出现多次读呢,因前面设置了拦截器,用到了request中的参数,在取 body参数时把request.getInputStream()关闭,导致后面doFilter到@requestBody的对象拿取不到。

解决方案

思想是将inputStream缓存下来,然后后面filter再读的时候,直接从缓存中读取即可。

实现代码

  1. 缓存实现
public class CachingServletRequestWrapper extends HttpServletRequestWrapper {

    private byte[] body;

    public CachingServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (body == null) {
            body = IOUtils.toByteArray(super.getInputStream());
        }
        return new CachingServletInputStream(new ByteArrayInputStream(body));
    }

    @Override
    public String getCharacterEncoding() {
        String enc = super.getCharacterEncoding();
        return (enc != null ? enc : StandardCharsets.UTF_8.name());
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
    }

    public static class CachingServletInputStream extends ServletInputStream {

        private final ByteArrayInputStream is;

        public CachingServletInputStream(ByteArrayInputStream is) {
            this.is = is;
        }

        @Override
        public int read() {
            return is.read();
        }

        @Override
        public boolean isFinished() {
            return true;
        }

        @Override
        public boolean isReady() {
            return is.available() <= 0;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }

    }
}
  1. 过滤器实现
public class FundsCachingStreamRequestFilter implements Filter {
    private static final Set<String> SUPPORT_METHODS = ImmutableSortedSet.of("POST");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            if (shouldCache(httpServletRequest, servletResponse)) {
                //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
                requestWrapper = new CachingServletRequestWrapper(httpServletRequest);

                /*
                 * 重置RequestContextHolder.setRequestAttributes,
                 * 因为缓存了stream后,在CommonWebScope里(CookieInfoInjectFilter和APIRequestInfoInjectFilter会用到)
                 * HttpServletRequest request = ((ServletRequestAttributes) currentRequestAttributes()).getRequest();
                 * try (InputStream input = request.getInputStream()) {
                 * 这里又取了当前最原始的request,就导致了获取不到inputstream
                 * */
                httpServletRequest = (HttpServletRequest) requestWrapper;
                HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
                RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(httpServletRequest, httpServletResponse));
            }
        }

        // 在chain.doFiler方法中传递新的request对象
        if (requestWrapper == null) {
            requestWrapper = servletRequest;
        }

        filterChain.doFilter(requestWrapper, servletResponse);
    }

    @Override
    public void destroy() {

    }

    private boolean shouldCache(HttpServletRequest servletRequest, ServletResponse servletResponse) {
        return true;
        // 只有 SUPPORT_METHODS 中的请求,并且 Content-Type 不是 multipart 时才会包装,避免大文件上传时过高的占用内存
//        if (SUPPORT_METHODS.stream()
//                .anyMatch(method -> equalsIgnoreCase(method, servletRequest.getMethod()))) {
//            return !isMultipart(servletRequest);
//        }
//        return false;
    }
}
  1. 过滤器注入

顺序要尽量靠前,否则如果先被其他读了,就读不到没法再缓存了。

    /**
     * 用来解决重复获取getInputStream
     */
    @Bean
    public FilterRegistrationBean fundsCachingStreamRequestFilterRegistration() {
        FundsCachingStreamRequestFilter fundsCachingStreamRequestFilter = new FundsCachingStreamRequestFilter();
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(fundsCachingStreamRequestFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("fundsCachingStreamRequestFilter");
        registrationBean.setOrder(2);
        return registrationBean;
    }