
我为什么会发出这个疑问呢是因为我研究Web开发中的一个问题时HTTP请求体在Filter过滤器处被读取了之后在 Controller控制层就读不到值了使用 RequestBody 的时候。无论是字节流InputStream / OutputStream还是字符流Reader / Writer所有基于流的读取操作都会维护一个位置指针。初始状态下指针指向流的起始位置position 0每次调用read() / read(byte[]) / read(char[])等读取方法时指针会向后移动对应字节数当指针移动到流的末尾没有更多数据read() 方法会返回-1表示流读取完毕指针移动后不会自动回退也无法反向移动除非流显式支持重置因此再次读取只能得到-1。类比IO 流的读取过程就像用磁带播放器听磁带—— 磁头对应流的位置指针从磁带开头指针 0开始移动每读一个字节 / 字符磁头就往后走一步当磁头走到磁带末尾再继续播放读取就只能听到 沙沙声流返回-1并且磁头不会自动回到开头。当然不是所有流都只能读一次基于内存的流如ByteArrayInputStream / CharArrayReader支持重置指针因为它们的数据源是内存中的数组数据不会消失可以通过mark()和reset()方法将指针恢复到标记位置。需要注意调用reset()前必须先调用mark(int readlimit)不是所有流都支持mark() / reset()可以通过inputStream.markSupported()来进行判断。使用 mark() 和 reset() 方法// 仅适用于支持mark的流 public void processWithMark(InputStream input) throws IOException { if (!input.markSupported()) { throw new IOException(Mark not supported); } // 标记当前位置参数100表示最多可回退100字节 input.mark(100); // 第一次读取 byte[] firstRead new byte[50]; input.read(firstRead); System.out.println(First read: new String(firstRead)); // 重置到标记位置 input.reset(); // 第二次读取相同内容 byte[] secondRead new byte[50]; input.read(secondRead); System.out.println(Second read: new String(secondRead)); }使用包装类解决上文我们提到的HTTP请求体多次读取的问题public class MyRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; // 缓存请求体的字节数组 public MyRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 关键步骤在构造时一次性读取并存储原始请求流 body StreamUtils.copyToByteArray(request.getInputStream()); } // 提供一个便捷方法用于在过滤器中获取请求体内容例如记录日志 // 使用时直接调用 getBodyString() 即可 public String getBodyString() throws UnsupportedEncodingException { return new String(body, this.getCharacterEncoding()); } Override public ServletInputStream getInputStream() throws IOException { // 每次调用都返回一个基于缓存数据的新流 ByteArrayInputStream bais new ByteArrayInputStream(body); return new ServletInputStream() { Override public int read() { return bais.read(); } Override public boolean isFinished() { return bais.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener readListener) { // 无需实现 } }; } Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream(), this.getCharacterEncoding())); } }然后在过滤器处包装请求Slf4j Configuration public class RequestCachingFilterConfig { Bean public FilterRegistrationBean requestCachingFilter() { FilterRegistrationBean registrationBean new FilterRegistrationBean(); // 核心创建过滤器包装请求为 ContentCachingRequestWrapper registrationBean.setFilter(new OncePerRequestFilter() { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1. 仅包装 HTTP 请求排除 WebSocket 等 if (request instanceof HttpServletRequest !(request instanceof ContentCachingRequestWrapper)) { log.info(进入requestCachingFilter); // 2. 包装请求自动缓存请求体 MyRequestWrapper wrappedRequest new MyRequestWrapper(request); filterChain.doFilter(wrappedRequest, response); // 传递包装后的请求 } else { filterChain.doFilter(request, response); // 无需包装直接放行 } } }); // 3. 配置拦截所有请求可根据需求调整 URL 模式 registrationBean.addUrlPatterns(/*); registrationBean.setOrder(1); // 优先级最高确保先于其他过滤器执行 registrationBean.setName(requestCachingFilter); return registrationBean; } }