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

SpringBoot REST中如何在ExceptionHandler中获取请求体与查询参数

问题根因

所有尝试拿不到@RequestBody内容的核心原因只有一个:Servlet规范中HttpServletRequest的输入流仅支持读取一次,Spring MVC在执行Controller方法参数解析时,已经把请求体对应的流完全读取并反序列化为POJO对象,流读取完毕后会自动关闭,后续无论是直接调用request.getInputStream()、尝试注入请求POJO,拿到的都只会是空值。
同类方案失效,基本都是因为没有在Spring MVC读取请求流之前完成请求体缓存,等异常抛出时流已经没有剩余内容可读。

落地实现步骤

1. 实现请求体缓存逻辑

首先自定义可重复读的Request包装类,把请求体内容提前缓存到字节数组中:

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    private final byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = requestInputStream.readAllBytes();
    }

    @Override
    public ServletInputStream getInputStream() {
        return new CachedServletInputStream(new ByteArrayInputStream(cachedBody));
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
    }

    public byte[] getCachedBody() {
        return cachedBody;
    }

    private static class CachedServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream inputStream;

        public CachedServletInputStream(ByteArrayInputStream inputStream) {
            this.inputStream = inputStream;
        }

        @Override
        public boolean isFinished() {
            return inputStream.available() == 0;
        }

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

        @Override
        public void setReadListener(ReadListener readListener) {
            // 同步阻塞IO无需额外实现
        }

        @Override
        public int read() {
            return inputStream.read();
        }
    }
}

2. 注册最高优先级的缓存过滤器

必须保证过滤器在所有请求处理逻辑(包括Spring MVC参数解析、自定义拦截器、安全框架过滤器)之前执行,把原始Request替换为缓存后的包装对象:

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CachedBodyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 仅缓存带请求体的HTTP方法,减少不必要的内存占用
        if (request instanceof HttpServletRequest httpRequest
                && ("POST".equalsIgnoreCase(httpRequest.getMethod())
                || "PUT".equalsIgnoreCase(httpRequest.getMethod())
                || "PATCH".equalsIgnoreCase(httpRequest.getMethod()))) {
            CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(httpRequest);
            chain.doFilter(wrappedRequest, response);
            return;
        }
        chain.doFilter(request, response);
    }
}

如果项目使用Spring Security,需要额外在Security配置中将该过滤器添加到安全过滤器链最前端,避免安全框架提前读空流:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(new CachedBodyFilter(), UsernamePasswordAuthenticationFilter.class)
        // 保留原有Security配置即可
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

3. 在全局异常处理器中读取参数

在@ExceptionHandler标注的方法中,直接从包装后的Request对象中提取缓存的请求体;查询参数存储在URL中,不依赖请求流,可以直接通过Servlet原生方法获取,不需要额外缓存:

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Object handleBizException(Exception e, HttpServletRequest request) {
        // 读取缓存的请求体内容
        String requestBody = "";
        if (request instanceof CachedBodyHttpServletRequest cachedRequest) {
            requestBody = new String(cachedRequest.getCachedBody(), StandardCharsets.UTF_8);
            // 需要转POJO的话,直接用项目内的JSON工具反序列化requestBody即可
        }

        // 读取Query Param查询参数
        Map<String, String> queryParams = new HashMap<>();
        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            queryParams.put(paramName, request.getParameter(paramName));
        }

        // 基于requestBody、queryParams组装自定义错误响应即可
        return null;
    }
}
避坑说明
  • 不要在@ExceptionHandler方法的参数列表中直接声明@RequestBody对应的POJO类,Spring不会为异常处理方法注入请求体参数,必然返回null
  • 注意Spring版本对应的Servlet包路径:Spring Boot 2.x对应javax.servlet.*,Spring Boot 3.x/Spring 6+对应jakarta.servlet.*,包导错会导致过滤器不生效
  • 大文件上传、流处理类接口不要走请求体缓存逻辑,会导致全量内容加载进内存引发OOM,可以在过滤器中通过请求路径、Content-Type做白名单/黑名单过滤
  • 如果存在其他自定义过滤器读取请求流,必须保证CachedBodyFilter的执行顺序在所有读流逻辑之前,否则缓存的内容为空

内容的提问来源于stack exchange,提问作者justina antony

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 21:33:25