深入剖析@RequestBody无法被重复解析的原因


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


在之前SpringMVC流程分析(九):从源码解释@ReqeustBody参数无法绑定的问题中,我们从源码角度深入剖析了在SpringMVC中使用@RequestBoyd注解无法将传递的值封装到Java实体对象中的原因。

简单来看,当我们使用@ReqeustBody注解后,SpringMVC内部会对请求体中Json格式的内容解析,然后封装为一个Java对象 。换言之,@RequestBody完成请求体到Java对象的封装可以简单理解为Json字符串与Java对象的简单转换。

进一步,这一操作成功进行的背后就需要确保 Json数据与Java类的匹配 。换言之,Json数据中的属性名称必须与目标Java类中的字段或属性名称匹配。或许,你觉得掌握了这点就可以轻易拿捏@RequestBody注解了。但如果我写出如下代码,阁下又该如何应对呢?

java 复制代码
@PostMapping("/duplicate")
public ResponseEntity getBookAndUserInfo(@RequestBody BookInfo bookInfo ,
                                         @RequestBody UserInfo userInfo) {
    BookInfoDto bookInfoDto = BookInfoDto.builder()
            .bookInfo(bookInfo)
            .userInfo(userInfo)
            .build();


    return new ResponseEntity(bookInfoDto, HttpStatus.OK);
}

也许你会觉得这样做很搞笑,因为基本不会在一个方法中入参中连续使用两次@RequestBody注解。

虽然这样的做法很疯狂,但不知道你是否想过那这样做会出什么问题呢?进一步,诱发这一问题的原因又是什么呢? 对此不了解也没关系,接下来我们便从源码的角度来对这一问题进行深入解读。

在分析之前,我们先来简单回顾一下@RequestBody注解的基本使用。

概览@RequestBody

Spring MVC中,@RequestBody 用于将HTTP请求体映射到方法参数的注解。该注解用于指示方法参数应该从请求体中获取,并通过适当的消息转换器将请求体的内容转换为方法参数的类型。其用法也很简单,只需标注在方法入参之前就可以。具体如下所示:

java 复制代码
@PostMapping("/example")
public ResponseEntity<String> handleRequestBody(@RequestBody SomeObject someObject) {
    // 处理请求体的内容,SomeObject是自定义的Java对象
    // ...
    return ResponseEntity.ok("Success");
}

在上述例子中,SomeObject是一个自定义的Java对象,而@RequestBody注解告诉Spring MVC将请求体的内容转换为SomeObject类型的对象,并作为方法参数传递。总结一句话来说,@RequestBody注解用于从HTTP请求体中提取数据,并将其映射到方法参数上。

了解了Spring MVC内部对于@ReqesutBody注解的使用后,接下来我们就在揭开在@RequestBody在一个方法中无法重复使用的原因!

重复使用@RequestBody所导致的问题

在学习SpringMVC相信你一定听过这样的言论,即"Spring MVC不支持多个@RequestBody注解用于同一个方法参数上,因为一个请求通常只有一个请求体,而不是多个"

换言之,如果你需要处理多个部分的数据,可以使用一个自定义的Java对象来封装这些部分。这个对象可以包含多个字段,每个字段对应请求体的一个部分。但一个方法中同时多个@RequestBody会出什么问题呢?

为复现一个方法入参中重复使用@RequestBody注解的现象,我们很容易写出如下代码:

java 复制代码
@PostMapping("/duplicate")
public ResponseEntity getBookAndUserInfo(@RequestBody BookInfo bookInfo ,
                                         @RequestBody UserInfo userInfo) {
    BookInfoDto bookInfoDto = BookInfoDto.builder()
            .bookInfo(bookInfo)
            .userInfo(userInfo)
            .build();


    return new ResponseEntity(bookInfoDto, HttpStatus.OK);
}

当通过PostMan请求/duplicate路径后,会发现提示如下的信息:

进一步,我们可以看到控制台会提示如下信息: Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing

通过Idea日志提示信息,我们可以知道请求出400的原因在于Required request body is missing。进而导致请求无法正常被SpringMVC所处理,所以提出400的错误码。

接下来,我们便来深入探究下其出现该问题的原因,因为只有知晓了出错的原因,我们才能着手解决问题。

注:后续内容可能会涉及到一点对于@RequestBody注解解析原理的知识,不了解的可参考:剖析SpringMVC内部对于@ReqeustBody注解的解析

深究@RequestBody无法被重复解析原因

众所周知,在SpringMVC中有关参数入参解析通过InvocableHandlerMethod中的getMethodArgumentValues来完成,而类似@RequestBody这样的注解解析又会委托于HandlerMethodArgumentResolver来完成。

进一步,在SpringMVCRequestResponseBodyMethodProcessor主要负责完成@RequestBody的解析工作。其核心方法readWithMessageConverters的逻辑如下:

RequestResponseBodyMethodProcessor

java 复制代码
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) {

   // ......省略无关代码
   Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
   if (arg == null && checkRequired(parameter)) {
      throw new HttpMessageNotReadableException("Required request body is missing: " +
            parameter.getExecutable().toGenericString(), inputMessage);
   }
   return arg;
}

看到上述代码中的Required request body is missing:是不是有一种眼前一亮的感觉?

这正是我们请求失败后,控制台所输出的内容这表明,只要我们分析清楚清楚上述代码中为什么arg == null && checkRequired(parameter)执行结果给true的条件我们便能搞清楚SpringMVC内部不支持重复使用@RequestBody注解的原因。

注:checkRequired(parameter)方法主要用于校验方法中是否有@RequestBody注解。换言之,只要方法入参中有@RequestBody注解,该方法返回值则永远为true

那么接下来,只要我们能探究出arg==null成立的原因,其实我们也就清楚了@RequestBody无法重复被解析的秘密。

所以接下来我们把目光聚焦到readWithMessageConverters方法内部。

java 复制代码
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType) {
      
   Object body = NO_VALUE;

   EmptyBodyCheckingHttpInputMessage message = null;
   try {
      message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
      // 循环遍历SpringMVC中的HttpMessageConverter寻找到合适的处理器来完成解析
      for (HttpMessageConverter<?> converter : this.messageConverters) {
         Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
         GenericHttpMessageConverter<?> genericConverter =
               (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
         if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
               (targetClass != null && converter.canRead(targetClass, contentType))) {
            if (message.hasBody()) {
               HttpInputMessage msgToUse =
                     getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
               body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                     ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
               body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
            }
           
      }
   }
 
   // 如果body经过处理器解析后,未被解析则返回null
   if (body == NO_VALUE) {
      if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
            (noContentType && !message.hasBody())) {
         return null;
      }
    
   return body;
}

阅读上述代码不难发现,如果body对象内容无法通过解析成功,那么则会body的取值则为NO_VALUE,进一步,也就会进入到body == NO_VALUE的计算结果变为true,进而readWithMessageConverters方法内部返回的值也为null

body内容解析主要受那两方面影响呢?不难发现,其主要无非受两方便因素影响

  1. 无参数对应的HttpMessageConverter
  2. 有对应的HttpMessageConverter, 但message.hasBody()执行结果为true

事实上,导致此处body对象无法解析成功原因只能为message.hasBody()。因为注解@ReqeustBody对应解析器为SpringMVC内部提供的,无需我们手动编写,进而导致此处body对象无法封装成功的原因只能为:有对应的HttpMessageConverter, 但message.hasBody()执行结果为true

而此处为什么message.hasBody()执行结果为true的原因其实也很简单,一言以蔽之,就是因为SpringMVC内部对于请求体的内容是通过I/O流操作的,而I/O流执行完毕后是会被关闭的,因此第二次读取时I/O流已被关闭,所以导致数据无法读取。

注:此处笔者只是简要的给出解释,具体原因我们会在下一遍进行深入分析,并给出具体的破局之道,换言之,我们在一个方法中是可以使用@RequestBody多次的!

总结

至此为何 throw new HttpMessageNotReadableException("Required request body is missing:)会执行的原因我们也就清楚了,进一步,其实我们也就对SpringMVC内部重复使用@RequestBody无法被解析的原因进行深入的分析简单来看,就是因为SpringMVC请求体中的内容通过I/O流的方式来读取,其只被读取一次,读取完毕后会将I/O流关闭,因此后续再解析请求体中内容,并将内容封装到@RequestBody修饰的对象中。

希望文章对你理解SpringMVC有所帮助,如果觉得文章不错不妨点赞、收藏、关注。我们下次再见~

相关推荐
冷琴19965 分钟前
基于java+springboot的酒店预定网站、酒店客房管理系统
java·开发语言·spring boot
九圣残炎28 分钟前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端
daiyang123...32 分钟前
IT 行业的就业情况
java
爬山算法1 小时前
Maven(6)如何使用Maven进行项目构建?
java·maven
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛1 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
吹老师个人app编程教学1 小时前
详解Java中的BIO、NIO、AIO
java·开发语言·nio
爱学的小涛1 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪1 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
琴智冰1 小时前
SpringBoot
java·数据库·spring boot