背景介绍
我们才采用springboot构件项目的时候,往往会使用拦截器来做一些事情,比如做权限验证,做请求日志审计等。就会获取http的请求body。但是http的请求body是inputStream的,获取了一次滞后就不允许再获取了。如果在拦截器(HandlerInterceptor)中获取了,Controller层就没法获取了。所以RequestWrapper的思想出现了,spring 的官方提供了org.springframework.web.util.ContentCachingRequestWrapper类。
但是我想说"有问题",可能是我姿势不对。先介绍网上和大模型给出的常用解决方案。
网上和大模型的解决方案
过滤器(WrappingFilter )
WrappingFilter 在Fliter的时候引入spring官网的Wrapper
java
@Component
public class WrappingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(requestWrapper, responseWrapper);
if(!responseWrapper.isCommitted()){
responseWrapper.copyBodyToResponse();
}
}
}
拦截器(MyInterceptor)
再自定义拦截器,MyInterceptor
java
package com.shenyun.lyguide.config.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 拦截逻辑
Date date=new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=simpleDateFormat.format(date);
String uri=request.getRequestURI();
// 获取requestBody
String requestBody = "";
if(request instanceof ContentCachingRequestWrapper ) {
byte[] bytes=((ContentCachingRequestWrapper)request).getContentAsByteArray();
requestBody=new String(bytes, StandardCharsets.UTF_8);
}
//此次希望用requestBody,header,等验证签名,但是requestBody是null
System.out.println("Interceptor Request Body: " + requestBody);
System.out.printf("-----进入拦截器了%s,%s%n",time, uri);
// 继续处理请求
return true;
}
}
controller
java
@PostMapping("/test")
@ResponseBody
public String test(HttpServletRequest request,
HttpServletResponse response,
@RequestBody MyRequestDTO myRequestDTO){
String bodyJson=JSON.toJSONString(myRequestDTO);
System.out.println("controller print:"+bodyJson);
return "test";
}
原理解释
结论
byte[] bytes=((ContentCachingRequestWrapper)request).getContentAsByteArray();
在拦截器MyInterceptor中ContentCachingRequestWrapper的`getContentAsByteArray()`无法获取到requestBody。
解释
ContentCachingRequestWrapper的getContentAsByteArray方法只有在getInputStream读取了数据之后才能使用而获取到数据。
进一步跟踪
如果要使用getInputStream读取数据,那我就随ta的先读取一次
这是错误的方法,我只是好说明原理
java
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 拦截逻辑
Date date=new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=simpleDateFormat.format(date);
String uri=request.getRequestURI();
// 获取requestBody
String requestBody = "";
if(request instanceof ContentCachingRequestWrapper ) {
//故意从流中再读取数据
ServletInputStream inputStream =request.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; // 缓冲区大小为1024字节
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead); // 写入已读取的数据块
}
byte[] bytesTemp= byteArrayOutputStream.toByteArray();
String body=new String(bytesTemp, StandardCharsets.UTF_8);
System.out.println("Stream print:"+body);
//因为上面读取了一次,所以这儿可以使用getContentAsByteArray()方法获取
byte[] bytes=((ContentCachingRequestWrapper)request).getContentAsByteArray();
requestBody=new String(bytes, StandardCharsets.UTF_8);
}
//此次希望用requestBody,header,等验证签名,但是requestBody是null
System.out.println("Interceptor Request Body: " + requestBody);
System.out.printf("-----进入拦截器了%s,%s%n",time, uri);
// 继续处理请求
return true;
}
}
但是
在拦截器中故意读取了一次流,之后也可以使用getContentAsByteArray读取数据了。但是Controller层就报错了,Controller层需要读取数据映射到RequestBody注解的DTO对象上,底层是用inputStream中读取数据的。而,因为在拦截器中已经读取了一次流了,所以request的inputStream就无法获取数据了,因为流读取了之后只能使用ContentCachingRequestWrapper的getContentAsByteArray方法读取数据。
所以,没有达到效果,这就是ContentCachingRequestWrapper的巨坑。
下一篇文章,介绍spring boot拦截器获取requestBody的真实实践。