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

文章探讨了SpringMVC中使用@RequestBody注解时,如何确保Json数据与Java对象匹配,以及在方法参数中重复使用@RequestBody可能导致的问题,揭示了I/O流操作导致的解析失败原因。
摘要由CSDN通过智能技术生成

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

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

 

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修饰的对象中。

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值