Spring的HandlerInterceptor拦截器中重复获取Post方法的请求体数据方法

avatar
作者
筋斗云
阅读量:0

文章目录

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.解决

如果你需要在拦截器中读取请求体,但又希望后续处理能够再次访问请求体数据,可以采用以下方法:

  1. 缓存请求体数据:在拦截器中将请求体数据读取并缓存,然后再使用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 关键点

  1. preHandle 方法:

    • 使用 CachedBodyHttpServletRequest 包装请求并缓存请求体数据。
    • 解析请求体 JSON 数据,进行相关逻辑判断。
    • 将缓存的请求体数据存储在请求属性中,以供 postHandle 方法使用。
  2. postHandle 方法:

    • 从请求属性中获取缓存的请求体数据。
    • 根据业务逻辑需求,处理特定的逻辑,例如在 queryCode 为 “A” 的情况下执行某些操作。
    • postHandle 方法中的操作可以包括修改响应数据、记录日志、进行额外的数据处理等。
    • 确保使用缓存的请求对象:
  3. 在整个拦截器和过滤器链中,确保使用 CachedBodyHttpServletRequest以便可以多次读取请求体数据

  4. 确保 CachedBodyHttpServletRequest 在第一次被读取时就已经包装,并且后续所有读取请求体的操作都使用此包装对象。

4.3 怎么确保在所有需要读取请求体的地方使用 CachedBodyHttpServletRequest,而不是原始的HttpServletRequest呢?

要确保在所有需要读取请求体的地方都使用 CachedBodyHttpServletRequest,可以采取以下措施:

  1. 全局替换请求对象:
    在拦截器或过滤器的开始处,统一用CachedBodyHttpServletRequest替换原始的HttpServletRequest,并将其传递到后续的所有处理中。
  2. 使用请求属性:
    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();             // 处理请求体数据         }     } }  

确保统一使用包装后的请求

  1. 在全局配置中进行处理: 统一在过滤器或拦截器的开始处使用包装后的请求对象,这样可以避免遗漏的情况。
  2. 代码审查和规范: 对于所有处理请求体的地方,确保代码规范中明确规定使用缓存后的请求对象。
  3. 使用自定义注解或拦截: 如果可能,使用自定义注解或 AOP 技术,在需要处理请求体的地方强制使用包装后的请求对象。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!