Location>code7788 >text

HTTP request body can only be read once in SpringBoot project? Try this program

Popularity:437 ℃/2024-08-07 20:40:37

Description of the problem

In the development of Java projects based on Spring, you may need to repeatedly read the data in the HTTP request body , such as the use of interceptors to print the incoming information , etc., but when we repeatedly call getInputStream () or getReader (), we usually encounter error messages similar to the following:
image
The general idea is that getInputStream() has already been called for the current request. So why is this a problem?

Cause analysis

There are two main reasons, one is Java's own design, InputStream as a data pipeline itself only supports reading once, if you want to support repeated reading then you need to re-initialize; Second is the implementation of Request in the Servlet container, we take the default Tomcat as an example, you can find that in the Request there are two boolean type attributes, respectively, usingReader and usingInputStream, when calling getInputStream () or getReader () will check the value of the two attributes, and after the implementation of the corresponding attribute will be set to true, if the value of the variable in the check is already true, then it will be reported as above. If the value of the variable is already true at the time of checking, then the above error message will be reported.
image

prescription

Less viable option: simple and brute force reflection mechanism

Involving the modification of variables, our first thought is that there is no method provided to modify, but unfortunately usingReader and usingInputStream does not provide, so you want to use the process of modifying these two attributes can only rely on the reflection of the process of using the process of reflecting each time you call the usingReader and usingInputStream set to false each time you read out the contents of the initialization of the data stream back to the theoretical can be read again. usingInputStream after each call through reflection will be usingReader and usingInputStream set to false, each time according to read out the contents of the data stream initialized back, theoretically, you can read again.

First of all, the reflection mechanism itself is to achieve dynamic modification by destroying the encapsulation of the class, a little too rough, and the second is also the main reason, we can only deal with the code for our own implementation, the framework itself, if you call getInputStream () and getReader (), we can not be interfered with through this approach, so this program is not feasible in giving the Spring Web project is not feasible.

Theoretically feasible solution: HttpServletRequest interface

HttpServletRequest is an interface, in theory we only need to create an implementation class can customize the behavior of getInputStream () and getReader (), naturally, can also solve the problem of RequestBody can not be repeated to read the problem, but the problem with this scheme is that HttpServletRequest has 70 methods, and we only need to modify two of them only, through this way to solve the problem a little bit more than lost.

Possible solutions for some scenarios: ContentCachingRequestWrapper

Spring itself provides a Request wrapper class to deal with the problem of repeated reads , that is, ContentCachingRequestWrapper, the idea of its implementation is to read the RequestBody will be cached in memory to its internal byte stream , subsequent reads can be obtained by calling the getContentAsString() or getContentAsByteArray() to get the cached content.

The reason why this option is part of the scenario feasible is mainly two aspects, one is ContentCachingRequestWrapper did not rewrite the getInputStream () and getReader () methods, so the framework where these two methods are used still can not get the cache down the content, and only support for custom business logic; The second point is related to the first point, because it does not modify the getInputStream() and getReader() methods, so we can only use the ContentCachingRequestWrapper after using the RequestBody annotation, otherwise there will be a RequestBody annotation modification of the parameter cannot read the request body properly, which limits its scope of use as shown in the following figure:
image

If you only need to read the content of the request body again after the business code, then the use of ContentCachingRequestWrapper is also sufficient to meet the needs of the specific use of the instructions in the next section.

Current Best Practice: Inherit HttpServletRequestWrapper

Previously we mentioned the implementation of HttpServletRequest need to implement 70 methods, so it is unlikely to implement their own, this solution is an advanced version of the inheritance of HttpServletRequest implementation of the class, and then customize the two methods we need to modify.

HttpServletRequest as an interface, there will certainly be its implementation to support its business functions, because the choice of Servlet containers more, we can not use a party to provide the implementation, so the scope of the choice is also limited to the Java EE (now called Jakarta EE) standard scope, by looking at the HttpServletRequest implementation , you can find within the standard provides a wrapper class : HttpServletRequestWrapper, our program is also around it .

train of thought

  1. Custom subclass that inherits HttpServletRequestWrapper and caches RequestBody to a custom property in the constructor method of the subclass.
  2. Customize the business logic of getInputStream() and getReader(), no longer check usingReader and usingInputStream, and read the cached content when calling.
  3. Custom Filter to replace the default HttpServletRequest with a custom wrapper class.

Code Showcase

  1. Inherit HttpServletRequestWrapper, implement subclass CustomRequestWrapper, and customize the business logic of getInputStream() and getReader()
// 1.predecessorHttpServletRequestWrapper
public class CustomRequestWrapper extends HttpServletRequestWrapper {

    // 2.definefinalcausality,Used to cache request body content
    private final byte[] content;

    public CustomRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 3.构造methodologies中将请求体内容缓存到内部causality中
         = (());
    }

    // 4.reconsidergetInputStream()
    @Override
    public ServletInputStream getInputStream() {
        // 5.Convert cached content to byte streams
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() {
                // 6.When reading the first5Step Initialized Byte Stream
                return ();
            }
        };
    }

    // 7.rewritegetReader()methodologies,Here the reuse ofgetInputStream()logic
    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}
  1. Custom Filter replaces the default HttpServletRequest with a custom CustomRequestWrapper.
// 1. Implement the Filter interface, here you can also choose to inherit the HttpFilter
public class RequestWrapperFilter implements Filter {
    // 2. Override or implement the doFilter method.
    Override the doFilter method
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 3. This judgment is to narrow the scope of the impact of the CustomRequestWrapper itself is only for HttpServletRequest, do not judge may affect other types of requests
        if (request instanceof HttpServletRequest) {
            // 4. Convert the default HttpServletRequest to a custom CustomRequestWrapper
            CustomRequestWrapper requestWrapper = new CustomRequestWrapper((HttpServletRequest) request); // 5.
            // 5. Pass the transformed request into the call chain
            (requestWrapper, response); } else { requestWrapper((HttpServletRequest))
        } else {
            (request, response); } else {
        }
    }
}
  1. Filter registration to the Spring container, this step can be executed in a variety of ways, here using the more traditional but more flexible Bean way of registration, if the figure can be convenient through the ServletComponentScan annotation + WebFilter annotation.
/**
 * Filter Configuration,Support for third-party filters
 */
@Configuration
public class FilterConfigure {
    /**
     * request body encapsulation
     * @return
     */
    @Bean
    public FilterRegistrationBean<RequestWrapperFilter> filterRegistrationBean(){
        FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
        (new RequestWrapperFilter());
        ("/*");
        return bean;
    }
}

At this point we can repeat the reading of the request body in the project, if you choose to use the ContentCachingRequestWrapper provided by Spring, then in the Filter will be CustomRequestWrapper replaced by ContentCachingRequestWrapper can be. However, you need to be aware of the small scope available as mentioned in the previous section.

The code within the article can be found at/itartisans/itartisans-framework, this is my open source a SpringBoot project scaffolding , I will add some general features from time to time , welcome to pay attention .