一、回顾
在 《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
@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入参的分析,就得从两者对应的 解析器入手。
4.1、表单入参类型转换-解析器
(以 @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 类型,而转换结果根据方法对象中的字段类型为主,查找匹配的类型转换器进行参数转化,并返回转化后的结果。
4.2、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 入参的解析。
五、总结
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 默认提供的几种类型反序列类