一、回顾
在 《Spring-MVC请求参数和响应结果解析》 一文中,提到了 Spring MVC 在进行请求处理提供了 HandlerMethodArgumentResolver(方法参数解析器)集合 和 HandlerMethodReturnValueHandler(返回结果值解析器)集合 分别对请求参数和返回结果进行数值操作设置(如:入参的类型转换,返回结果的类型转换)。
二、表单入参 和 JSON格式入参
在日常开发中,针对入参通常有两种接收方式:
- 表单格式接收
- JSON格式接收
在日常开发中,针对入参的处理我们可以使用注解的方式对入参格式进行类型转换。
例如:对前端的入参时间格式进行类型转换
- 表单接收使用:@DateTimeFormat
- JSON 格式接收使用:@JsonFormat
三、常用的参数类型转换注解的使用
表单类型入参:时间类型转换
JSON 类型入参:时间类型转换
针对这两种类型的入参,在 Spring MVC 中对应了不同的 Resolver(参数解析器)
表单入参对应的 Resolver 为:ServletModelAttributeMethodProcessor
JSON入参对应的 Resolver 为:RequestResponseBodyMethodProcessor
四、源码分析
通过源码,分析一下这两个 Resolver 解析器是如何工作
入口:DispatcherServlet#doDispatch
以 DispatcherServlet#doDispatch 作为入口进行追踪,追到 InvocableHandlerMethod#getMethodArgumentValues 该方法。
先来看看该方法代码
public class InvocableHandlerMethod extends HandlerMethod {protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {// 获取所有方法参数MethodParameter[] parameters = this.getMethodParameters();Object[] args = new Object[parameters.length];for(int i = 0; i < parameters.length; ++i) {// 遍历每个方法参数,并堆每个方法参数根据 Resolver 进行解析args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);}return args;}}
主线逻辑:
1、获取所有参数值
2、根据入参获取对应的 Resolver 进行参数解析
3、将所有参数通过 Resolver 进行处理之后,重新放入新的参数对象 args 中进行返回。
代码很直观,遍历所有的入参,然后调用 Resolvers 解析器对入参进行操作,完成之后得到解析之后的值,拼接新的参数对象 args 。然后以新的参数对象 args 作为入参进行方法调用。
以 HandlerMethodArgumentResolverComposite#resolveArgument 作为入口,分别对 表单入参 和 JSON 格式入参 进行简单的源码分析。
HandlerMethodArgumentResolverComposite#resolveArgument 为入口
先来看看 HandlerMethodArgumentResolverComposite#resolveArgument 代码
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {@Override@Nullablepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);if (resolver == null) {throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]." +" supportsParameter should be called first.");}return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}}
上面的代码也很简单:根据前面入参的封装对象 MethodParameter 作为参数,获取该参数对应的 Resolver (解析器),然后调用对应的 Resolver (解析器)的 resolveArgument 方法对入参进行解析。
针对表单入参和JSON入参的分析,就得从两者对应的 解析器入手。
4.1、表单入参类型转换-解析器
(以 @DateTimeFormat 为例)表单参数解析对应的 Resolver 为: ServletModelAttributeMethodProcessor
ServletModelAttributeMethodProcessor 类图结构如下:
聚焦 ServletModelAttributeMethodProcessor#resolveArgument
该方法由其父类 ModelAttributeMethodProcessor#resolveArgument 实现)
查看代码:ModelAttributeMethodProcessor#resolveArgument
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {@Override@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {// 新的返回结果:这里为:FromDTOObject attribute = null;// 创建 web 数据绑定器物// webRequest :当前请求(其中包含的入参参数集合 paramters)// attribute:为入参映射对象 FromDTO// name :为入参映射对象名字(默认:类首字母小写)这里为:fromDTOWebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);// 进行参数绑定,将请求 webRequest 中的入参,绑定到对应的映射对象 FromDTO 中this.bindRequestParameters(binder, webRequest);return attribute;}protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {((WebRequestDataBinder) binder).bind(request);}}
代码主线逻辑:对参数进行绑定操作。
代码很直观:将请求中的所有入参,映射到我们指定的接收对象 FromDTO 每个参数中,然后返回。
其中的主线代码为:ModelAttributeMethodProcessor#bindRequestParameters ,而真正执行参数绑定逻辑的是 ③ ServletRequestDataBinder#bind
针对表单入参对应的参数绑定类ServletRequestDataBinder
以 ③ ServletRequestDataBinder#bind 为入口继续追踪代码
ServletRequestDataBinder 类图结果如下:
代码追踪 UML 图:
查看 ⑧ AbstractNestablePropertyAccessor#setPropertyValues 代码
public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor {protected void setPropertyValue(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {if (tokens.keys != null) {this.processKeyedProperty(tokens, pv);} else {this.processLocalProperty(tokens, pv);}}private void processLocalProperty(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) {// 入参原来的值Object originalValue = pv.getValue();// 新的参数值:默认等于原始入参值Object valueToApply = originalValue;// 进行类型转换的执行valueToApply = this.convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());// 重新设置ph.setValue(valueToApply);}}
代码中的主线逻辑:
1、获取入参的原始值:originalValue
2、定义一个新的入参的值:valueToApply,该值等于原始值。
3、调用 AbstractNestablePropertyAccessor#convertForProperty 进行新的类型值的获取
4、将新的值 valueToApply 作为新的参数返回。
到这里就能够看到,类型转换代码 AbstractNestablePropertyAccessor#convertForProperty。
继续追代码。
查看 AbstractNestablePropertyAccessor#convertForProperty 代码。
public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor {@Nullableprotected Object convertForProperty(String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) throws TypeMismatchException {return this.convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);}private Object convertIfNecessary(......){// 类型值的转换委派给了 typeConverterDelegate 来执行return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);}}
到这里就能够看到类型转换的操作了,代码运行到这里直接把参数类型转换的操作委派给了 TypeConverterDelegate 来执行。
直接上代码TypeConverterDelegate#convertIfNecessary
class TypeConverterDelegate {public <T> T convertIfNecessary(......){// 获取相关的转化服务类ConversionService conversionService = this.propertyEditorRegistry.getConversionService();// 其中:sourceTypeDesc 为入参原始类型// typeDescriptor 为需要转化的类型if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {try {return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);} catch (ConversionFailedException var14) { conversionAttemptEx = var14; }}}}
主线逻辑:【以 @DateTimeFormat 为例】
1、获取所有类型转换的相关类的集合 ConversionService
2、根据入参原始类型,和入参转换类型调用 GenericConversionService#canConvert 判定,相关转换的类是否存在
3、当对应的类型转换类存在时调用 GenericConversionService#convert 方法,获取对应的类型转化类,同时调用其 convert 方法对参数类型进行转化
还是维护的一个 List 集合,然后挨个调用 canConvert 判断是否支持被转换,支持的话直接调用的 convert
以 @DateTimeFormat 为例:此时 GenericConversionService#convert 获取到String -> LocalDateTime 类型转换的类为:FormattingConversionService 。通过调用其:FormattingConversionService#convert 实现 String -> LocalDateTime 的类型转换。(FormattingConversionService#convert 具体实现不展开)
至此,对于表单入参类型转换的整个流程有了清晰地认识:
1、针对入参,Spring MVC 提供有相关的 HandlerMethodArgumentResolver 方法参数解析器
2、表单入参的 HandlerMethodArgumentResolver 解析器,会针对每个参数进行尝试类型转换。入参通常为:String 类型,而转换结果根据方法对象中的字段类型为主,查找匹配的类型转换器进行参数转化,并返回转化后的结果。
4.2、JSON入参类型转换源码分析(以@JsonFormat为例 )
JSON格式入参对应的解析器为:RequestResponseBodyMethodProcessor
查看代码 RequestResponseBodyMethodProcessor#resolveArgument
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {@Overridepublic Object resolveArgument(......){// 对入参进行解析Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());}@Overrideprotected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {Object arg = readWithMessageConverters(inputMessage, parameter, paramType);return arg;}}
主线逻辑很直观,在 RequestResponseBodyMethodProcessor#resolveArgument 中,直接调用其父类 AbstractMessageConverterMethodProcessor#readWithMessageConverters 进行相关入参的处理操作
直接查看相关代码
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {@Nullableprotected <T> Object readWithMessageConverters(......){for (HttpMessageConverter<?> converter : this.messageConverters) {if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :(targetClass != null && converter.canRead(targetClass, contentType))) {// JSON 入参从 body 中获取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);}}}return body;}}
针对 JSON 参数类型转换的类为 HttpMessageConverter 实现类 MappingJackson2HttpMessageConverter 看到该类之后,大体就能够猜测得到相关的处理流程。Spring Boot中集成了 Jackson 框架作为 json入参,和返回值的解析工具。
主线逻辑:
1、调用 MappingJackson2HttpMessageConverter#canRead 判定入参是否符合两个条件,条件一:Content-Type 符合 application/json 或者 application/ + json。条件二:入参映射对象允许被反序列化(在该demo中,也就是 JsonDTO 是否允许被反序列化)
2、当符合两个条件,并且 request 的 body 中有入参数据,调用 *MappingJackson2HttpMessageConverter#read 进行入参到对象的映射和转换。(在该demo中,就是 request 中body的请求入参到JsonDTO对象的映射)
略过 MappingJackson2HttpMessageConverter#canRead 的判断逻辑,直接来看 MappingJackson2HttpMessageConverter#read。而该方法由其父类 AbstractJackson2HttpMessageConverter#read 实现.
直接查看 AbstractJackson2HttpMessageConverter#read 代码
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {@Overridepublic Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException {JavaType javaType = getJavaType(type, contextClass);return readJavaType(javaType, inputMessage);}private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {return this.objectMapper.readValue(inputMessage.getBody(), javaType);}}
主线逻辑:
1、将入参映射的对象封装成 JavaType 对象
在该 demo 中,将 JsonDTO 封装成 JavaType 对象
2、从 reqquest body 中获取参数入参值,调用 ObjectMapper#readValue 进行对象解析设值。
直接来看 ObjectMapper#readValue 相关逻辑
public class ObjectMapper extends ObjectCodec implements Versioned,java.io.Serializable{ // as of 2.1{public <T> T readValue(InputStream src, JavaType valueType)throws IOException, JsonParseException, JsonMappingException{return (T) _readMapAndClose(_jsonFactory.createParser(src), valueType);}protected Object _readMapAndClose(JsonParser p0, JavaType valueType) throws IOException{final DeserializationConfig cfg = getDeserializationConfig();final DeserializationContext ctxt = createDeserializationContext(p, cfg);JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);result = deser.deserialize(p, ctxt);return result;}}
到这里,结果基本就呼之欲出了,这里的代码逻辑操作为 Jackson 框架的使用
主线逻辑:
1、将 Request 中的 body 作为输入流,构造 JsonParser 对象
2、根据映射对象类型 JavaType 构建 反序列化上下文,同时构建 JSON 反序列化处理类:BeanDeserializer
3、调用 BeanDeserializer#deserialize 进行反序列化设值操作
在构建完,Jackson 相关处理参数直接,进入到反序列化操作中,直接上代码
public class BeanDeserializer extends BeanDeserializerBase implements java.io.Serializable{@Overridepublic Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{return deserializeFromObject(p, ctxt);}@Overridepublic Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException{// 构造入参构建对象:例如:JSONDtofinal Object bean = _valueInstantiator.createUsingDefault(ctxt);// 遍历每个入参对象的值,并进行设值操作if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {// 获取入参名称:例如:JSONDTO 中的 time 属性String propName = p.getCurrentName();do {p.nextToken();// 根据入参名称,获取对应的 SettableBeanPropert(这里为:MethodProperty)SettableBeanProperty prop = _beanProperties.find(propName);if (prop != null) { // normal casetry {// 调用对应 MethodProperty 中的 对应的反序列化处理器,进行反序列化设值操作prop.deserializeAndSet(p, ctxt, bean);} catch (Exception e) {wrapAndThrow(e, bean, propName, ctxt);}continue;}handleUnknownVanilla(p, ctxt, bean, propName);} while ((propName = p.nextFieldName()) != null);}return bean;}}
主线逻辑:
1、根据传递过来的 DeserializationContext 构造入参映射对象(这里为:JsonDTO)。
2、遍历入参映射对象(这里为 JsonDTO)的每个参数,并获取每个参数对应的 MethodPeoperty 。
3、调用 MethodPeoperty#deserializeAndSet ,进行反序列化设值操作。
下面来看看 MethodPeoperty#deserializeAndSet 的相关逻辑代码
public final class MethodProperty extends SettableBeanProperty{public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,Object instance) throws IOException{value = _valueDeserializer.deserialize(p, ctxt);}}
关键代码只有一行,调用 _valueDeserializer 的反序列化方法进行值的获取。这里的 _valueDeserializer 就是每个参数类型对应的反序列化处理类。
。针对每一种入参映射类型有对应的反序列化类。例如:JsonDTO 中的 time 属性的类型为 Date,则对应的反序列化处理类为:DateDeserializer,如果 JsonDTO 中的另一个属性 author 类型为 String ,则对应的反序列化处理为:StringDeserializer。
(这里不对 DateDeserializer 进行展开)
至此,对于 JSON 格式入参有一个比较直观的认识:在 JSON 格式入参类型转换的过程中,实际使用的是 Jackson 框架,进行 json 入参的解析。
五、总结
5.1、表单入参解析
Spring MVC 中,表单入参对应的参数解析类为:ServletModelAttributeMethodProcessor
该类对每个入参进行遍历操作,获取对应匹配的类型转换器,并通过类型类型转换器对入参进行类型转换操作。如果获取不到对应的类型转换器,直接原值返回。
来看看表单入参支持的一些解析类:
在图中,有很多类型转换的处理类,如:
- String 转换 Boolean
- String 转换 Integer
- String 转换 Locale
- String 转换 Long
每一种转换的实现类也有很多,比如图中的 String 转换 Long 中,有三个类型转换实现类。
遍历这些类型转换器,匹配对应的类型转换,进行类型转换。
5.2、Json 格式入参解析
Spring MVC 中,Json 格式入参对应的参数解析类为:RequestResponseBodyMethodProcessor
该类在 Spring boot2.0 默认使用 Jackson 框架作为解析工具,使用 Jackson 来对 Json 入参进行解析映射。
来看看 Jackson 默认提供的几种类型反序列类
