Spring Boot中IOUtils读取请求体不全的问题解决
问题根源
Servlet的请求输入流是一次性读取的,Spring MVC在尝试解析请求体到目标对象时,已经读取过一次输入流了,所以你在异常处理器里再次调用getInputStream()只能读取到剩余的部分,自然会出现内容截断。
解决方案:提前缓存请求体
通过自定义请求包装类和过滤器,在Spring解析请求前就把请求体缓存起来,这样异常处理器可以重复读取完整内容。
1. 自定义请求包装类(缓存请求体)
import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import org.apache.commons.io.IOUtils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); // 读取并缓存原始请求体 this.cachedBody = IOUtils.toByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() throws IOException { // 返回缓存的输入流,支持重复读取 return new CachedServletInputStream(this.cachedBody); } private static class CachedServletInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public CachedServletInputStream(byte[] cachedBody) { this.inputStream = new ByteArrayInputStream(cachedBody); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) {} @Override public int read() throws IOException { return inputStream.read(); } } // 直接获取缓存的请求体字符串 public String getCachedBodyAsString() { return new String(this.cachedBody, StandardCharsets.UTF_8); } }
2. 实现过滤器,包装请求
import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; public class CachedBodyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { // 将原始请求包装为缓存请求 CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest((HttpServletRequest) request); chain.doFilter(cachedRequest, response); } else { chain.doFilter(request, response); } } }
3. 注册过滤器使其生效
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<CachedBodyFilter> cachedBodyFilter() { FilterRegistrationBean<CachedBodyFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CachedBodyFilter()); // 指定需要拦截的接口路径,根据实际业务调整 registrationBean.addUrlPatterns("/api/*"); // 设置优先级,确保在Spring MVC解析请求前执行 registrationBean.setOrder(1); return registrationBean; } }
4. 修改异常处理器,读取缓存的请求体
@ExceptionHandler(value = { MethodArgumentNotValidException.class, HttpMessageNotReadableException.class }) public ResponseEntity<ErrorMessage> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex, WebRequest req) { String output = ""; if (req instanceof ServletWebRequest servletWebRequest) { HttpServletRequest request = servletWebRequest.getRequest(); // 从缓存请求中获取完整请求体 if (request instanceof CachedBodyHttpServletRequest cachedRequest) { output = cachedRequest.getCachedBodyAsString(); System.out.println(output); } } ErrorMessage errorMessage = ErrorMessage.builder() .operationId(Utils.generateId()) .status(RequestOperationStatus.ERROR.name()) .message(ex.getMessage()) .createdAt(new Date()) .data(output) .build(); // JSON解析错误属于客户端请求格式问题,返回400更符合HTTP语义 return new ResponseEntity<>(errorMessage, new HttpHeaders(), HttpStatus.BAD_REQUEST); }
注意事项
- 如果请求体体积过大(如超过50MB),内存缓存可能引发OOM,这种情况可以修改包装类,将请求体缓存到临时文件中。
- 过滤器的
urlPatterns要根据实际业务接口路径调整,避免不必要的请求缓存。
内容的提问来源于stack exchange,提问作者davidvera
相关产品推荐
相关产品推荐

