一、回顾

《Spring-MVC请求参数和响应结果解析》 一文中,提到了 Spring MVC 在进行请求处理提供了 HandlerMethodArgumentResolver(方法参数解析器)集合HandlerMethodReturnValueHandler(返回结果值解析器)集合 分别对请求参数和返回结果进行数值操作设置(如:入参的类型转换,返回结果的类型转换)。

二、表单入参 和 JSON格式入参

在日常开发中,针对入参通常有两种接收方式:

  • 表单格式接收
  • JSON格式接收

在日常开发中,针对入参的处理我们可以使用注解的方式对入参格式进行类型转换。
例如:对前端的入参时间格式进行类型转换

三、常用的参数类型转换注解的使用

表单类型入参:时间类型转换
05-01.png
JSON 类型入参:时间类型转换
05-02.png
针对这两种类型的入参,在 Spring MVC 中对应了不同的 Resolver(参数解析器)

表单入参对应的 Resolver 为:ServletModelAttributeMethodProcessor
JSON入参对应的 Resolver 为:RequestResponseBodyMethodProcessor

四、源码分析

通过源码,分析一下这两个 Resolver 解析器是如何工作

入口:DispatcherServlet#doDispatch

DispatcherServlet#doDispatch 作为入口进行追踪,追到 InvocableHandlerMethod#getMethodArgumentValues 该方法。

先来看看该方法代码

  1. public class InvocableHandlerMethod extends HandlerMethod {
  2. protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
  3. // 获取所有方法参数
  4. MethodParameter[] parameters = this.getMethodParameters();
  5. Object[] args = new Object[parameters.length];
  6. for(int i = 0; i < parameters.length; ++i) {
  7. // 遍历每个方法参数,并堆每个方法参数根据 Resolver 进行解析
  8. args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
  9. }
  10. return args;
  11. }
  12. }

主线逻辑:
1、获取所有参数值

2、根据入参获取对应的 Resolver 进行参数解析

3、将所有参数通过 Resolver 进行处理之后,重新放入新的参数对象 args 中进行返回。

代码很直观,遍历所有的入参,然后调用 Resolvers 解析器对入参进行操作,完成之后得到解析之后的值,拼接新的参数对象 args 。然后以新的参数对象 args 作为入参进行方法调用。

HandlerMethodArgumentResolverComposite#resolveArgument 作为入口,分别对 表单入参JSON 格式入参 进行简单的源码分析。

HandlerMethodArgumentResolverComposite#resolveArgument 为入口

先来看看 HandlerMethodArgumentResolverComposite#resolveArgument 代码

  1. public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
  2. @Override
  3. @Nullable
  4. public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  5. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  6. HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  7. if (resolver == null) {
  8. throw new IllegalArgumentException(
  9. "Unsupported parameter type [" + parameter.getParameterType().getName() + "]." +
  10. " supportsParameter should be called first.");
  11. }
  12. return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  13. }
  14. }

上面的代码也很简单:根据前面入参的封装对象 MethodParameter 作为参数,获取该参数对应的 Resolver (解析器),然后调用对应的 Resolver (解析器)的 resolveArgument 方法对入参进行解析。

针对表单入参和JSON入参的分析,就得从两者对应的 解析器入手。

4.1、表单入参类型转换-解析器

(以 @DateTimeFormat 为例)表单参数解析对应的 Resolver 为: ServletModelAttributeMethodProcessor

ServletModelAttributeMethodProcessor 类图结构如下:
05.png

聚焦 ServletModelAttributeMethodProcessor#resolveArgument

该方法由其父类 ModelAttributeMethodProcessor#resolveArgument 实现
06.png
查看代码:ModelAttributeMethodProcessor#resolveArgument

  1. public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
  2. @Override
  3. @Nullable
  4. public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  5. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  6. // 新的返回结果:这里为:FromDTO
  7. Object attribute = null;
  8. // 创建 web 数据绑定器物
  9. // webRequest :当前请求(其中包含的入参参数集合 paramters)
  10. // attribute:为入参映射对象 FromDTO
  11. // name :为入参映射对象名字(默认:类首字母小写)这里为:fromDTO
  12. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
  13. // 进行参数绑定,将请求 webRequest 中的入参,绑定到对应的映射对象 FromDTO 中
  14. this.bindRequestParameters(binder, webRequest);
  15. return attribute;
  16. }
  17. protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
  18. ((WebRequestDataBinder) binder).bind(request);
  19. }
  20. }

代码主线逻辑:对参数进行绑定操作。

代码很直观:将请求中的所有入参,映射到我们指定的接收对象 FromDTO 每个参数中,然后返回。

其中的主线代码为:ModelAttributeMethodProcessor#bindRequestParameters ,而真正执行参数绑定逻辑的是 ③ ServletRequestDataBinder#bind

针对表单入参对应的参数绑定类ServletRequestDataBinder

③ ServletRequestDataBinder#bind 为入口继续追踪代码

ServletRequestDataBinder 类图结果如下:
07.png
代码追踪 UML 图:
08.png

查看 ⑧ AbstractNestablePropertyAccessor#setPropertyValues 代码

  1. public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor {
  2. protected void setPropertyValue(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
  3. if (tokens.keys != null) {
  4. this.processKeyedProperty(tokens, pv);
  5. } else {
  6. this.processLocalProperty(tokens, pv);
  7. }
  8. }
  9. private void processLocalProperty(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) {
  10. // 入参原来的值
  11. Object originalValue = pv.getValue();
  12. // 新的参数值:默认等于原始入参值
  13. Object valueToApply = originalValue;
  14. // 进行类型转换的执行
  15. valueToApply = this.convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
  16. // 重新设置
  17. ph.setValue(valueToApply);
  18. }
  19. }

代码中的主线逻辑:

1、获取入参的原始值:originalValue

2、定义一个新的入参的值:valueToApply,该值等于原始值。

3、调用 AbstractNestablePropertyAccessor#convertForProperty 进行新的类型值的获取

4、将新的值 valueToApply 作为新的参数返回。

到这里就能够看到,类型转换代码 AbstractNestablePropertyAccessor#convertForProperty

继续追代码。

查看 AbstractNestablePropertyAccessor#convertForProperty 代码。

  1. public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor {
  2. @Nullable
  3. protected Object convertForProperty(String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) throws TypeMismatchException {
  4. return this.convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
  5. }
  6. private Object convertIfNecessary(......){
  7. // 类型值的转换委派给了 typeConverterDelegate 来执行
  8. return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
  9. }
  10. }

到这里就能够看到类型转换的操作了,代码运行到这里直接把参数类型转换的操作委派给了 TypeConverterDelegate 来执行。

直接上代码TypeConverterDelegate#convertIfNecessary

  1. class TypeConverterDelegate {
  2. public <T> T convertIfNecessary(......){
  3. // 获取相关的转化服务类
  4. ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
  5. // 其中:sourceTypeDesc 为入参原始类型
  6. // typeDescriptor 为需要转化的类型
  7. if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
  8. try {
  9. return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
  10. } catch (ConversionFailedException var14) { conversionAttemptEx = var14; }
  11. }
  12. }
  13. }

主线逻辑:【以 @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

  1. public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  2. @Override
  3. public Object resolveArgument(......){
  4. // 对入参进行解析
  5. Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
  6. }
  7. @Override
  8. protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
  9. Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
  10. Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
  11. return arg;
  12. }
  13. }

主线逻辑很直观,在 RequestResponseBodyMethodProcessor#resolveArgument 中,直接调用其父类 AbstractMessageConverterMethodProcessor#readWithMessageConverters 进行相关入参的处理操作

直接查看相关代码

  1. public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
  2. @Nullable
  3. protected <T> Object readWithMessageConverters(......){
  4. for (HttpMessageConverter<?> converter : this.messageConverters) {
  5. if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
  6. (targetClass != null && converter.canRead(targetClass, contentType))) {
  7. // JSON 入参从 body 中获取
  8. if (message.hasBody()) {
  9. HttpInputMessage msgToUse =
  10. getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
  11. body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
  12. ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
  13. body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
  14. }
  15. }
  16. }
  17. return body;
  18. }
  19. }

针对 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 代码

  1. public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
  2. @Override
  3. public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
  4. throws IOException, HttpMessageNotReadableException {
  5. JavaType javaType = getJavaType(type, contextClass);
  6. return readJavaType(javaType, inputMessage);
  7. }
  8. private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
  9. return this.objectMapper.readValue(inputMessage.getBody(), javaType);
  10. }
  11. }

主线逻辑:
1、将入参映射的对象封装成 JavaType 对象

在该 demo 中,将 JsonDTO 封装成 JavaType 对象

2、从 reqquest body 中获取参数入参值,调用 ObjectMapper#readValue 进行对象解析设值。

直接来看 ObjectMapper#readValue 相关逻辑

  1. public class ObjectMapper extends ObjectCodec implements Versioned,java.io.Serializable{ // as of 2.1{
  2. public <T> T readValue(InputStream src, JavaType valueType)
  3. throws IOException, JsonParseException, JsonMappingException{
  4. return (T) _readMapAndClose(_jsonFactory.createParser(src), valueType);
  5. }
  6. protected Object _readMapAndClose(JsonParser p0, JavaType valueType) throws IOException{
  7. final DeserializationConfig cfg = getDeserializationConfig();
  8. final DeserializationContext ctxt = createDeserializationContext(p, cfg);
  9. JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
  10. result = deser.deserialize(p, ctxt);
  11. return result;
  12. }
  13. }

到这里,结果基本就呼之欲出了,这里的代码逻辑操作为 Jackson 框架的使用

主线逻辑:
1、将 Request 中的 body 作为输入流,构造 JsonParser 对象
2、根据映射对象类型 JavaType 构建 反序列化上下文,同时构建 JSON 反序列化处理类:BeanDeserializer
3、调用 BeanDeserializer#deserialize 进行反序列化设值操作

在构建完,Jackson 相关处理参数直接,进入到反序列化操作中,直接上代码

  1. public class BeanDeserializer extends BeanDeserializerBase implements java.io.Serializable{
  2. @Override
  3. public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
  4. return deserializeFromObject(p, ctxt);
  5. }
  6. @Override
  7. public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException{
  8. // 构造入参构建对象:例如:JSONDto
  9. final Object bean = _valueInstantiator.createUsingDefault(ctxt);
  10. // 遍历每个入参对象的值,并进行设值操作
  11. if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
  12. // 获取入参名称:例如:JSONDTO 中的 time 属性
  13. String propName = p.getCurrentName();
  14. do {
  15. p.nextToken();
  16. // 根据入参名称,获取对应的 SettableBeanPropert(这里为:MethodProperty)
  17. SettableBeanProperty prop = _beanProperties.find(propName);
  18. if (prop != null) { // normal case
  19. try {
  20. // 调用对应 MethodProperty 中的 对应的反序列化处理器,进行反序列化设值操作
  21. prop.deserializeAndSet(p, ctxt, bean);
  22. } catch (Exception e) {
  23. wrapAndThrow(e, bean, propName, ctxt);
  24. }
  25. continue;
  26. }
  27. handleUnknownVanilla(p, ctxt, bean, propName);
  28. } while ((propName = p.nextFieldName()) != null);
  29. }
  30. return bean;
  31. }
  32. }

主线逻辑:

1、根据传递过来的 DeserializationContext 构造入参映射对象(这里为:JsonDTO)。
2、遍历入参映射对象(这里为 JsonDTO)的每个参数,并获取每个参数对应的 MethodPeoperty 。
3、调用 MethodPeoperty#deserializeAndSet ,进行反序列化设值操作。

下面来看看 MethodPeoperty#deserializeAndSet 的相关逻辑代码

  1. public final class MethodProperty extends SettableBeanProperty{
  2. public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,Object instance) throws IOException{
  3. value = _valueDeserializer.deserialize(p, ctxt);
  4. }
  5. }

关键代码只有一行,调用 _valueDeserializer 的反序列化方法进行值的获取。这里的 _valueDeserializer 就是每个参数类型对应的反序列化处理类。

。针对每一种入参映射类型有对应的反序列化类。例如:JsonDTO 中的 time 属性的类型为 Date,则对应的反序列化处理类为:DateDeserializer,如果 JsonDTO 中的另一个属性 author 类型为 String ,则对应的反序列化处理为:StringDeserializer
(这里不对 DateDeserializer 进行展开)

至此,对于 JSON 格式入参有一个比较直观的认识:在 JSON 格式入参类型转换的过程中,实际使用的是 Jackson 框架,进行 json 入参的解析。

五、总结

5.1、表单入参解析

Spring MVC 中,表单入参对应的参数解析类为:ServletModelAttributeMethodProcessor

该类对每个入参进行遍历操作,获取对应匹配的类型转换器,并通过类型类型转换器对入参进行类型转换操作。如果获取不到对应的类型转换器,直接原值返回。

来看看表单入参支持的一些解析类:
05-03.png
在图中,有很多类型转换的处理类,如:

  • String 转换 Boolean
  • String 转换 Integer
  • String 转换 Locale
  • String 转换 Long

每一种转换的实现类也有很多,比如图中的 String 转换 Long 中,有三个类型转换实现类。

遍历这些类型转换器,匹配对应的类型转换,进行类型转换。

5.2、Json 格式入参解析

Spring MVC 中,Json 格式入参对应的参数解析类为:RequestResponseBodyMethodProcessor

该类在 Spring boot2.0 默认使用 Jackson 框架作为解析工具,使用 Jackson 来对 Json 入参进行解析映射。

来看看 Jackson 默认提供的几种类型反序列类
05-04.png