You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 14:35:27