Enabling Multiple @RequestBody Parameters in Spring MVC by Caching the Request Body
This article explains why Spring MVC cannot parse multiple @RequestBody annotations on a single handler method, analyzes the internal I/O stream closure that causes the failure, and provides a practical solution using a request‑body caching wrapper and a servlet filter to allow repeated reads of the request payload.
Spring MVC normally rejects the use of more than one @RequestBody annotation on a single controller method because the underlying input stream is consumed and closed after the first read, leaving the second parameter with a null body and resulting in a 400 error.
The root cause lies in EmptyBodyCheckingHttpInputMessage which, after the first invocation of readWithMessageConverters , sets the internal body field to null as the I/O stream has already been exhausted. Consequently, the second @RequestBody cannot be resolved.
To overcome this limitation, the article proposes caching the request payload so that it can be read multiple times. A custom HttpServletRequestWrapper named CacheRequestBodyContent reads the original input stream into a byte array, then overrides getReader and getInputStream to supply a fresh stream backed by the cached bytes.
public class CacheRequestBodyContent extends HttpServletRequestWrapper {
private byte[] body = null;
public CacheRequestBodyContent(HttpServletRequest request, ServletResponse response) {
super(request);
try {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
body = IoUtil.readBytes(request.getInputStream(), false);
} catch (Exception e) {
log.error("请求数据读取失败,请重试");
}
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream cache = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException { return cache.read(); }
@Override
public int available() throws IOException { return body.length; }
// ... other methods omitted
};
}
}A servlet Filter called CacheRequestFilter wraps incoming HttpServletRequest objects with this wrapper, ensuring that downstream handlers receive the cached request body.
public class CacheRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new CacheRequestBodyContent((HttpServletRequest) request, response);
}
if (requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
}The filter is registered via a Spring configuration class, applying it to all URL patterns.
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CacheRequestFilter());
registration.addUrlPatterns("/*");
registration.setName("requestFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
}After adding the wrapper and filter, the same endpoint (e.g., http://localhost:8080/users/duplicate ) successfully processes a controller method with two @RequestBody parameters, returning the combined DTO without errors, thereby solving the duplicate @RequestBody parsing issue in Spring MVC.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.