文章目录

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

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

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