ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java中HttpServletRequest读取请求体:三种方式与可重复读实践

Java中HttpServletRequest读取请求体:三种方式与可重复读实践 简介这份PDF资料面向Java Web开发人员聚焦于通过HttpServletRequest获取POST请求body内容的实现方法适合需要处理JSON提交、接口参数解析等场景的初中级开发者参考。资源包共1个PDF文件大小约38KB内容以代码示例与要点讲解为主结构紧凑便于快速查阅。目前已有34465人学习下载说明该知识点在实际开发中具有较高的关注度。资料围绕getInputStream()与BufferedReader配合读取body的核心思路展开并重点提示了调用getParameter()后IO流读取失效、body与URL参数读取顺序等易踩坑细节同时给出Servlet中doPost方法的完整示例代码帮助读者理解如何将客户端提交的body内容取出并用于后续业务逻辑处理。对于正在排查POST请求参数获取异常或希望规范接口参数读取方式的开发者这份资料能提供可直接参考的实现思路与避坑经验。1. HttpServletRequest 读 body为什么 getParameter 拿不到 JSON很多人第一次在 Controller 里写request.getParameter(name)发现 Postman 发的 JSON 里明明有name字段返回却是 null。这不是代码写错了而是 Servlet 规范里getParameter只解析application/x-www-form-urlencoded和multipart/form-data两种格式的请求体对application/json根本不碰。换句话说JSON 数据躺在输入流里getParameter看都不看一眼。这个标题要解决的就是这件事在 Java Web 项目里怎么从HttpServletRequest里把 POST 请求的 body 完整、安全、可重复地读出来。适合谁正在写 Spring Boot 接口但被 body 读取坑过的后端、做接口自动化测试需要手动解析请求的测试开发、以及维护老 Servlet 项目没法直接上RequestBody的工程师。读完你能掌握三种读法、知道每种读法的边界、以及为什么有些场景必须提前缓存 body。2. 三种读取方式从原生流到 Spring 封装2.1 原生 InputStream最底层但只能读一次Servlet 的HttpServletRequest继承自ServletRequest提供了getInputStream()和getReader()两个方法。前者返回字节流后者返回字符流两者互斥——调用了其中一个另一个就失效。这是 Servlet 规范明确规定的不是实现 bug。先看最原始的字节流读法import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; public class BodyReader { public static String readByInputStream(HttpServletRequest request) throws IOException { StringBuilder sb new StringBuilder(); // 必须指定编码否则中文会乱码 try (InputStream is request.getInputStream(); BufferedReader reader new BufferedReader( new InputStreamReader(is, StandardCharsets.UTF_8))) { String line; while ((line reader.readLine()) ! null) { sb.append(line); } } return sb.toString(); } }这段代码的逻辑很直白拿到字节流用 UTF-8 包装成字符流逐行读取拼接。参数上唯一需要调的是编码StandardCharsets.UTF_8是绝大多数前后端约定的默认值。如果前端用的是 GBK这里必须跟着改否则中文全是问号。但这段代码有个致命问题流被消费后就没有了。如果同一个请求里你先调了readByInputStream后面再想用RequestBody或者再读一次拿到的就是空字符串。这就是为什么很多人在 Filter 里读了 body到 Controller 里就报 “Required request body is missing”。2.2 getReader 与 getInputStream 的互斥陷阱有人会想那我用getReader()是不是更省事看代码public static String readByReader(HttpServletRequest request) throws IOException { StringBuilder sb new StringBuilder(); try (BufferedReader reader request.getReader()) { String line; while ((line reader.readLine()) ! null) { sb.append(line); } } return sb.toString(); }看起来比字节流版本少了一层包装但坑是一样的只能读一次。而且更隐蔽的是如果你先调了getInputStream()再调getReader()会直接抛IllegalStateException提示 “getReader() has already been called for this request”。反过来也一样。这个异常在本地测试时不容易发现因为很多人只读一次就完事了但一旦项目里加了日志切面或者权限过滤器两个组件各读一次线上就炸了。我一般会建议如果确定只读一次用getReader()更简洁如果后续还有组件要读必须走缓存方案下面会讲。2.3 Spring 的 RequestBody方便但有前提Spring MVC 提供了RequestBody注解底层用的是HttpMessageConverter最常见的是MappingJackson2HttpMessageConverter。写法很简单import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; RestController public class UserController { PostMapping(/user) public String createUser(RequestBody UserDTO user) { // Spring 已经帮你把 JSON 转成了 UserDTO return created: user.getName(); } }这段代码能跑通的前提是请求头Content-Type必须是application/json且项目里引入了 Jackson 依赖。如果前端发的是text/plainSpring 会抛HttpMediaTypeNotSupportedException返回 415。参数上RequestBody默认要求 body 不能为空如果允许空 body需要写RequestBody(required false)。RequestBody的本质也是读一次流只不过 Spring 在框架层面帮你做了。它和原生流不能混用如果你在 Filter 里已经读了getInputStream()Controller 里的RequestBody就会拿到空数据。这就是为什么有些项目里加了打印请求日志的 Filter 之后所有 POST 接口都开始报 400。2.4 三种方式怎么选方式能否重复读依赖适用场景getInputStream否无老 Servlet 项目、需要字节级处理getReader否无纯文本 body、只读一次RequestBody否但框架封装好Spring Jackson标准 Spring Boot 接口选型逻辑很简单新项目直接用RequestBody别自己造轮子老项目或者需要在 Filter 里读 body 的用缓存方案需要处理非 JSON 格式比如 XML、纯文本的用getReader()手动解析。3. 可重复读 body包装类与 Filter 的配合3.1 为什么需要可重复读实际项目里body 被读两次的场景非常常见一次是权限过滤器要解析 token 或租户 ID一次是 Controller 要拿业务参数。Servlet 的原生流不支持重复读所以必须自己缓存。思路不复杂在 Filter 里把 body 读出来存成字节数组然后用一个HttpServletRequestWrapper包装原始 request重写getInputStream()和getReader()让它们每次都从缓存数组里返回新的流。3.2 自定义 Wrapper 的完整实现先写 Wrapper 类import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.StandardCharsets; public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { // 缓存 body 字节这是可重复读的核心 private final byte[] cachedBody; public CachedBodyRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 一次性把流读干净存进字节数组 this.cachedBody readBytes(request.getInputStream()); } private byte[] readBytes(InputStream is) throws IOException { ByteArrayOutputStream bos new ByteArrayOutputStream(); byte[] buffer new byte[1024]; int len; while ((len is.read(buffer)) ! -1) { bos.write(buffer, 0, len); } return bos.toByteArray(); } Override public ServletInputStream getInputStream() { ByteArrayInputStream bais new ByteArrayInputStream(cachedBody); 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 listener) { // 同步读取不需要异步监听 } }; } Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader( new ByteArrayInputStream(cachedBody), StandardCharsets.UTF_8)); } // 方便业务代码直接拿字符串 public String getBody() { return new String(cachedBody, StandardCharsets.UTF_8); } }关键点有三个第一构造函数里就把原始流读干净存成byte[]这样后续无论读多少次都有数据第二getInputStream()每次返回一个新的ByteArrayInputStream互不干扰第三getReader()同样基于缓存数组重建编码固定 UTF-8。参数上缓冲区大小 1024 是经验值body 特别大的场景可以调到 4096但要注意内存占用。3.3 Filter 里替换 request 并放行Wrapper 写好了还需要在 Filter 里把原始 request 替换掉import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; WebFilter(urlPatterns /*) public class BodyCacheFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; // 只对 POST/PUT/PATCH 做缓存GET 没有 body String method httpRequest.getMethod(); if (POST.equalsIgnoreCase(method) || PUT.equalsIgnoreCase(method) || PATCH.equalsIgnoreCase(method)) { CachedBodyRequestWrapper wrapper new CachedBodyRequestWrapper(httpRequest); chain.doFilter(wrapper, response); } else { chain.doFilter(request, response); } } }这段代码的逻辑是拦截所有请求判断方法类型只对带 body 的方法做包装然后放行。注意chain.doFilter(wrapper, response)传的是包装后的对象这样后续的 Filter 和 Controller 拿到的都是可重复读的 request。如果项目用的是 Spring BootWebFilter需要配合ServletComponentScan才能生效或者直接用FilterRegistrationBean注册。3.4 在 Controller 里同时拿原始 body 和对象包装之后Controller 里可以这样写PostMapping(/order) public String createOrder(HttpServletRequest request, RequestBody OrderDTO order) throws IOException { // 从 wrapper 里拿原始 JSON 字符串 String rawBody ((CachedBodyRequestWrapper) request).getBody(); // RequestBody 也能正常拿到对象因为流可重复读 System.out.println(raw: rawBody); return ok: order.getOrderId(); }这里request实际上是CachedBodyRequestWrapper的实例强转后调getBody()就能拿到原始字符串。同时RequestBody也能正常工作因为 Spring 读流时拿到的是新的ByteArrayInputStream。这个方案在需要做签名验签、审计日志、或者把原始 body 存库的场景里非常实用。4. 避坑与排查body 读取的五个血泪教训4.1 中文乱码现象是返回问号原因是编码不一致现象Postman 发的中文后端读出来是????或者乱码。原因getInputStream()是字节流不指定编码时用平台默认编码Windows 开发机是 GBKLinux 服务器是 UTF-8本地正常线上乱。解决读流时强制指定StandardCharsets.UTF_8同时确认请求头Content-Type: application/json;charsetUTF-8。如果前端没带 charset可以在 Filter 里先调request.setCharacterEncoding(UTF-8)但注意这行必须在读流之前执行。4.2 Required request body is missing流被提前消费了现象Controller 里RequestBody报 400提示 body 缺失但前端确实发了数据。原因某个 Filter 或 Interceptor 里调用了getInputStream()或getReader()把流读完了Spring 再读就是空。解决用第 3 章的 Wrapper 方案确保所有读操作都走缓存。排查技巧是在 Filter 里打断点看getInputStream被调了几次。4.3 getReader 和 getInputStream 互斥IllegalStateException现象抛IllegalStateException: getReader() has already been called。原因同一个 request 对象上先调了getReader()又调getInputStream()或者反过来。解决统一用一种或者用 Wrapper 包装后各自返回独立流。这个异常在集成测试里容易漏因为单元测试通常只调一次。4.4 大 body 导致内存溢出缓存不是无限制的现象上传大文件或者批量接口时服务 OOM。原因Wrapper 把整个 body 读进byte[]如果 body 是 100MB每个请求就占 100MB 堆内存。解决对文件上传接口跳过缓存或者在 Wrapper 里加大小限制超过阈值直接抛异常。常见做法是判断Content-Typemultipart/form-data不走缓存 Filter。4.5 Postman 能通、前端不通Content-Type 不一致现象Postman 测试正常前端调用报 415。原因Postman 默认发application/json前端可能发的是text/plain或者没设 Content-Type。解决让前端确认请求头或者在 Controller 里用consumes指定多种类型。排查时用浏览器 F12 看请求头对比 Postman 的 Raw 视图。5. 进阶技巧用 ContentCachingRequestWrapper 少写一半代码Spring 其实自带了一个ContentCachingRequestWrapper在org.springframework.web.util包下。它的原理和我们的 Wrapper 类似也是缓存 body但有个关键区别它是在第一次读流时才缓存而不是构造时就读。这意味着如果你在 Filter 里包装了但没读到 Controller 里RequestBody读的时候才会触发缓存之后getContentAsByteArray()就能拿到数据。用法如下import org.springframework.web.util.ContentCachingRequestWrapper; Component public class CacheFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { ContentCachingRequestWrapper wrapper new ContentCachingRequestWrapper(request); chain.doFilter(wrapper, response); // 请求结束后可以拿到缓存的 body byte[] body wrapper.getContentAsByteArray(); if (body.length 0) { System.out.println(body: new String(body, StandardCharsets.UTF_8)); } } }这段代码比自定义 Wrapper 简洁得多而且不用手动处理ServletInputStream的匿名类。但要注意getContentAsByteArray()必须在流被读过之后才有数据。如果你在 Filter 里包装完直接调拿到的是空数组因为还没人读流。所以它适合在请求处理完成后做日志记录不适合在 Filter 里提前解析参数。我自己的习惯是如果只是做访问日志用ContentCachingRequestWrapper就够了如果需要在 Filter 里提前拿 body 做鉴权或路由还是用自定义 Wrapper因为构造时就读时机可控。另外提醒一句ContentCachingRequestWrapper默认缓存上限是 1024 字节超过部分不缓存大 body 场景要调contentCacheLimit参数或者干脆自己实现。验证方案是否生效最简单的办法是写一个接口同时打印RequestBody对象和request.getInputStream()读到的内容如果两者都有数据且一致说明缓存生效。如果第二个是空说明 Wrapper 没起作用检查 Filter 注册顺序和chain.doFilter传的对象。最后说个我踩过的坑有一次在网关层加了 body 缓存结果所有文件上传接口都挂了因为multipart请求的流被提前消费文件解析失败。后来在 Filter 里加了Content-Type判断跳过multipart/form-data才解决。所以缓存虽好但别一刀切按需启用才是正道。希望帮到你。本文还有配套的精品资源点击获取
返回列表