在 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 中文为乱码;
- 解决:
-
读取流时指定 UTF-8 编码:
new String(body, StandardCharsets.UTF_8); -
配置 Spring 全局编码:
yamlspring: http: encoding: charset: utf-8 enabled: true force: true
-
3. 400 Bad Request 异常(JSON 解析失败)
- 原因:包装后的流读取异常,或 JSON 格式错误;
- 解决:
- 确保
RepeatableReadServletRequestWrapper中getInputStream实现正确; - 用 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+)时,内存占用过高;
- 解决:
-
仅读取核心参数(如通过 RequestBodyAdvice 提取 token/userId),不解析完整 JSON;
-
限制请求体大小(Spring Boot 配置):
yamlspring: 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)
五、总结
- 核心方案 :
- 通用场景:通过
RepeatableReadServletRequestWrapper包装 Request,缓存输入流,让流可重复读取; - 轻量场景:通过
RequestBodyAdvice解析参数并缓存到RequestAttribute,拦截器直接获取;
- 通用场景:通过
- 关键注意 :
- Filter 必须早于拦截器执行(设置最高优先级);
- 读取流时指定 UTF-8 编码,避免中文乱码;
- 大 JSON 数据优先提取核心参数,不解析完整内容;
- 避坑关键 :
- 拦截器读取流后,必须保证 Controller 仍能读取,核心是「流可重复读取」;
- 不要在拦截器中多次读取流,一次读取后缓存数据复用。
按上述方案实现后,拦截器既能正常获取 JSON 参数,又不影响 Controller 的参数解析,完全解决「拦截器获取不到 POST JSON 参数」的问题。