Spring的过滤器获取请求体中JSON参数,同时解决Controller获取不到请求体参数的问题。
文章目录
- 前言
- 一、需求场景描述
- 二、原因解析
- 三、自定义 `HttpServletRequestWrapper` 来保存数据解决Controller获取不到的问题。
- 四、案例(要注意的点)
前言
Spring的过滤器获取请求体中JSON参数,同时解决Controller获取不到请求体参数的问题。
一、需求场景描述
在我的开发的一个项目中,有一个需求就是将每个请求的参数,都记录在访问日志中,这样在出现问题时可以进行反查。
当时我就想去用过滤器来实现这个功能。 当请求来到过滤器时,会有一个Request参数,通过该参数就能获取到请求路径和请求参数等等相关内容。
对于GET请求、请求头参数,我们很容易去获取到,但是对于请求体,如POST请求传的JSON,那就没法获取到。我们需要去借助流,POST的请求是在请求体body中(body参数是以流形式存在的),我们可以通过获取到输入流来获取body,然后就可以获取到了!!!
// 获取请求体的输入流,读取请求体的内容
ServletInputStream inputStream = httpRequest.getInputStream();
// 使用 UTF-8 编码读取所有行并拼接成字符串
String bodyData = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
//输出
System.out.println(bodyData);
通过上面的方法,我们确实能在过滤器filter中获取到POST的JSON参数了,但是按照上面的方法实现的过滤器, 我们会发现,当请求经过过滤器来到Controller的时候,你再去获取请求体里面的参数,就会一直报错!核心原因就是请求体body已经被销毁!
二、原因解析
根据IDEA的DeBug跟踪分析,大致如下:
Spring Boot 他是通过 获取 request
的输入流(InputStream)来读取请求参数。InputStream
内部有一个 位置指针(position),用于标记当前读取到的位置。每次 读取数据,位置指针都会向前移动;当读取到末尾时,read()
方法会返回 -1
,表示数据已经读完。
如果想 重新读取 请求体数据,可以调用 inputStream.reset()
方法,这样位置指针会回到 上次调用 mark()
的位置(默认是 0
),从而可以从头开始读取。
由于 request
的输入流 只能读取一次,如果在 过滤器中 读取了请求体数据,但没有 重置流,那么到 Controller 层 时,输入流就已经被消费完了,导致请求参数无法再次获取。
上面就是原因。
所以,在过滤器中如果要多次使用请求体数据,通常会 缓存请求体,比如使用 ContentCachingRequestWrapper
或者自定义 HttpServletRequestWrapper
来保存数据。
这里我采用的就是通过自定义 HttpServletRequestWrapper
来保存数据。
三、自定义 HttpServletRequestWrapper
来保存数据解决Controller获取不到的问题。
解决问题核心思路:由于 InputStream
只能读取一次,如果在过滤器中读取了请求体数据,Spring Boot 后续就无法再次读取。
为了解决这个问题,我们可以 先把 InputStream
的数据缓存起来,然后将其 完整地传递下去,这样 Spring Boot 在后续处理请求时仍然可以读取到原始数据。
这里就需要用到 HttpServletRequestWrapper
(HttpServletRequest
的包装类)。它允许我们 自定义方法,比如将请求体数据 提前读取并存储,然后 重写 getInputStream()
方法,确保后续仍然可以读取请求体数据。这样,无论是 过滤器 还是 Controller,都可以多次读取请求体,而不会丢失数据。
我的代码截图如下:
代码如下:
public class RequestWrapper extends HttpServletRequestWrapper {
private final byte[] body; // 用于缓存请求体数据的字节数组
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取请求体数据并转换为字节数组进行存储,防止InputStream被消费后无法重复读取
body = StreamUtils.copyToByteArray(request.getInputStream());
}
// 获取请求体的字符串内容
public String getBodyString() {
return new String(body, StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() throws IOException {
// 返回包装后的 BufferedReader,使请求体可多次读取
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 用已缓存的字节数组创建新的输入流,确保请求体可重复读取
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read(); // 读取字节数据
}
@Override
public boolean isFinished() {
return bais.available() == 0; // 判断是否读取完毕
}
@Override
public boolean isReady() {
return true; // 随时可以读取
}
@Override
public void setReadListener(ReadListener readListener) {
// 这里不做特殊处理
}
};
}
}
通过保存一份流,就可实现在过滤器中能拿到JSON参数,同时Controller也不会丢失参数!!!
四、案例(要注意的点)
下面是我的代码,对于业务进行简化了,方便大家阅读。
我这里用POST发起请求,控制台打印的如下:
这样大家就可以处理自己的业务了!!!
OK!到这里结束!!!