SpringBoot interceptor – solve java.io.IOException: Stream closed problem

1. What is the SpringBoot interceptor

SpringBoot interceptors and filters are a mechanism of Spring Boot, which are used to intercept requests and responses, and are a manifestation of AOP programming. This method can intercept, filter and change some operations of SpringBoot without changing the basic business and logic of the code.

The operation of SpirngBoot interceptor (Interceptor) and filter (Filter) is shown in the figure below. If you don’t want to understand carefully, you can also remember this execution step: Filter (Filter) –> Interceptor (Interceptor) –> Controller Adavice –> Controller.

2. AOP programming

SpringBoot is the embodiment of the idea of AOP programming. AOP programming is aspect-oriented programming, which is an ideological embodiment of functional programming. There is no conflict between AOP programming and OOP programming. The idea of OOP programming is encapsulation, inheritance and polymorphism, while the idea of AOP programming is to provide function extensions without changing the original interface and functions.

Generally speaking, the realization idea of AOP programming is to provide a proxy, which separates some common functions such as permission check, log, transaction, etc. from each business method, and provides a unified implementation of the proxy.

SpringBoot interceptors and filters are the embodiment of AOP programming, that is, to unify some functions for unified interception, filtering and implementation.

3. Request interception and return interception

The request and return can be intercepted uniformly.

Generally speaking, if it is a very simple operation, you can use Controller Adavice to intercept it. code show as below:

@ControllerAdvice
public class TestResponseBodyAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        Class targetClass = returnType.getMethod().getDeclaringClass();
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //operate

        return body;
    }
}

If it is a more complicated operation, it can be intercepted with an interceptor (Interceptor), the code is as follows:

@Component
public class TokenInterceptor implements HandlerInterceptor {
    /**
     * This method will be called before accessing a method of Controller.
     *
     * @param request
     * @param response
     * @param handler
     * @return false means not to execute the postHandle method, true means to execute the postHandle method
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //false means that the postHandle method is not executed, the next chain chain is not executed, and the response is returned directly
        if(/***related operations***/) {
            return false;
        } else {
            return true;
        }
    }
}

The gist of the method is:

1) You can choose preHandel and psotHanle operations by yourself;

2) This interceptor needs to be registered in the Bean;

The registration code (under Configuration injection) is as follows:

@Autowired
TokenInterceptor tokenInterceptor;

/**
 * Add interceptor
 * @param registry
 */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry. addInterceptor(tokenInterceptor)
.addPathPatterns("/api/**")//Specify the url intercepted by this class
.excludePathPatterns( "/static/**");//Filter static resources
}

2) If there is an operation on the request, it will cause java.io.IOException: Stream closed;

4.Java.io.IOException: Stream closed problem solving

The reason for Java.io.IOException: Stream closed is that the HTTP Stream only allows one operation, and if the operation is performed multiple times, it will fail.

The solution to this problem is: provide a replication flow, and let the operation be based on the replication flow.

The solution code is as follows (the original interceptor remains unchanged):

1) Provide file parsing:

public class HttpHelper {
    public static String getBodyString(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request. getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset. forName("UTF-8")));
            String line = "";
            while ((line = reader. readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream. close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader. close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}

2) Provide Filter filtering:

public class HttpServletRequestReplacedFilter implements Filter {
    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);
        }

        //Get the stream in the request, convert the extracted string into a stream again, and put it into the new request object.
        // Pass the new request object in the chain.doFiler method
        if(requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

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

3) Decoration of the request, used to establish the replication stream.

public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    public RequestReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
    }

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

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais. read();
            }

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

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}

4) Add relevant operations to the Bean operation (Configuration injection):

@Bean
public FilterRegistrationBean httpServletRequestReplacedRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new HttpServletRequestReplacedFilter());
registration.addUrlPatterns("/*");
registration.addInitParameter("paramName", "paramValue");
registration.setName("httpServletRequestReplacedFilter");
registration. setOrder(1);
return registration;
}

The most critical part of the solution to this problem is actually in HttpServletRequestReplacedFilter

if(requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}

This is what makes copying the stream work, avoiding the stream being closed.