springboot 解决过滤器 HttpServletRequest body IO流只能读取一次的问题

 更新时间:2026年05月19日 09:43:25   作者:java叶新东  
本文主要介绍了在过滤器中打印请求body参数的难点及其原因,并提供了一种解决方案:使用包装类缓存流数据,这样可以在过滤器中打印body内容的同时,不影响控制器获取body参数,感兴趣的可以了解一下

问题

老手都知道过滤器的作用,但是如果想要打印请求的参数就有点难度了, url 上拼接的参数可以轻而易举的得到,调用 request.getParameterMap() 方法就可以拿到;

但是要拿body 体就没这么简单了,request 里面有个 getInputStream(),这个流 是一次性的, 如果你在这里读出来,那么在控制层就拿不到body参数了;

错误方式:直接读取原始流打印(导致控制器无法获取)

如果在过滤器中直接调用原始请求的 getInputStream() 或 getReader() 打印,会直接消耗流,导致控制器后续读取时无数据:

// 错误示例:直接读取原始流,导致流被消耗

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    // 直接读取原始流并打印(错误!)
    String body = IOUtils.toString(httpRequest.getInputStream(), StandardCharsets.UTF_8);
    System.out.println("Filter打印body:" + body);
    // 此时流已被消耗,控制器无法读取
    chain.doFilter(request, response); 
}

方案

问题本质:输入流的 “一次性” 特性
ServletRequest 的输入流(getInputStream()/getReader())是单向流动的字节流,默认情况下,一旦读取(如在过滤器中打印),流的指针会移动到末尾,后续(如控制器)再读取时就会获取到空数据。
因此,问题的核心不是 “是否在过滤器中打印”,而是 “打印时是否消耗了原始流且未缓存”。

正确方式:通过包装类缓存后打印(控制器可正常获取)

如果先通过 HttpServletRequestWrapper 缓存流数据,再在过滤器中读取缓存的数据打印,原始流不会被消耗,控制器可通过包装类的缓存读取;

过滤器内容

@Component
@0rder(o)
@Slf4j
public class WebFilter extends OncePerRequestFilter {

@0verride
protected void dofilterInternal(HttpservletRequest request, HttpservletResponse response, Filterchain filterchain)throws servletException, I0Exception{
// 正确示例:基于包装类的缓存打印,不消耗原始流
    if (request instanceof HttpServletRequest) {
        // 先包装请求,缓存body
        CachedServletRequestWrapper wrappedRequest = new CachedServletRequestWrapper((HttpServletRequest) request);
        // 从包装类的缓存中读取并打印(不影响原始流)
        String body = new String(wrappedRequest.getCachedBody(), StandardCharsets.UTF_8);
        System.out.println("Filter打印body:" + body);
        // 传递包装后的请求,控制器可从缓存中读取
        chain.doFilter(wrappedRequest, response);
    } else {
        chain.doFilter(request, response);
    }
}

CachedServletRequestWrapper.java

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.nio.charset.StandardCharsets;

/**
 * 缓存请求体的包装类,解决ServletRequest输入流只能读取一次的问题
 */
public class CachedServletRequestWrapper extends HttpServletRequestWrapper {

    // 缓存的请求体字节数组
    private final byte[] cachedBody;

    /**
     * 构造方法:读取原始请求的输入流并缓存
     */
    public CachedServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.cachedBody = readRequestBody(request);
    }

    /**
     * 读取原始请求的输入流并转换为字节数组
     */
    private byte[] readRequestBody(HttpServletRequest request) throws IOException {
        // GET/DELETE等方法通常没有请求体,直接返回空数组
        String method = request.getMethod();
        if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
            return new byte[0];
        }

        // 读取输入流并缓存
        try (InputStream inputStream = request.getInputStream();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            return outputStream.toByteArray();
        }
    }

    /**
     * 重写getInputStream(),返回基于缓存的输入流
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true; // 同步读取时始终就绪
            }

            @Override
            public void setReadListener(ReadListener listener) {
                // 异步场景可忽略,同步场景无需实现
            }
        };
    }

    /**
     * 重写getReader(),基于缓存的输入流构建字符读取器
     */
    @Override
    public java.io.BufferedReader getReader() throws IOException {
        return new java.io.BufferedReader(
            new java.io.InputStreamReader(getInputStream(), StandardCharsets.UTF_8)
        );
    }

    /**
     * 提供获取缓存的请求体字节数组的方法(方便过滤器/拦截器直接使用)
     */
    public byte[] getCachedBody() {
        return cachedBody;
    }

    /**
     * 提供获取缓存的请求体字符串的方法(简化使用)
     */
    public String getCachedBodyAsString() {
        return new String(cachedBody, StandardCharsets.UTF_8);
    }
}

可以看到,正常打印了body数据

到此这篇关于springboot 解决过滤器 HttpServletRequest body IO流只能读取一次的问题的文章就介绍到这了,更多相关springboot HttpServletRequest body IO读取一次内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java——Byte类/包装类的使用说明

    java——Byte类/包装类的使用说明

    这篇文章主要介绍了java——Byte类/包装类的使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • SpringBoot3 整合Docker-Compose的实现步骤

    SpringBoot3 整合Docker-Compose的实现步骤

    本文主要介绍了SpringBoot3 整合Docker-Compose的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-08-08
  • Java使用FastExcel实现合并单元格

    Java使用FastExcel实现合并单元格

    FastExcel 是一个采用纯 java 开发的 excel 文件读写组件,支持 Excel'97(-2003)(BIFF8)文件格式,本文主要介绍了如何使用FastExcel实现合并单元格,需要的可以参考下
    2024-12-12
  • JDK自带监控工具jstat、jmap、jstack的使用指南(附命令示例)

    JDK自带监控工具jstat、jmap、jstack的使用指南(附命令示例)

    jstat是JDK中提供的一个命令行工具,主要用来打印JVM 性能数据相关的统计数据,这篇文章主要介绍了JDK自带监控工具jstat、jmap、jstack的使用指南,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2026-01-01
  • Springboot通过aop实现事务控制过程解析

    Springboot通过aop实现事务控制过程解析

    这篇文章主要介绍了Springboot通过aop实现事务控制过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • ES6学习笔记之新增数据类型实例解析

    ES6学习笔记之新增数据类型实例解析

    这篇文章主要介绍了ES6学习笔记之新增数据类型,结合实例形式分析了ES6数据解构赋值、新增数据类型Set集合、新增数据类型Map、Symbol类型等相关原理与操作注意事项,需要的朋友可以参考下
    2020-01-01
  • springboot中将日志信息存储在catalina.base中过程解析

    springboot中将日志信息存储在catalina.base中过程解析

    这篇文章主要介绍了springboot中将日志信息存储在catalina.base中过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • 如何在spring事务提交之后进行异步操作

    如何在spring事务提交之后进行异步操作

    这篇文章主要为大家介绍了如何在spring事务提交之后进行异步操作,这些异步操作必须得在该事务成功提交后才执行,回滚则不执行,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2023-09-09
  • Spring Data JPA实现分页Pageable的实例代码

    Spring Data JPA实现分页Pageable的实例代码

    本篇文章主要介绍了Spring Data JPA实现分页Pageable的实例代码,具有一定的参考价值,有兴趣的可以了解一下
    2017-07-07
  • JAVA时间类型转换处理方式

    JAVA时间类型转换处理方式

    本文详细介绍了Java中LocalDate与Date类型之间的转换方法,以及字符串格式化与解析技巧,同时,还涵盖了LocalDateTime和LocalTime与Date类型的转换,感兴趣的朋友跟随小编一起看看吧
    2026-02-02

最新评论