写于:2019-09-28 05:50:37

# 回顾

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

# 表单入参 和 JSON格式入参

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

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

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

  • 表单接收使用:@DateTimeFormat
  • JSON 格式接收使用:@JsonFormat

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

表单类型入参:时间类型转换

表单接收数据并进行类型转换_表单入参

JSON 类型入参:时间类型转换

表单接收数据并进行类型转换_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
    @Nullable
    public 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入参的分析,就得从两者对应的 解析器入手。

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

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

ServletModelAttributeMethodProcessor 类图结构如下:

# 聚焦 ServletModelAttributeMethodProcessor#resolveArgument

该方法由其父类 ModelAttributeMethodProcessor#resolveArgument 实现

查看代码:ModelAttributeMethodProcessor#resolveArgument

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
    @Override
    @Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 新的返回结果:这里为:FromDTO
        Object attribute = null;

        // 创建 web 数据绑定器物
        // webRequest :当前请求(其中包含的入参参数集合 paramters)
        // attribute:为入参映射对象 FromDTO
        // name :为入参映射对象名字(默认:类首字母小写)这里为:fromDTO
        WebDataBinder 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 {
    @Nullable
    protected 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 类型,而转换结果根据方法对象中的字段类型为主,查找匹配的类型转换器进行参数转化,并返回转化后的结果。

# JSON入参类型转换源码分析(以@JsonFormat为例 )

JSON格式入参对应的解析器为:RequestResponseBodyMethodProcessor

查看代码 RequestResponseBodyMethodProcessor#resolveArgument

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    public Object resolveArgument(......){
        // 对入参进行解析
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    }

    @Override
    protected <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 {
    @Nullable
    protected <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> {
    @Override
    public 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{
    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
        return deserializeFromObject(p, ctxt);
    }

    @Override
    public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException{
        // 构造入参构建对象:例如:JSONDto
        final 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 case
                    try {
                        // 调用对应 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 入参的解析。

# 总结

# 表单入参解析

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

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

来看看表单入参支持的一些解析类:

表单参数解析类展示

在图中,有很多类型转换的处理类,如:

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

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

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

# Json 格式入参解析

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

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

来看看 Jackson 默认提供的几种类型反序列类

JSON参数解析类展示

精彩内容推送,请关注公众号!
最近更新时间: 4/20/2020, 5:10:25 PM