当RequestBody遇上Request数据去哪儿了一、问题引入宝子们今天来和大家聊聊我在日常开发中遇到的一个超让人迷惑的问题。不知道你们有没有遇到过在使用RequestBody注解之后request里的数据就好像 “人间蒸发” 了一样啥都没有了。这可把我折磨惨了调试了好久查阅了各种资料才终于搞明白其中的缘由。今天就来给大家分享一下这个问题希望能帮助到同样被这个问题困扰的小伙伴们‍。接下来就让我们一起深入探究一下RequestBody之后request里为什么 “什么都没有了”。二、RequestBody 与 Request 简介一RequestBody 是什么在 Spring MVC 和 Spring Boot 开发中RequestBody注解可是个 “狠角色”。它主要用于读取 HTTP 请求体Body中的数据并将其通过HttpMessageConverter通常是 Jackson反序列化为 Java 对象。简单来说就是当客户端发送请求时RequestBody可以帮我们把请求体中的数据比如 JSON 格式的数据转化为 Java 对象方便我们在后端进行处理。举个例子在一个前后端分离的项目中前端以 JSON 格式发送用户注册信息{username:张三,password:123456,email:zhangsanexample.com}后端的 Controller 层可以这样接收PostMapping(/register)publicStringregister(RequestBodyUseruser){// 这里的user就是反序列化后的Java对象userService.register(user);return注册成功;}在这个例子中RequestBody就像是一个 “翻译官”把前端传来的 JSON 数据准确无误地 “翻译” 成我们后端可以操作的 Java 对象。它最常用于处理Content-Type: application/json的请求是实现现代前后端分离架构RESTful API 中最核心的注解之一。二Request 的基本概念在 Java Web 开发中HttpServletRequest是一个非常重要的接口。它代表客户端向服务器发送的 HTTP 请求Servlet 在收到请求后会创建一个HttpServletRequest实例并把它作为参数传递给 Servlet 的service()、doGet()、doPost()等方法。简单来说HttpServletRequest就像是一个 “快递包裹”里面装着客户端发送给服务器的所有信息包括请求行方法、路径、协议、请求头Headers、请求参数query string form data、请求体JSON、文件、二进制、Cookie、Session 以及客户端信息IP、User - Agent 等。我们可以通过它的各种方法来获取这些信息比如getParameter(String name)获取请求参数比如String username request.getParameter(username);getHeader(String name)获取请求头信息例如String userAgent request.getHeader(User - Agent);getMethod()获取请求的 HTTP 方法像String method request.getMethod();返回值可能是GET、POST等getInputStream()获取请求的输入流用于处理 POST 请求等当请求体是输入流形式时就可以用这个方法。HttpServletRequest在整个 Web 请求处理过程中扮演着至关重要的角色是我们与客户端请求数据交互的重要桥梁。三、数据丢失现象呈现一实际案例展示下面给大家分享一个我在实际项目中遇到的案例。在一个电商项目的订单创建接口中前端会发送包含订单信息如商品列表、用户信息、收货地址等的 JSON 数据到后端。后端的 Controller 代码大致如下RestControllerRequestMapping(/order)publicclassOrderController{AutowiredprivateOrderServiceorderService;PostMapping(/create)publicStringcreateOrder(RequestBodyOrderorder,HttpServletRequestrequest){// 这里尝试从request中获取一些额外的参数比如用户的IP地址StringuserIprequest.getRemoteAddr();System.out.println(用户IP: userIp);// 调用服务层创建订单orderService.createOrder(order);return订单创建成功;}}当我满怀信心地进行测试时却发现一个严重的问题在createOrder方法中通过request.getRemoteAddr()获取用户 IP 地址时得到的竟然是null。这就很奇怪了明明前端已经发送了请求RequestBody也能正常接收订单数据为什么request里的其他数据好像 “消失” 了呢我进一步查看日志发现没有任何报错信息只是数据获取不到这让排查问题变得更加困难。二引发的问题思考这种数据丢失的现象对开发的影响可不小。首先它会导致业务逻辑无法正常进行。就像上面的订单创建接口获取不到用户 IP 地址可能会影响后续的一些业务操作比如根据用户 IP 地址进行地域统计分析或者记录用户操作日志时缺少关键信息。其次数据处理也可能出现错误。如果在后续的代码中依赖request里的数据进行计算或判断而这些数据丢失了就会导致计算结果错误或判断逻辑失误从而影响整个系统的稳定性和可靠性。这不仅会增加开发人员的调试成本还可能给用户带来不好的体验甚至造成业务损失。所以我们必须要深入探究这种现象背后的原因找到解决办法。四、深度剖析数据丢失原因一HTTP 协议的流式传输特性HTTP 协议是基于请求 - 响应模型的应用层协议其中请求数据是通过流式传输的。这意味着请求体中的数据就像一条源源不断的 “数据流”从客户端流向服务器。当服务器端读取这个数据流时就像从水管中取水一样一旦读取数据就会从 “水管” 中流过无法再次回到水管中供下次读取。在 HTTP 协议中输入流数据一旦被读取就会被消耗掉。这是因为 HTTP 协议设计的初衷是为了高效地传输数据避免数据在内存中不必要的重复存储。当RequestBody注解读取请求体数据时它实际上是从请求的输入流中读取数据并将其反序列化为 Java 对象。在这个过程中输入流中的数据被读取并处理之后如果再尝试从request中读取数据就会发现数据已经被 “读光” 了自然就 “什么都没有了”。就好比你一次性把杯子里的水喝完了再去看杯子肯定是空的啦。这种流式传输特性在大多数情况下能够满足我们的需求但在一些特殊场景下就可能会引发数据丢失的问题。二Servlet 中 Request 的工作机制在 Java Servlet 中request.getInputStream()方法用于获取请求的输入流以便读取请求体中的数据。然而这个方法有一些特殊的调用逻辑和限制。当我们调用request.getInputStream()时Servlet 容器会创建一个ServletInputStream对象它继承自InputStream。这个ServletInputStream对象负责从底层的网络连接中读取数据并提供给我们的应用程序。但问题来了ServletInputStream并没有实现reset()方法这意味着一旦输入流被读取到末尾就无法将读取位置重置到开头从而无法再次读取数据。当RequestBody注解调用request.getInputStream()读取数据时输入流的指针会移动到数据末尾后续再调用request.getInputStream()或者其他依赖输入流的方法时就会因为指针已经在末尾而无法读取到任何数据。而且Servlet 中对于请求数据并没有内置的缓存机制。不像我们日常用的缓存工具它不会自动帮我们把读取过的数据存起来以备后用。所以一旦数据被读取就没有其他地方可以找回这些数据了除非我们自己手动缓存。这就像是你看完一本书后没有把它放在书架上缓存起来下次再想看的时候就找不到它了。这就是为什么在使用RequestBody之后request里的数据会丢失的重要原因之一。五、解决数据丢失问题的方法一使用 HttpServletRequestWrapper既然我们知道了数据丢失的原因是因为request的输入流不可重复读取那么有没有办法解决这个问题呢答案是肯定的我们可以使用HttpServletRequestWrapper类来解决这个问题。HttpServletRequestWrapper是HttpServletRequest的一个包装类它提供了一种简单的方式来扩展或修改HttpServletRequest的行为。我们可以通过继承HttpServletRequestWrapper类并重写它的getInputStream()和getReader()方法来实现对输入流数据的缓存和多次读取。具体来说我们在自定义的HttpServletRequestWrapper类中在构造函数中读取一次输入流的数据并将其缓存起来比如保存在一个字节数组中。然后在重写的getInputStream()和getReader()方法中返回从缓存中读取数据的输入流和读取器这样就可以实现对输入流数据的多次读取了就像给输入流数据找了一个 “小仓库”把数据存起来什么时候想用都能拿出来。二代码示例与详细步骤下面给大家上代码看看具体是怎么实现的。自定义 HttpServletRequestWrapper 类importjavax.servlet.ReadListener;importjavax.servlet.ServletInputStream;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletRequestWrapper;importjava.io.BufferedReader;importjava.io.ByteArrayInputStream;importjava.io.IOException;importjava.io.InputStreamReader;publicclassBodyReaderHttpServletRequestWrapperextendsHttpServletRequestWrapper{// 用于存储请求体数据的字节数组privatefinalbyte[]body;publicBodyReaderHttpServletRequestWrapper(HttpServletRequestrequest)throwsIOException{super(request);// 读取请求体数据并存储到body数组中bodygetBodyString(request).getBytes();}OverridepublicServletInputStreamgetInputStream()throwsIOException{finalByteArrayInputStreambaisnewByteArrayInputStream(body);returnnewServletInputStream(){Overridepublicintread()throwsIOException{returnbais.read();}OverridepublicbooleanisFinished(){returnfalse;}OverridepublicbooleanisReady(){returnfalse;}OverridepublicvoidsetReadListener(ReadListenerreadListener){// 不实现}};}OverridepublicBufferedReadergetReader()throwsIOException{returnnewBufferedReader(newInputStreamReader(getInputStream()));}// 获取请求体数据的工具方法privateStringgetBodyString(HttpServletRequestrequest)throwsIOException{StringBuildersbnewStringBuilder();BufferedReaderreaderrequest.getReader();Stringline;while((linereader.readLine())!null){sb.append(line);}returnsb.toString();}}在这个类中我们首先定义了一个body字节数组来存储请求体数据。在构造函数中通过调用getBodyString方法读取请求体数据并将其转换为字节数组存储在body中。然后重写getInputStream方法返回一个从body数组中读取数据的ByteArrayInputStream这样就实现了对输入流数据的缓存读取。重写getReader方法通过调用重写后的getInputStream方法来获取读取器确保读取的数据是从缓存中获取的。获取输入流内容的工具类上面已经包含在自定义类中这里单独提取出来讲解importjavax.servlet.ServletRequest;importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStreamReader;publicclassHttpHelper{publicstaticStringgetBodyString(ServletRequestrequest)throwsIOException{StringBuildersbnewStringBuilder();BufferedReaderreaderrequest.getReader();Stringline;while((linereader.readLine())!null){sb.append(line);}returnsb.toString();}}这个工具类中的getBodyString方法用于从ServletRequest中读取请求体数据。它通过创建一个BufferedReader逐行读取请求体数据并将其拼接成一个字符串返回。这个方法在自定义的HttpServletRequestWrapper类的构造函数中被调用用于获取并存储请求体数据。配置过滤器importorg.springframework.stereotype.Component;importorg.springframework.web.filter.OncePerRequestFilter;importjavax.servlet.FilterChain;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;ComponentpublicclassRequestBodyCacheFilterextendsOncePerRequestFilter{OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{// 使用自定义的HttpServletRequestWrapper包装原始requestHttpServletRequestwrappedRequestnewBodyReaderHttpServletRequestWrapper(request);// 将包装后的request传递给过滤器链filterChain.doFilter(wrappedRequest,response);}}这里我们创建了一个过滤器RequestBodyCacheFilter它继承自OncePerRequestFilter保证每个请求只会被该过滤器执行一次。在doFilterInternal方法中我们使用自定义的BodyReaderHttpServletRequestWrapper类包装原始的HttpServletRequest然后将包装后的request传递给过滤器链。这样在后续的处理中所有对request的读取操作都会从我们缓存的数据中获取从而避免了数据丢失的问题。通过以上三步我们就成功地解决了RequestBody之后request数据丢失的问题。小伙伴们可以根据自己的项目需求将这些代码集成到项目中让你的项目更加健壮和稳定。六、总结与拓展一回顾核心内容今天我们深入探讨了在使用RequestBody之后request里数据丢失的问题。我们先了解了RequestBody的作用是将 HTTP 请求体中的数据反序列化为 Java 对象而HttpServletRequest则代表了客户端发送的 HTTP 请求包含了各种请求信息。接着通过实际案例展示了数据丢失的现象以及这种现象对开发造成的业务逻辑和数据处理方面的影响。在剖析原因时我们发现 HTTP 协议的流式传输特性以及 Servlet 中Request的工作机制是导致数据丢失的关键因素。HTTP 请求体数据通过流式传输一旦被读取就无法再次读取而 Servlet 中request的输入流没有内置缓存机制且不可重复读取这就使得RequestBody注解读取数据后request里的数据好像 “消失” 了。为了解决这个问题我们使用了HttpServletRequestWrapper类通过继承并重写其getInputStream()和getReader()方法实现了对输入流数据的缓存和多次读取。并通过自定义HttpServletRequestWrapper类、获取输入流内容的工具类以及配置过滤器这三个步骤成功解决了数据丢失的问题。小伙伴们一定要记住这些关键知识点在实际开发中遇到类似问题时能够快速定位并解决。二拓展思考在实际开发中遇到类似问题时我们可以从多个角度进行思考和排查。首先要深入理解相关技术的原理和工作机制比如 HTTP 协议、Servlet 规范等这有助于我们从根本上分析问题产生的原因。其次要善于利用日志和调试工具通过打印日志信息和使用调试工具我们可以更清楚地了解程序的执行过程和数据的变化情况从而快速定位问题所在。另外多参考官方文档和优秀的开源项目也是一个很好的方法这些资源中往往包含了许多解决常见问题的最佳实践和经验总结。希望今天的分享能对大家有所帮助如果你在开发中也遇到了有趣的问题或者有更好的解决方案欢迎在评论区留言分享让我们一起学习共同进步