拦截器获取不到 POST 请求 JSON 结构体参数(完整解决方案)

在 Spring/Spring Boot 中,拦截器(Interceptor)获取不到 POST 请求的 JSON 参数是高频问题,核心原因是JSON 请求体的输入流(InputStream)只能读取一次:DispatcherServlet 或 RequestBodyAdvice 会先读取流解析参数,导致拦截器中流已关闭/为空。

一、问题根源(先理解本质)

1. HTTP 请求体的输入流特性

POST 请求的 JSON 数据存储在 HttpServletRequest 的输入流(InputStream)中,该流是一次性的

  • 第一次读取后,流的指针会移到末尾,后续读取会返回空;
  • Spring 中,@RequestBody 注解的解析逻辑(RequestResponseBodyMethodProcessor)会先读取输入流,将 JSON 解析为实体类/Map,此时拦截器再读取流就会获取不到数据。

2. 拦截器与参数解析的执行顺序

Spring参数解析器 Controller Spring参数解析器(RequestBody) 拦截器(preHandle) 客户端 Spring参数解析器 Controller Spring参数解析器(RequestBody) 拦截器(preHandle) 客户端 发送POST(JSON)请求 放行 读取输入流,解析JSON为参数 传递解析后的参数 返回响应

✅ 结论:拦截器的 preHandle 执行时,输入流尚未被读取(可正常获取);但如果拦截器中读取了流,后续 Controller 的 @RequestBody 会获取不到参数;反之,若 Controller 先读取了流,拦截器就获取不到。

二、解决方案(分场景适配)

场景1:仅在拦截器中读取 JSON 参数(不影响 Controller)

核心思路:包装 HttpServletRequest,缓存输入流数据,让流可重复读取

步骤1:自定义 RequestWrapper(缓存输入流)
java 复制代码
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

/**
 * 包装 HttpServletRequest,缓存输入流,支持重复读取
 */
public class RepeatableReadServletRequestWrapper extends HttpServletRequestWrapper {
    // 缓存请求体字节数组
    private final byte[] body;

    public RepeatableReadServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 读取原始输入流,缓存到字节数组
        body = StreamUtils.copyToByteArray(request.getInputStream());
    }

    /**
     * 重写 getInputStream,返回缓存的字节数组生成的流
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {}

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }
}
步骤2:配置 Filter(替换原始 Request)

Filter 执行顺序早于拦截器,先将原始 HttpServletRequest 替换为包装后的对象,让输入流可重复读取:

java 复制代码
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 请求体缓存过滤器(让输入流可重复读取)
 */
@Component
@WebFilter(urlPatterns = "/*") // 拦截所有请求
public class RepeatableReadFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 仅处理 HTTP 请求,且是 POST + JSON 类型
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String contentType = httpRequest.getContentType();
            if ("POST".equals(httpRequest.getMethod()) 
                    && contentType != null 
                    && contentType.contains("application/json")) {
                // 替换为可重复读取的 Request
                request = new RepeatableReadServletRequestWrapper(httpRequest);
            }
        }
        // 继续执行过滤器链
        chain.doFilter(request, response);
    }
}
步骤3:拦截器中读取 JSON 参数
java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;

/**
 * 自定义拦截器(读取 JSON 参数)
 */
@Component
public class JsonParamInterceptor implements HandlerInterceptor {

    @Autowired
    private ObjectMapper objectMapper; // Spring 内置的 JSON 解析器

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 仅处理 POST + JSON 请求
        if ("POST".equals(request.getMethod()) 
                && request.getContentType() != null 
                && request.getContentType().contains("application/json")) {

            // 1. 读取请求体字节数组(从包装后的 Request 中读取)
            byte[] body = StreamUtils.copyToByteArray(request.getInputStream());
            String jsonStr = new String(body, StandardCharsets.UTF_8);
            System.out.println("【拦截器】读取到 JSON 参数:" + jsonStr);

            // 2. 解析为 Map(通用方式,无需实体类)
            if (!jsonStr.isEmpty()) {
                // 方法1:解析为 Map
                Map<String, Object> params = objectMapper.readValue(jsonStr, Map.class);
                String name = (String) params.get("name");
                Integer age = (Integer) params.get("age");
                System.out.println("【拦截器】解析参数:name=" + name + ",age=" + age);

                // 方法2:解析为自定义实体类(如 User)
                // User user = objectMapper.readValue(jsonStr, User.class);
                // System.out.println("【拦截器】User:" + user);
            }
        }
        // 放行,后续 Controller 仍能正常获取参数
        return true;
    }
}
步骤4:注册拦截器
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器注册配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private JsonParamInterceptor jsonParamInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,指定拦截路径
        registry.addInterceptor(jsonParamInterceptor)
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/login", "/static/**"); // 排除无需拦截的路径
    }
}

场景2:拦截器仅需获取少量核心参数(无需完整 JSON)

若只需获取 JSON 中的 1-2 个关键参数(如 token、userId),可通过 RequestAttribute 传递,避免重复解析流:

步骤1:自定义 RequestBodyAdvice(解析参数并缓存)
java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.lang.reflect.Type;

/**
 * 请求体增强:解析 JSON 参数并缓存到 RequestAttribute
 */
@ControllerAdvice
public class JsonParamAdvice implements RequestBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅处理 JSON 类型的请求体
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 1. body 是解析后的实体类/Map(如 User、Map<String,Object>)
        if (body instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) body;
            // 2. 提取核心参数,存入 RequestAttribute
            String token = (String) params.get("token");
            if (token != null) {
                // 获取当前请求,存入属性(拦截器中可通过 request.getAttribute 获取)
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                request.setAttribute("token", token);
            }
        } else if (body instanceof User) { // 若解析为实体类
            User user = (User) body;
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            request.setAttribute("userId", user.getId());
        }
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}
步骤2:拦截器中获取缓存的参数
java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 直接从 RequestAttribute 获取解析后的参数,无需读取流
    String token = (String) request.getAttribute("token");
    Long userId = (Long) request.getAttribute("userId");
    System.out.println("【拦截器】获取到 token:" + token + ",userId:" + userId);
    return true;
}

场景3:非 Spring Boot 原生场景(纯 Servlet 拦截器)

若项目未使用 Spring MVC,仅用 Servlet Filter/Interceptor,解决方案如下:

核心代码(Servlet Filter 缓存流)
java 复制代码
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

@WebFilter(urlPatterns = "/*")
public class ServletRepeatableReadFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if ("POST".equals(httpRequest.getMethod()) && httpRequest.getContentType().contains("application/json")) {
            // 读取流并缓存
            InputStream is = httpRequest.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            byte[] body = baos.toByteArray();
            // 包装 Request,重写 getInputStream
            HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
                @Override
                public InputStream getInputStream() {
                    return new ByteArrayInputStream(body);
                }
            };
            // 拦截器中读取 body 数组
            String jsonStr = new String(body, "UTF-8");
            System.out.println("【Servlet Filter】JSON 参数:" + jsonStr);
            chain.doFilter(wrappedRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }
}

三、避坑要点(高频问题)

1. 拦截器中读取流后,Controller 获取不到参数

  • 原因:未使用 RepeatableReadServletRequestWrapper,流被拦截器读取后耗尽;
  • 解决:必须通过 Filter 包装 Request,缓存输入流,让流可重复读取。

2. 中文乱码

  • 现象:拦截器读取的 JSON 中文为乱码;
  • 解决:
    1. 读取流时指定 UTF-8 编码:new String(body, StandardCharsets.UTF_8)

    2. 配置 Spring 全局编码:

      yaml 复制代码
      spring:
        http:
          encoding:
            charset: utf-8
            enabled: true
            force: true

3. 400 Bad Request 异常(JSON 解析失败)

  • 原因:包装后的流读取异常,或 JSON 格式错误;
  • 解决:
    1. 确保 RepeatableReadServletRequestWrappergetInputStream 实现正确;
    2. 用 JSON 校验工具检查请求体格式。

4. Filter 执行顺序问题

  • 现象:包装后的 Request 未生效,拦截器仍读取不到参数;

  • 原因:自定义 Filter 执行顺序晚于 Spring 的 CharacterEncodingFilter 等核心 Filter;

  • 解决:指定 Filter 执行顺序(@Order(Ordered.HIGHEST_PRECEDENCE)):

    java 复制代码
    @Component
    @WebFilter(urlPatterns = "/*")
    @Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级执行
    public class RepeatableReadFilter implements Filter {
        // ...
    }

5. 大 JSON 数据性能问题

  • 现象:拦截器读取大 JSON(如 10MB+)时,内存占用过高;
  • 解决:
    1. 仅读取核心参数(如通过 RequestBodyAdvice 提取 token/userId),不解析完整 JSON;

    2. 限制请求体大小(Spring Boot 配置):

      yaml 复制代码
      spring:
        servlet:
          multipart:
            max-request-size: 10MB
            max-file-size: 1MB

四、完整测试示例

1. 测试接口(Controller)

java 复制代码
@RestController
public class TestController {
    @PostMapping("/test/json")
    public String testJson(@RequestBody User user) {
        System.out.println("【Controller】获取到参数:" + user);
        return "success:" + user;
    }
}

// User 实体类
@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
}

2. 测试请求

复制代码
URL: http://localhost:8080/test/json
Method: POST
Header: Content-Type: application/json;charset=utf-8
Body: {"id":1,"name":"张三","age":20}

3. 预期输出

复制代码
【拦截器】读取到 JSON 参数:{"id":1,"name":"张三","age":20}
【拦截器】解析参数:name=张三,age=20
【Controller】获取到参数:User(id=1, name=张三, age=20)

五、总结

  1. 核心方案
    • 通用场景:通过 RepeatableReadServletRequestWrapper 包装 Request,缓存输入流,让流可重复读取;
    • 轻量场景:通过 RequestBodyAdvice 解析参数并缓存到 RequestAttribute,拦截器直接获取;
  2. 关键注意
    • Filter 必须早于拦截器执行(设置最高优先级);
    • 读取流时指定 UTF-8 编码,避免中文乱码;
    • 大 JSON 数据优先提取核心参数,不解析完整内容;
  3. 避坑关键
    • 拦截器读取流后,必须保证 Controller 仍能读取,核心是「流可重复读取」;
    • 不要在拦截器中多次读取流,一次读取后缓存数据复用。

按上述方案实现后,拦截器既能正常获取 JSON 参数,又不影响 Controller 的参数解析,完全解决「拦截器获取不到 POST JSON 参数」的问题。

相关推荐
Lyyaoo.1 小时前
Spring Boot日志
spring boot·缓存·单元测试
-Excalibur-2 小时前
IP数据包在计算机网络传输的全过程
java·网络·c++·笔记·python·网络协议·智能路由器
东离与糖宝2 小时前
JDK 26 HTTP/3原生客户端实战|高并发接口性能压测全流程
java·人工智能
番茄去哪了2 小时前
从0到1独立开发一个论坛项目(一)
java·数据库·oracle·maven
BioRunYiXue2 小时前
从现象到机制:蛋白降解调控研究的系统策略与实验设计
java·linux·运维·服务器·网络·人工智能·eclipse
希望永不加班2 小时前
如何在 SpringBoot 里自定义 Spring MVC 配置
java·spring boot·后端·spring·mvc
weixin199701080162 小时前
“迷你京东”全栈架构设计与实现
java·大数据·python·数据库架构
Welcome_Back2 小时前
SpringBoot后端开发测试全指南
spring boot·后端·log4j
东离与糖宝2 小时前
3月20日紧急修复|Spring AI双漏洞CVE-2026-22730/22729实战防护方案
java