SpringMVC流程分析(九):从源码解释@ReqeustBody参数无法绑定的问题
本系列文章皆在分析SpringMVC
的核心组件和工作原理,让你从SpringMVC
浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC
的工作原理。
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜
前言
而本文笔者
主要想通过工作中遇到的一个小问题为例,来讲一讲在工作中我们该如何利用所学的源码
知识来快速定位发生地
。
问题复现
在工作中笔者定义了这样一个控制层接口:
@RestController
@ReqeustMapping("/u")
public class UserController {
@PostMapping("/add-user")
public CommonResult<User> getUser(@RequestBody UserInfo userInfo,
String uid ) {
// 省略其他逻辑
retrun CommonResut.success(userInfo,uid)
}
}
其中UserInfo
对象中包括两个属性:
Integer : userId
: 用以作用用户的唯一表示信息String : userName
: 用户昵称信息
当前端发送请求时,笔者
最开始提供的Json
格式信息如下:
{
"UserInfo" : {
"userId": 123,
"userName": "zhangSan"
},
"uid" : "123"
}
如果熟悉@RequestBody
注解和SpringMVC
框架使用的小伙伴一眼就能看出上述代码中存在的问题。事实上,针对上述代码中主要存在问题,有如下的修改意见:
- 在
Post
请求下,如果要接受前端传递uid
信息,此时对于getUser
方法应该在其参数uid
前,加上一个@ReqeustParam
注解,从而获取到请求url
的参数信息。 - 在
<1>
的基础上,参数
传递过程中应该其请求体中的uid
信息去掉,并改为如下的写法。
{
"userId": 123,
"userName": "zhangSan"
}
当程序按照如上的修改意见修改后,才能确保前端传递的参数信息可以自动
封装给getUser
参数列表中的对象中。
此时我的疑问随之而来,为什么最开始那种参数
传递方式不可以? 进一步, 如果我将getUser
方法参数列表改为:
@RestController
@ReqeustMapping("/u")
public class UserController {
@PostMapping("/add-user")
public CommonResult<User> getUser(@RequestBody UserInfo userInfo,
@RequestBody String uid ) {
// 省略其他逻辑
retrun CommonResut.success(userInfo,uid)
}
}
的形式,那此时前端传递的内容可以封装到userInfo
对象和uid
对象中吗?
可能在平常的开发中,我们就是按部就班
的按照既定的程序开发, 不曾想过这样的问题。即使遇到了后端Controller
使用@RequestBody
接收参数信息,但参数无法进行自动封装的问题,也只是询问一下百度
,按照其他人博客
的内容一同折腾配置后,结果无非两种。一种是成功解决,一种则是无法解决,然后在不断的翻阅其他博客,直到问题解决。
虽然最后问题是解决了,但真的懂了其背后的原理了吗?如果下次在遇到这类问题又该如何排查呢?
接下来,不妨看看当我们了解了框架处理流程后如何来快速定位问题。针对这个问题我们是如何定位问题和寻求解之道的。
SpringMVC中对于一个请求的处理流程
在之前文章中我们曾提到过,在SpringMVC
中DispatcherServlet
对于一个请求处理过程可总结为如图所示的过程:
即当一个请求到达Spring MVC
的控制器方法时,一个请求的处理流程大致如下所示:
- Servlet容器处理请求:最初,请求由
Servlet
容器接收和处理。Servlet
容器将HTTP
请求的内容读取到一个输入流中,其中包括请求体中的数据。 - DispatcherServlet分派请求:
Servlet
容器将请求传递给SpringMVC
的DispatcherServlet
。进一步,DispatcherServlet
负责将请求分派给适当的控制器方法进行处理。 - HandlerMapping确定控制器方法:
DispatcherServlet
使用HandlerMapping
来确定哪个控制器方法应该处理请求。HandlerMapping
根据请求的URL、HTTP
方法等信息找到匹配的控制器方法。 - 寻找适配器HandlerAdapter: 一旦确定了要使用的
Controller
,它会使用HandlerAdapter
来适配执行请求的处理器。 - HttpMessageConverter执行数据解析:一旦确定了要调用的控制器方法,
Spring MVC
使用HttpMessageConverter
来执行请求体数据的绑定。 - 数据绑定和参数传递:消息转换器将请求体中的数据解析为控制器方法参数的类型,然后将参数传递给控制器方法。此时,控制器方法可以使用参数来访问请求体中的数据并进行进一步的处理。
- 控制器方法处理请求:控制器方法使用解析后的数据执行业务逻辑,然后返回响应。
- HttpMessageConverter执行响应数据转换:在控制器方法返回响应时,
Spring MVC
再次使用HttpMessageConverter
来将Java对象转换为适当的响应格式。这是@ResponseBody
注解的工作原理。 - 响应返回客户端:
DispatcherServlet
将响应发送回给客户端,由Servlet容器负责将其传递给浏览器或客户端应用程序。
那@RequestBody
注解的内容会被Spring MVC
的请求处理流程哪一步处理呢?答案其实很明显。其会在<5>HttpMessageConverter执行数据解析
部分被处理。
事实上,HttpMessageConverter
的作用是将请求体中的数据解析为控制器方法参数所需的Java
对象。换言之,HttpMessageConverter
可以根据请求头中的Content-Type
和控制器方法参数的类型,选择合适的消息转换器来解析请求体中的数据。
当明白了处理@RequestBody
的组件,接下来,我们的疑问就是这一组件是在何处被用到的呢?
换言之,如果我们分析@RequestBody
注解的解析过程,应该从哪着手呢? 是应该从HandlerMappring
入手吗?当然不是!
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
// .... 省略其他无关代码
// <2> 调用 HandlerMethod 方法
mav = invokeHandlerMethod(request, response, handlerMethod);
// .... 省略其他无关代码
return mav;
}
所以如果我们要分析@ReqeustBody
解析过程,我们的入口一定是RequestMappingHandlerAdapter
的handleInternal
方法。进一步,其内部对于@ReqeustBoyd
解析流程如下所示:
可以看到,对于
@ReqeustBody
注解的解析,最终解析结果是委托给objectMapper
中的readValue
来完成的。通过名称也可以看出,这一过程就是将请求体
中传递的Json
格式信息进行读取并封装为一个Java
对象。具体代码细节如下:
这部分解析代码如下:
ObjectMapper#readValue
public <T> T readValue(InputStream src, JavaType valueType)
throws IOException, JsonParseException, JsonMappingException
{
_assertNotNull("src", src);
return (T) _readMapAndClose(_jsonFactory.createParser(src), valueType);
}
总结来看readValue
无非完成两个逻辑:
- 创建JSON解析器:使用
_jsonFactory
(这是ObjectMapper
内部的Json
工厂)创建一个JSON解析器(JsonParser
)。这个解析器将从提供的Reader
中读取数据即Post
的请求体。 - 反序列化过程:
_readMapAndClose
方法开始解析输入的Json
数据。在这个过程中,它会执行与之前描述的readValue
方法相似的操作,包括使用Json
解析器解析JSON数据,将数据映射到目标Java
类的实例上,并执行必要的类型转换。
(注:这一过程执行完会关闭数据流,也就是说只能从Post
请求体中的内容只能被读取一次!)
总结
至此,其实我们之前疑问已经有了答案。为什么
{
"UserInfo" : {
"userId": 123,
"userName": "zhangSan"
},
"uid" : "123"
}
无法封装为一个UserInfo
对象?答案很简单,当后端Controller
中方法使用了@ReqeustBody
后,其会解析请求体中Json
格式的内容解析,然后封装为一个Java
对象。而这一过程本质都是委托给JackSon
来做的。
事实上,确保Json
数据可以封装为一个Java
对象的的前提就是。Json
数据与Java
类的匹配。换言之,Json
数据中的属性名称必须与目标Java
类中的字段或属性名称匹配。虽然Jackson
可以通过注解和配置来自定义映射规则,但默认情况下,它会使用属性名称来匹配。
(注:SpringBoot
默认是使用Jackson
作为Json
数据格式处理的类库)
此外,SpringMVC
框架只能解析一次请求体中的内容,一旦请求体的内容被绑定到一个方法参数上,该方法中其他参数就无法再次使用@RequestBody
注解来解析同一个请求体的内容。
本文以笔者工作中使用@RequestBody
注解遇到的一个小问题为例,利用SpringMVC
对于一个处理的流程,逐步刨根问底一步步揭秘@RequestBody
解析的全过程。希望本次问题排查过程对你日后快速定位问题有所帮助!
转载自:https://juejin.cn/post/7278239421706747940