PathVariable 的参数解析场景
@PathVariable
的参数解析场景
本文对
@PathVariable
的参数解析场景进行了详细的源码分析,希望通过该具体场景,帮助理解以下内容:
- SpringMVC 请求处理的参数解析的大致流程
- 请求映射
RequestMappingHandlerMapping
的工作原理- 处理器适配器
RequestMappingHandlerAdapter
的工作原理- 数据绑定器
WebDataBinder
中的转换服务ConversionService
的工作原理
浏览器请求是:GET /car/3/owner/lisi
Controller
处理器定义是:
1 |
|
请求映射阶段
请求映射阶段的主要工作是:获取请求对应的处理器,并找到支持该处理器的处理器适配器
请求首先需要经过 HandlerMapping
来找到对应的处理器。
首先尝试
RequestMappingHandlerMapping
来进行处理,也就是查询MappingRegistry
是否有注册能够匹配该请求的RequestMappingInfo
,如果有,以RequestMappingInfo
为 key 即可找到对应的HandlerMethod
返回:先根据请求路径
/car/3/owner/lisi
查询是否存在对应的RequestMappingInfo
。MappingRegistry
中的urlLookup
存储的都是确定的 url 请求路径所对应的RequestMappingInfo
集合。但对于
@GetMapping("/car/{id}/owner/{username}")
注解,路径变量{id}
和{username}
的值是可变的,该@GetMapping
注解不存在一个确定的 url 请求路径,因此在urlLookup
中将不会存储@GetMapping("/car/{id}/owner/{username}")
对应的RequestMappingInfo
信息。所以,根据请求路径
/car/3/owner/lisi
到urlLookup
中将查询不到匹配的RequestMappingInfo
!刚才查不到任何匹配结果,那么第二步会将当前
MappingRegistry
中所有已注册的RequestMappingInfo
与请求request
进行匹配。RequestMappingInfo
中包含多个匹配条件,比如:请求方式条件methodsCondition
,请求路径模板条件patternsCondition
。请求
request
必须满足RequestMappingInfo
的所有条件,才表示找到匹配,但凡一个不满足,均匹配失败。对于
@GetMapping("/car/{id}/owner/{username}")
对应的RequestMappingInfo
,当前请求request
GET /car/3/owner/lisi
可以成功匹配!具体匹配情况是:- 请求方式是
GET
,满足methodsCondition
条件 - 请求路径
/car/3/owner/lisi
能够匹配模板/car/{id}/owner/{username}
,所以也满足patternsCondition
条件 - 当然,其他所有条件也都满足。
- 请求方式是
请求
request
在MappingRegistry
中找到第一个匹配成功的RequestMappingInfo
后,还会继续进行匹配,如果最后匹配到多个,一般会报错,因为一个请求,只能被一个RequestMappingInfo
匹配,从而保证只会被一个HandlerMethod
处理。找到匹配的
RequestMappingInfo
后,会再到MappingRegistry
中的mappingLookup
Map 集合中取出对应的HandlerMethod
,然后将RequestMappingInfo
和HandlerMethod
一并封装为Match
返回。
找到唯一的最佳匹配后,在将最佳匹配中封装的
HandlerMethod
返回之前,还需要做一些工作:使用PathMatcher
以及UrlPathHelper
对请求路径以及其他请求信息做一些处理,将处理结果设置到request
作用域中。比如对于当前包含路径变量的请求
/car/3/owner/lisi
,会使用PathMatcher
按照模板/car/{id}/owner/{username}
先解析出一个 Map 集合,其中包含两个键值对:("id", "3")
("username", "lisi")
,然后将该 Map 集合设置到request
作用域中,key 为HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE
。拿到返回的
HandlerMethod
后,将其再封装为HandlerExecutionChain
返回。HandlerExecutionChain
不为空,说明RequestMappingHandlerMapping
能够处理该请求,后续不再检查其他的HandlerMapping
实现能否处理请求。HandlerExecutionChain
中除了拿到的HandlerMethod
以外,还包括与当前请求路径匹配的所有拦截器,目前我们先不用关心拦截器相关的事情。这里需要清楚一点:对于不同的
HandlerMapping
实现,其最终返回的处理器的类型是不同的,比如RequestMappingHandlerMapping
返回的是HandlerMethod
,其他实现类一般返回的都是Object
。根据返回的
HandlerExecutionChain
处理器执行链中的处理器,找到支持该处理器的处理器适配器HandlerAdapter
。首先尝试
RequestMappingHandlerAdapter
是否支持,RequestMappingHandlerAdapter
的支持逻辑是:只要提供的处理器是HandlerMethod
类型就宣布支持。所以
RequestMappingHandlerAdapter
能支持当前的处理器,直接将其返回,后续不再检查其他的HandlerAdapter
实现能否支持处理器。
以上的第 1, 2 步的代码详情看:
1 | // AbstractHandlerMethodMapping.java |
请求处理执行阶段
请求处理执行阶段的主要工作有三个:
- 先通过参数解析器,从请求中解析得到处理器方法的所有实参
- 然后传入实参,反射执行处理器方法,得到结果
- 通过返回值处理器,将结果处理后返回
这里我们仅仅关注第一个工作:通过参数解析器,从请求中解析得到处理器方法的所有实参。
RequestMappingHandlerAdapter
处理器适配器简介
RequestMappingHandlerAdapter
对于动态请求场景,也即处理器是 HandlerMethod
类型的情况提供了非常多的支持。
内部属性
argumentResolvers
就包括了很多参数解析器HandlerMethodArgumentResolver
,来对HandlerMethod
处理器方法上的所有参数进行解析:根据不同的参数MethodParameter
情况,选择不同的参数解析器从请求中解析出相应的值作为后续调用处理器方法的实参。对于
@PathVariable
标注的处理器方法参数,会使用PathVariableMethodArgumentResolver
进行解析对于使用
@RequestBody
标注的处理器方法参数,会选择RequestResponseBodyMethodProcessor
进行解析,RequestResponseBodyMethodProcessor
内部包括了很多的报文转换器HttpMessageConverter
,彼时将根据请求信息,处理器方法参数类型等信息,选择合适的报文转换器,来将请求报文转换为相应的处理器方法参数。内部属性
returnValueHandlers
就包括了很多返回值处理器HandlerMethodReturnValueHandler
,来对HandlerMethod
处理器方法的返回值进行处理:根据不同的返回情况,比如返回类型不同,方法上是否标注了@ResponseBody
注解等,来选择不同的返回值处理器进行处理。对于使用
@ResponseBody
标注的处理器方法,同样是会选择RequestResponseBodyMethodProcessor
进行返回值处理,其内部诸多的报文转换器彼时会根据请求信息,处理器方法返回值类型等信息,选择合适的报文转换器,来将处理器方法返回值,转换为相应的响应报文。内部属性
webBindingInitializer
是绑定初始化器,该属性内部又包括了一个转换服务conversionService
,该转换服务中包括了很多转换器Converters
。该属性在后续会用于创建数据绑定工厂
WebDataBinderFactory
,数据绑定工厂将用于生产数据绑定器WebDataBinder
,数据绑定器主要负责参数解析中的最后一步:将从请求中得到的原始数据,绑定到后续将传递给HandlerMethod
处理器方法的实参上。因此,对于每个处理器方法参数,相应的参数解析器在完成初步的解析后,就会使用
WebDataBinderFactory
创建一个数据绑定器WebDataBinder
,通过该数据绑定器来完成具体的绑定工作。对于简单类型的处理器方法参数,数据绑定器的主要工作一般只是完成转换。比如
@PathVariable("id") Integer id
,初步解析时得到的路径变量值是java.lang.String
类型的,为了完成绑定,数据绑定器会使用内部的转换服务进行类型转换,最终得到java.lang.Integer
类型的实参。数据绑定器内部的转换服务,其实就是从webBindingInitializer
绑定初始化器中的转换服务conversionService
。对于非简单类型的处理器方法参数,数据绑定器则同时需要进行数据绑定和转换。比如没有带任何注解的
User user
类型的形参,初步解析时会通过反射调用无参构造器创建一个属性值均为默认值的User
对象,可称之为壳对象,然后数据绑定器内部会解析请求获得所有的请求参数,然后底层借助BeanWrapperImpl
的强大功能来最终反射调用User
的 setter 方法来完成属性值的注入,同时在类型不兼容时,会先使用内部的转换服务进行类型转换然后再注入。WebDataBinderFactory
创建数据绑定器WebDataBinder
时,需要提供三个重要参数:原始的请求,需要绑定到的目标对象(简单类型无需提供),目标对象的命名。
综上,可以将处理器适配器视为一个处理特定请求场景的工具箱,其中的属性就是所封装的辅助请求处理的大量工具。
请求处理执行阶段的初始化工作
将当前请求
request
和response
打包为ServletWebRequest
。创建
ServletInvocableHandlerMethod
来包装HandlerMethod
,并为其装配RequestMappingHandlerAdapter
中定制的大量工具,包括:- 参数解析器
argumentResolvers
- 返回值处理器
returnValueHandlers
- 数据绑定工厂
WebDataBinderFactory
- 参数解析器
创建
ModelAndViewContainer
对象,可以将该对象视为一次请求的上下文对象。如果处理器方法上有
Model
ModelMap
以及不带注解的Map
类型参数,则后续会将该对象内部的defaultModel
属性作为对应的实参。因此对于页面开发的情况,向
Model
参数保存键值对,其实就是保存到ModelAndViewContainer
对象内部的defaultModel
属性中;同时返回的视图名称,最终也会保存到ModelAndViewContainer
对象内部的view
属性中。
至此,ServletInvocableHandlerMethod
将 HandlerMethod
以及用于支持 HandlerMethod
调用执行的一大堆工具全都封装到内部了。接下来,是时候正式开始调用处理 HandlerMethod
了。
以上初始化过程的代码详情看:
1 | // RequestMappingHandlerMapping.java |
处理器方法参数解析
首先拿到的
HandlerMethod
的所有形参列表中的所有参数,将每个形参的信息,比如所标注的注解,具体的类型,都封装为一个MethodParameter
对象返回,全部形参则返回一个MethodParameter[] parameters
数组。创建一个
Object[] args
数组,长度为parameters.length
,该数组中的每个元素初始均为null
,但最终将保存解析得到的实参结果。遍历
parameters
数组,对取出的每个MethodParameter
选择合适的参数解析器进行解析,解析的结果保存到args
数组中。
以上第 1, 2, 3 步的代码详情看:
1 | // InvocableHandlerMethod.java |
对于 @PathVariable("id") Integer id
对应的 MethodParameter
,会选择 PathVariableMethodArgumentResolver
来进行参数解析。
PathVariableMethodArgumentResolver
的解析过程主要分为两步:根据 @PathVariable("id")
中的 id
从请求路径中获取相应的字符串值,然后利用数据绑定器内部的转换服务,将字符串路径变量值从 java.lang.String
转换为处理器方法形参对应的 java.lang.Integer
。具体代码如下所示:
1 | // AbstractNamedValueMethodArgumentResolver.java |
我们先来解析 resolveName()
的执行,该方法会通过动态绑定,调用子类 PathVariableMethodArgumentResolver
中重写的 resolveName()
方法。
在请求映射阶段,我们就已经通过 PathMatcher
对请求路径进行了处理,以 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE
为 key 向请求域中放入了一个 Map 集合,该 Map 集合中存放了 ("id", "3")
和 ("username", "lisi")
。
因此 PathVariableMethodArgumentResolver
的 resolveName()
方法就是先从请求域中获取 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE
为 key 的 Map 集合,然后执行 map.get("id")
拿到字符串路径变量值 "3"
,将其返回。具体代码如下所示:
1 | // PathVariableMethodArgumentResolver.java |
SpringMVC 数据绑定器的转换工作 binder.convertIfNecessary()
被封装了非常多层,但所做的工作,用一句话就可以概括清楚:根据原始类型信息和目标类型信息从 conversionService
转换服务中选取合适的转换器 Converter
来进行转换工作,如下面的代码所示:
1 | // TypeConverterDelegate.java |
ConversionService
对象就是最开始 RequestMappingHandlerAdapter
内部 webBindingInitializer
中的转换服务,它有两个重要的属性:
1 | // 保存了一系列 Spring 提供好的转换器 |
目前我们的原始类型信息是 java.lang.String
,目标类型信息是 @PathVariable java.lang.Integer
,它们都是 TypeDescriptor
对象,不仅记录了类型,还记录了注解信息。
数据绑定器首先将 java.lang.String
和 @PathVariable java.lang.Integer
封装为一个 ConverterCacheKey
作为 key,尝试先从转换器缓存 converterCache
中获取转换器。
第一次执行时,转换器缓存为空,因此从缓存中获取不到对应的转换器,此时去到 converters
属性中查找合适的转换器。
converters
属性内部封装了一个 Map<ConvertiblePair, ConvertersForPair> converters
转换器 Map 集合,该 Map 集合中保存了一系列 Spring 提供好的转换器。
ConvertiblePair
内部封装的是原始类型和目标类型,区别于之前提到的 TypeDescriptor
,ConvertiblePair
内部的原始类型和目标类型是 Class
对象,也即仅保存类型,不关心注解信息。通过它可以在 converters
中快速定位到一个 ConvertersForPair
对象。
ConvertersForPair
内部则保存了一个的转换器列表 LinkedList<GenericConverter> converters
,列表中的所有转换器都满足键 ConvertiblePair
的原始类型和目标类型要求,但在类型以外的信息上各自又存在差异。
需要注意的是:当在缓存中找不到时,SpringMVC 并不是直接将原始类型和目标类型,直接封装为 ConvertiblePair
去 converters
中查找。
SpringMVC 会先分析原始类型和目标类型的各自的继承树,得到所有候选原始类型,以及所有候选目标类型。
SpringMVC 会从两个候选列表中,按顺序各自选出一个组成一对封装为 ConvertiblePair
,然后才到 converters
属性中查找合适的转换器。
对于原始类型 java.lang.String
,目标类型 java.lang.Integer
,得到的候选类型如下所示:
1 | // 原始类型 java.lang.String 的候选原始类型 |
候选类型使用 ArrayList
保存,且其中元素具有优先级。
SpringMVC 先将 java.lang.String
java.lang.Integer
组成一对封装为 ConvertiblePair
,到 converters
中查找,发现能找到对应的 ConvertersForPair
,其内部的转换器列表只包含一个适配 java.lang.String -> @NumberFormat java.lang.Integer
的转换器,该转换器要求目标参数必须是 java.lang.Integer
且必须标注了 @NumberFormat
注解,我们的目标参数不满足后者,因此本轮查找失败。
进入下一轮查找,保持候选原始类型不变,尝试 java.lang.String
java.lang.Number
组成一对封装为 ConvertiblePair
,到 converters
中查找,发现能找到对应的 ConvertersForPair
,其内部的转换器列表只包含一个适配 java.lang.String -> java.lang.Number: StringToNumberConverterFactory
的转换器,该转换器对目标参数没有其他要求,因此本次查找成功,该转换器将会放到缓存 converterCache
中,并最后返回。
以上查找转换器的过程的具体代码如下:
1 | // GenericConversionService.java |
查找到 java.lang.String -> java.lang.Number: StringToNumberConverterFactory
转换器以后,该转换器底层具体是怎么转换的呢?
目前我们获取到的转换器,其实还只是一个转换器工厂适配器,其内部保存了一个 converterFactory
转换器工厂属性,类型为 StringToNumberConverterFactory
。
在进行转换时,为了确保最终得到的是 java.lang.Integer
类型的数据,该转换器工厂适配器会使用 converterFactory
转换器工厂根据原始目标类型 java.lang.Integer
生产一个最终的转换器。
最终的转换器调用 Spring 的工具类 NumberUtils.parseNumber(source, this.targetType)
进行转换,this.targetType
就是最终的转换器内部记录的原始目标类型 java.lang.Integer
,工具类底层其实就是调用 Integer.valueOf(source)
进行转换。
以上转换器转换的具体代码如下:
1 | // ConversionUtils.java |