思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在之前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
来完成。
进一步,在SpringMVC
中RequestResponseBodyMethodProcessor
主要负责完成@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
内容解析主要受那两方面影响呢?不难发现,其主要无非受两方便因素影响
- 无参数对应的
HttpMessageConverter
- 有对应的
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
有所帮助,如果觉得文章不错不妨点赞、收藏、关注。我们下次再见~