当前位置: 首页 > article >正文

HttpSevletRequest Body信息不能被多次读取的问题

        在 Java Web 开发中,HTTP 请求体是客户端向服务器发送数据的主要载体,例如表单提交、JSON 数据等。当服务器收到请求后,通常通过 HttpServletRequest 类获取请求体的内容。然而,HTTP 请求体通常只能被读取一次。这是因为请求体使用输入流(InputStream)进行读取,一旦读取完毕,流的位置就会到达流的末尾,无法再读取。因此,在开发中,如果不小心读取了请求体一次,后续的代码将无法访问请求体数据,从而导致数据丢失或者异常。

1. 问题背景

HTTP 请求体的读取限制

HTTP 请求体(如 POST 请求中的 JSON 数据或表单数据)是通过输入流(InputStream)传输的。Servlet 容器在接收到请求时,会将输入流读取并解析到内存中,然后进行后续的处理。但是,HTTP 请求体的流一次性读取特性意味着:

  1. 只能读取一次:一旦读取完请求体的数据,流的位置就会指向末尾,无法再次读取同一数据。
  2. 重复访问时失败:如果在请求处理过程中,多个组件或方法需要访问请求体数据,但该数据已经被读取过一次,那么后续的读取操作将无法成功,通常会抛出异常或返回空数据。

这种问题在需要多次读取请求体的场景中尤为明显。例如,在拦截器或过滤器中读取请求体数据时,后续的业务处理方法(如 Controller)可能无法再访问请求体。

2. 问题产生的场景

以下是一些常见的导致请求体只能读取一次的场景:

  • 使用 Servlet 读取请求体:当使用 HttpServletRequest 获取请求体时,流会被消耗,无法再次获取。
  • Spring MVC 中的 @RequestBody:Spring MVC 提供了 @RequestBody 注解,用于直接将请求体映射为 Java 对象。但一旦请求体被映射,流就会被消耗,导致后续无法再访问请求体数据。
  • 自定义过滤器或拦截器:在一些需要多次读取请求体的场景下,过滤器或拦截器读取请求体后,后续代码无法再次访问请求体数据。

3. 解决方案

使用 HttpServletRequestWrapper 包装请求体

通过创建一个自定义的 HttpServletRequestWrapper 来缓存请求体的数据,并提供多次读取的能力。这种方式允许请求体数据在第一次读取时被缓存,后续的请求处理流程可以从缓存中读取数据。

import cn.hutool.extra.servlet.ServletUtil;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 *  Request Body 缓存 Wrapper
 *
 * @author wingzingliu
 */
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {

    /**
     * 缓存的内容
     */
    private final byte[] body;

    public CacheRequestBodyWrapper(HttpServletRequest request) {
        super(request);
        body = ServletUtil.getBodyBytes(request);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
        // 返回 ServletInputStream
        return new ServletInputStream() {

            @Override
            public int read() {
                return inputStream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {}

            @Override
            public int available() {
                return body.length;
            }

        };
    }

}
创建过滤器
import cn.hutool.core.util.StrUtil;
import org.springframework.http.MediaType;
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;

/**
 * Request Body 缓存 Filter,实现它的可重复读取
 *
 * @author wingzingliu
 */
public class CacheRequestBodyFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        filterChain.doFilter(new CacheRequestBodyWrapper(request), response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // 只处理 json 请求内容
        return !isJsonRequest(request);
    }
  
  	public static boolean isJsonRequest(ServletRequest request) {
        return StrUtil.startWithIgnoreCase(request.getContentType(), 		MediaType.APPLICATION_JSON_VALUE);
    }
}
自动配置类
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;
/**
 * @author wingzingliu
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
  
    /**
     * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
     */
    @Bean
    public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
        return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
    }
  
  	public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
        bean.setOrder(order);
        return bean;
    }
}
/**
 * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
 *
 *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
 *
 * @author wingzingliu
 */
public interface WebFilterOrderEnum {

  	...

    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;

    ...

}


http://www.kler.cn/a/442778.html

相关文章:

  • 小结:华为路由器常用的操作指令
  • 【Logstash03】企业级日志分析系统ELK之Logstash 过滤 Filter 插件
  • stb_image简单使用
  • ElasticSearch在Windows环境搭建测试
  • 金融项目实战 04|JMeter实现自动化脚本接口测试及持续集成
  • 51单片机 和 STM32 的烧录方式和通信协议的区别
  • Java-注解
  • ARM/Linux嵌入式面经(五七):东方微电
  • AIGC魔性视频创作教程,即梦AI、海螺AI、混元大模型、通义万相
  • 低代码/无代码开发平台下的API接口创新实践
  • 请求三方http工具
  • ElasticSearch08-分析器详解
  • SQL 自然连接(Natural Join)详解
  • 物理信息神经网络(PINN)八课时教案
  • 深度学习在日志分析中的应用:智能运维的新前沿
  • C#调用Python脚本的方式(一),以PaddleOCR-GUI为例
  • 【FFmpeg 教程】给视频加字幕
  • 机器学习周报(12.9-12.15)
  • LF CRLF
  • 微积分复习笔记 Calculus Volume 2 - 4.3 Separable Equations
  • go面试问题
  • 利用git上传项目到GitHub
  • CSS 语法
  • 遇到“REMOTE HOST IDENTIFICATION HAS CHANGED!”(远程主机识别已更改)的警告
  • VSCode下的编译、调试、烧录
  • SQL Server 解决游标性能问题的替代方案