前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。
本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。
不论在SpringBoot还是SpringMVC中,一个HTTP请求会被 DispatcherServlet
类接收,它本质是一个 Servlet
,因为它继承自 HttpServlet
。在这里,Spring负责解析请求,匹配到 Controller
类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。
我们今天的重点在于解析参数,对应到上图的 目标方法调用
这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。Spring已经帮我们注册了一堆这东西。
它们有一个共同的接口 HandlerMethodArgumentResolver
。 supportsParameter
用来判断方法参数是否可以被当前解析器解析,如果可以就调用 resolveArgument
去解析。
在Controller方法中,如果你的参数标注了 RequestParam
注解,或者是一个简单数据类型。
我们的请求路径是这样的: http://localhost:8080/test1?t1=Jack&t2=Java
如果按照以前的写法,我们直接根据参数名称或者 RequestParam
注解的名称从Request对象中获取值就行。比如像这样:
Stringparameter=request.getParameter("t1");
在Spring中,这里对应的参数解析器是 RequestParamMethodArgumentResolver
。与我们的想法差不多,就是拿到参数名称后,直接从Request中获取值。
如果我们需要前端传输更多的参数内容,那么通过一个POST请求,将参数放在Body中传输是更好的方式。当然,比较友好的数据格式当属JSON。
面对这样一个请求,我们在Controller方法中可以通过 RequestBody
注解来接收它,并自动转换为合适的Java Bean对象。
在没有Spring的情况下,我们考虑一下如何解决这一问题呢?
首先呢,还是要依靠Request对象。对于Body中的数据,我们可以通过 request.getReader()
方法来获取,然后读取字符串,最后通过JSON工具类再转换为合适的Java对象。
比如像下面这样:
当然,在实际场景中,上面的SysUser.class需要动态获取参数类型。
在Spring中, RequestBody
注解的参数会由 RequestResponseBodyMethodProcessor
类来负责解析。
它的解析由父类 AbstractMessageConverterMethodArgumentResolver
负责。整个过程我们分为三个步骤来看。
在开始之前需要先获取请求的一些辅助信息,比如HTTP请求的数据格式,上下文Class信息、参数类型Class、HTTP请求方法类型等。
上面获取到的辅助信息是有作用的,就是要确定一个消息转换器。消息转换器有很多,它们的共同接口是 HttpMessageConverter
。在这里,Spring帮我们注册了很多转换器,所以需要循环它们,来确定使用哪一个来做消息转换。
如果是JSON数据格式的,会选择 MappingJackson2HttpMessageConverter
来处理。它的构造函数正是指明了这一点。
既然确定了消息转换器,那么剩下的事就很简单了。通过Request获取Body,然后调用转换器解析就好了。
再往下就是Jackson包的内容了,不再深究。虽然写出来的过程比较啰嗦,但实际上主要就是为了寻找两个东西:
方法解析器RequestResponseBodyMethodProcessor
消息转换器MappingJackson2HttpMessageConverter
都找到之后调用方法解析即可。
还有一种写法是这样的,在Controller方法上用Java Bean接收。
然后用GET方法请求:
http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=北京市海淀区
URL后面的参数名称对应Bean对象里面的属性名称,也可以自动转换。那么,这里它又是怎么做的呢 ?
笔者首先想到的就是Java的反射机制。从Request对象中获取参数名称,然后和目标类上的方法一一对应设置值进去。
比如像下面这样:
除了反射,Java还有一种内省机制可以完成这件事。我们可以获取目标类的属性描述符对象,然后拿到它的Method对象, 通过invoke来设置。
然后在上面的循环中,我们就可以调用这个方法来实现。
为什么要说到内省机制呢?因为Spring在处理这件事的时候,最终也是靠它处理的。
简单来说,它是通过 BeanWrapperImpl
来处理的。关于 BeanWrapperImpl
有个很简单的使用方法:
wrapper.setPropertyValue
最后就会调用到 BeanWrapperImpl#BeanPropertyHandler.setValue()
方法。
它的 setValue
方法和我们上面的 setProperty
方法大致相同。
通过上面的方式,就完成了GET请求参数到Java Bean对象的自动转换。
回过头来,我们再看Spring。虽然我们上面写的很简单,但真正用起来还需要考虑的很多很多。Spring中处理这种参数的解析器是 ServletModelAttributeMethodProcessor
。
它的解析过程在其父类 ModelAttributeMethodProcessor.resolveArgument()
方法。整个过程,我们也可以分为三个步骤来看。
根据参数类型,先生成一个目标类的构造函数,以供后面绑定数据的时候使用。
WebDataBinder
继承自 DataBinder
。而 DataBinder
主要的作用,简言之就是利用 BeanWrapper
给对象的属性设值。
在这里,又把 WebDataBinder
转换成 ServletRequestDataBinder
对象,然后调用它的bind方法。
接下来有个很重要的步骤是,将request中的参数转换为 MutablePropertyValuespvs
对象。
然后接下来就是循环pvs,调用 setPropertyValue
设置属性。当然了,最后调用的其实就是 BeanWrapperImpl#BeanPropertyHandler.setValue()
。
下面有段代码可以更好的理解这一过程,效果是一样的:
我们说所有的消息解析器都实现了 HandlerMethodArgumentResolver
接口。我们也可以定义一个参数解析器,让它实现这个接口就好了。
首先,我们可以定义一个 RequestXuner
注解。
然后是实现了 HandlerMethodArgumentResolver
接口的解析器类。
不要忘记需要配置一下。
一顿操作后,在Controller中我们可以这样使用它:
本文内容通过相关示例代码展示了Spring中部分解析器解析参数的过程。说到底,无论参数如何变化,参数类型再怎么复杂。
它们都是通过HTTP请求发送过来的,那么就可以通过 HttpServletRequest
来获取到一切。Spring做的就是通过注解,尽量适配大部分应用场景。