文章目录
- 1.需求
- 2.遇到的问题
- 3.解决
- 4.演示
- 4.1 代码实现
- 4.2 关键点
- 4.3 怎么确保在所有需要读取请求体的地方使用 `CachedBodyHttpServletRequest`,而不是原始的`HttpServletRequest`呢?
- 4.3.1 在过滤器中替换请求对象
- 4.3.2 在请求属性中存储包装的请求对象
1.需求
现在有一个实现了HandlerInterceptor的拦截器,现在要实现以下功能,拦截器只拦截指定两个接口进行处理,然后每个接口都有一个请求体参数,使用@RequestBody
传入,现在需要获取两个请求的请求体数据,请求体采用json传入,格式如下{"queryCode":"AAA","queryParams":{}}
,需要通过请求体参数的queryCode数据来执行处理逻辑。
2.遇到的问题
在Spring框架中,HandlerInterceptor
是一个用于拦截和处理HTTP请求的接口。当你在HandlerInterceptor
中读取了请求体(比如从HttpServletRequest
对象中获取输入流或读取请求体的内容),通常会影响后续其他过滤器或处理器读取请求体。
在 Servlet 请求中,HttpServletRequest 的 getReader() 方法或 getInputStream() 方法只能调用一次。这是因为HTTP请求请求体的输入流只能被读取一次,当一个过滤器或拦截器读取了输入流后,它的内容就被消耗掉了,再次读取时就会得到空数据。因此,如果在拦截器中读取了请求体,后续的处理流程(如控制器、其他过滤器或拦截器)将无法再次读取请求体,这可能导致请求体数据不可用的问题。
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
3.解决
如果你需要在拦截器中读取请求体,但又希望后续处理能够再次访问请求体数据,可以采用以下方法:
- 缓存请求体数据:在拦截器中将请求体数据读取并缓存,然后再使用
HttpServletRequestWrapper
来包裹原始的HttpServletRequest
,并提供重新读取缓存数据的能力。
4.演示
4.1 代码实现
自定义拦截器
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@Component
public class CustomInterceptor implements HandlerInterceptor {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 包装请求以缓存请求体
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
// 获取请求体并解析为JSON对象
String requestBody = cachedRequest.getBody();
MyRequestBody myRequestBody = objectMapper.readValue(requestBody, MyRequestBody.class);
// 缓存请求体数据到请求属性,供postHandle使用
request.setAttribute("cachedBody", requestBody);
// 判断queryCode
if ("A".equals(myRequestBody.getQueryCode())) {
// 在这里执行你需要的逻辑
}
return true; // 继续处理请求
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 从请求属性中获取缓存的请求体
String cachedBody = (String) request.getAttribute("cachedBody");
try {
MyRequestBody myRequestBody = objectMapper.readValue(cachedBody, MyRequestBody.class);
// 如果queryCode是A,执行特定操作
if ("A".equals(myRequestBody.getQueryCode())) {
// 在这里执行postHandle逻辑
// 例如,修改ModelAndView中的数据,记录日志等
System.out.println("PostHandle处理逻辑,queryCode为A");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
CachedBodyHttpServletRequest包装类,用于缓存请求体,以便可以多次读取请求体内容。
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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
/**
* 一个 HttpServletRequest 的包装类,用于缓存请求体,
* 以便可以多次读取请求体内容。
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
/**
* 构造一个新的 CachedBodyHttpServletRequest,包装原始请求并缓存其请求体数据。
*
* @param request 原始的 HttpServletRequest
* @throws IOException 在读取请求体时发生 I/O 错误
*/
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
this.cachedBody = toByteArray(request.getInputStream());
}
/**
* 将输入流读入字节数组中。
*
* @param input 要读取的输入流
* @return 包含输入流数据的字节数组
* @throws IOException 在读取时发生 I/O 错误
*/
private byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
return byteArrayOutputStream.toByteArray();
}
/**
* 返回一个从缓存的请求体读取数据的 ServletInputStream。
*
* @return ServletInputStream 对象
*/
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody);
}
/**
* 返回一个从缓存的请求体读取数据的 BufferedReader。
*
* @return BufferedReader 对象
*/
@Override
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream, StandardCharsets.UTF_8));
}
/**
* 返回缓存的请求体内容,作为字符串。
*
* @return 缓存的请求体字符串
*/
public String getBody() {
return new String(this.cachedBody, StandardCharsets.UTF_8);
}
/**
* 一个从字节数组中读取数据的 ServletInputStream 实现。
*/
private static class CachedBodyServletInputStream extends ServletInputStream {
private final InputStream cachedBodyInputStream;
/**
* 使用提供的字节数组构造一个新的 CachedBodyServletInputStream。
*
* @param cachedBody 要读取的字节数组
*/
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
/**
* 检查输入流是否已经读取完毕。
*
* @return 如果输入流读取完毕返回 true,否则返回 false
*/
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
return false;
}
}
/**
* 检查输入流是否可以读取。
*
* @return 如果输入流可以读取返回 true,否则返回 false
*/
@Override
public boolean isReady() {
return true;
}
/**
* 设置此输入流的 ReadListener。
*
* @param readListener 要设置的 ReadListener
*/
@Override
public void setReadListener(ReadListener readListener) {
// 未实现
}
/**
* 从输入流中读取下一个字节的数据。
*
* @return 下一个字节的数据,如果到达流的末尾则返回 -1
* @throws IOException 在读取时发生 I/O 错误
*/
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
}
}
请求体对象
public static class MyRequestBody {
private String queryCode;
private Object queryParams;
// Getters and setters
public String getQueryCode() {
return queryCode;
}
public void setQueryCode(String queryCode) {
this.queryCode = queryCode;
}
public Object getQueryParams() {
return queryParams;
}
public void setQueryParams(Object queryParams) {
this.queryParams = queryParams;
}
}
4.2 关键点
- preHandle 方法:
- 使用 CachedBodyHttpServletRequest 包装请求并缓存请求体数据。
- 解析请求体 JSON 数据,进行相关逻辑判断。
- 将缓存的请求体数据存储在请求属性中,以供 postHandle 方法使用。
- postHandle 方法:
- 从请求属性中获取缓存的请求体数据。
- 根据业务逻辑需求,处理特定的逻辑,例如在 queryCode 为 “A” 的情况下执行某些操作。
- postHandle 方法中的操作可以包括修改响应数据、记录日志、进行额外的数据处理等。
- 确保使用缓存的请求对象:
- 在整个拦截器和过滤器链中,确保使用
CachedBodyHttpServletRequest
以便可以多次读取请求体数据。 - 确保 CachedBodyHttpServletRequest 在第一次被读取时就已经包装,并且后续所有读取请求体的操作都使用此包装对象。
4.3 怎么确保在所有需要读取请求体的地方使用 CachedBodyHttpServletRequest
,而不是原始的HttpServletRequest
呢?
要确保在所有需要读取请求体的地方都使用 CachedBodyHttpServletRequest,可以采取以下措施:
- 全局替换请求对象:
在拦截器或过滤器的开始处,统一用CachedBodyHttpServletRequest
替换原始的HttpServletRequest
,并将其传递到后续的所有处理中。 - 使用请求属性:
将CachedBodyHttpServletRequest
存储在请求属性中,后续需要访问请求体的地方都从请求属性中获取该对象。
4.3.1 在过滤器中替换请求对象
假设你有多个过滤器,你可以在链条的最开始处将请求替换为 CachedBodyHttpServletRequest
:
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CachedBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 将原始请求包装为 CachedBodyHttpServletRequest
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
// 继续过滤器链,并将包装后的请求传递下去
filterChain.doFilter(cachedRequest, response);
}
}
4.3.2 在请求属性中存储包装的请求对象
在处理过程中,可以将CachedBodyHttpServletRequest
对象存储在请求属性中,供后续的处理器使用:
@Component
public class CustomInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 包装请求体以缓存请求数据
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
// 将包装的请求存储在请求属性中
request.setAttribute("cachedRequest", cachedRequest);
// 获取请求体或进行其他操作
String requestBody = cachedRequest.getBody();
// 继续处理请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 从请求属性中获取缓存的请求
CachedBodyHttpServletRequest cachedRequest = (CachedBodyHttpServletRequest) request.getAttribute("cachedRequest");
if (cachedRequest != null) {
String requestBody = cachedRequest.getBody();
// 处理请求体数据
}
}
}
确保统一使用包装后的请求
- 在全局配置中进行处理: 统一在过滤器或拦截器的开始处使用包装后的请求对象,这样可以避免遗漏的情况。
- 代码审查和规范: 对于所有处理请求体的地方,确保代码规范中明确规定使用缓存后的请求对象。
- 使用自定义注解或拦截: 如果可能,使用自定义注解或 AOP 技术,在需要处理请求体的地方强制使用包装后的请求对象。