文章目录
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 技术,在需要处理请求体的地方强制使用包装后的请求对象。