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

1,133 阅读7分钟

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


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

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

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

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

@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注解的现象,我们很容易写出如下代码:

@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路径后,会发现提示如下的信息:

image.png

进一步,我们可以看到控制台会提示如下信息: 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

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方法内部。

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有所帮助,如果觉得文章不错不妨点赞、收藏、关注。我们下次再见~