自定义类型参数的数据绑定源码分析

关于自定义类型参数的参数解析场景,SpringMVC处理请求源码分析 —— part2 场景分析 > 2. 表单提交 > 2.1 源码分析 已经讲得非常清晰了,下面主要是对该博客中的内容做一些补充。

前端页面表单:

1
2
3
4
5
6
7
8
<form action="/saveuser" method="post">
姓名:<input name="userName" value="zhangsan"/> <br/>
年龄:<input name="age" value="18"/> <br/>
生日:<input name="birth" value="2019/12/10"/> <br/>
宠物姓名:<input name="pet.name" value="阿猫"/> <br/>
宠物年龄:<input name="pet.age" value="5"/> <br/>
<input type="submit" value="保存"/>
</form>

后端自定义类型 Person 以及 Pet 类定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}

@Data
public class Pet {
private String name;
private Integer age;
}

后端处理器方法:

1
2
3
4
@PostMapping("/saveuser")
public Person saveuser(Person person) {
return person;
}

ServletModelAttributeMethodProcessor 的数据绑定原理

ServletModelAttributeMethodProcessor 参数解析实际调用的是其父类 ModelAttributeMethodProcessor#resolveArgument 方法,下面简化选出了其中的关键步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ModelAttributeMethodProcessor.java
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
bindRequestParameters(binder, webRequest);
}

// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);

return attribute;
}

当前 parameter 封装的是 Person saveuser(Person person)Person 参数的信息,参数解析时,会先通过反射调用 Person 的无参构造器,创建 Person 壳对象/空对象。

接着,为了将请求 webRequest 中的请求参数数据,绑定到刚刚创建的 Person 壳对象的属性中,需要数据绑定工厂生产出一个数据绑定器 binder 来帮我们完成这项工作(数据绑定工厂在生产时,会将其内部的转换服务,装配给所生产出的数据绑定器,从而数据绑定器具备数据转换能力)。

然后,数据绑定器解析得到请求中的数据,并将数据转换为恰当的格式后,绑定到 Person 壳对象的属性中。

数据绑定工作完成后,还会将已完成绑定的 Person 对象,以及本次绑定结果,都放到 mavContainer 中。

所以重点在于 bindRequestParameters(binder, webRequest),往下看:

1
2
3
4
5
6
7
8
9
// ServletModelAttributeMethodProcessor.java
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
// 拿到原生 ServletRequest 请求
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
// 数据转换器向下转型
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
// 根据请求完成绑定工作,注意绑定器中的 target 指向需要进行绑定的壳对象
servletBinder.bind(servletRequest);
}

重点在于 servletBinder.bind(servletRequest),往下看:

1
2
3
4
5
// ServletRequestDataBinder.java
public void bind(ServletRequest request) {
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
doBind(mpvs);
}

数据绑定器首先对请求参数数据进行解析,将其转换为 MutablePropertyValues

MutablePropertyValues 内部保存了一个 List<PropertyValue>PropertyValue 能够存储一个键值对,List 中每个 PropertyValue 就相应封装了请求参数数据中的一个 KV 对。

比如请求数据是 userName=tom&age=18,那么就会转换得到两个 PropertyValuePropertyValue1(name="userName", value="tom")PropertyValue2(name="age", value="18")

底层是如何进行转换的呢?其实是 request.getParameterNames() 调用原生 API 拿到所有请求参数的参数名,然后遍历每个参数名,调用 request.getParameterValues() 来获取值,拿到参数名和参数值以后,就可以封装为一个 PropertyValue

数据绑定操作重点在于 doBind(mpvs),往下看:

1
2
3
4
// DataBinder.java
protected void doBind(MutablePropertyValues mpvs) {
applyPropertyValues(mpvs);
}

重点在于 applyPropertyValues(mpvs),往下看:

1
2
3
4
5
// DataBinder.java
protected void applyPropertyValues(MutablePropertyValues mpvs) {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}

getPropertyAccessor() 会返回一个 BeanWrapperImpl

BeanWrapperImpl 见名知义,它就是一个包装对象,其内部封装了一个 JavaBean 对象(这里封装的是 Person 壳对象),它主要有两个功能:

  • 基于反射提供了一系列功能强大的函数,能够很方便地对被包装的 JavaBean 对象的属性进行各种访问和设置(比如支持级联属性的访问和设置)
  • 内部包含了转换服务,在操作属性时,提供类型转换功能。

getPropertyAccessor() 函数所做的工作是,找到 this.target 所指向的 Person 壳对象,使用 BeanWrapperImpl 对其进行包装,将数据绑定器内部的转换服务 conversionService 装配给 BeanWrapperImpl

这样后续我们就能直接调用 BeanWrapperImpl#setPropertyValues 来对被包装的 Person 壳对象的属性赋值了。

我们深入 setPropertyValues 看看:

1
2
3
4
5
6
7
8
9
@Override
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid)
throws BeansException {
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
setPropertyValue(pv);
}
}

可以看到是取出了之前准备好的 List<PropertyValue>,然后逐个 PropertyValue 调用 setPropertyValue(pv) 进行 Person 壳对象的属性绑定。

setPropertyValue(pv) 底层无非就是对值进行类型转换,然后通过反射调用 setter 进行属性值的设置。

进入 setPropertyValue 看看:

1
2
3
4
5
6
7
8
@Override
public void setPropertyValue(PropertyValue pv) throws BeansException {
PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
// 拿到 pv 的属性名:比如 userName pet.age 等
String propertyName = pv.getName();
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
nestedPa.setPropertyValue(tokens, pv);
}

getPropertyAccessorForPropertyPath(propertyName) 见名知义,就是根据属性名路径,确定对应的属性访问器,也即确定对应的 BeanWrapperImpl

propertyName 值为 userName 时,该方法获取到的 BeanWrapperImpl 就是 this!注意,当前 setPropertyValue 就是封装了 Person 壳对象的 BeanWrapperImpl 内部的方法,Person 壳对象中有 userName 属性。

propertyName 值为级联属性 pet.age 时,该方法获取到的 BeanWrapperImpl 就不是 this 了,而是一个 Pet 壳对象的 BeanWrapperImpl。无论是 Pet 壳对象,还是包装了 Pet 壳对象的 BeanWrapperImpl,都是在该方法底层刚刚处理 pet.age 时创建的。

有兴趣的话,可以深入 getPropertyAccessorForPropertyPath 源码底层进行学习。

根据属性路径,拿到对应的属性访问器 nestedPa 后,就可以执行 nestedPa.setPropertyValue(tokens, pv) 来做属性绑定了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
processLocalProperty(tokens, pv);
}

private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);

// 拿到属性值
Object originalValue = pv.getValue();
Object valueToApply = originalValue;

// 使用 BeanWrapperImpl 创建时就装配好的转换服务,对属性值进行转换
valueToApply = convertForProperty(
tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
// 转换后将值绑定到 BeanWrapperImpl 中封装的 JavaBean 的属性中
ph.setValue(valueToApply);
}

数据绑定器相关的 SpringMVC 自动配置

数据绑定器是数据绑定器工厂生产出来的。

1
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

数据绑定器工厂是在 RequestMappingHandlerAdapter#invokeHandlerMethod 中构造出来的,该工厂封装了 RequestMappingHandlerAdapter 中的 WebBindingInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// ...
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// ...
}

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
// ...
return createDataBinderFactory(initBinderMethods);
}

protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}

@Nullable
public WebBindingInitializer getWebBindingInitializer() {
return this.webBindingInitializer;
}

WebBindingInitializer 是一个接口,它只有一个实现类 ConfigurableWebBindingInitializer,实现类内部封装了转换服务 private ConversionService conversionService,数据绑定器中的转换服务就是一路从这里拿到的。

ConfigurableWebBindingInitializer 并没有直接注册在容器中!SpringMVC 的自动配置类中,是在注册 RequestMappingHandlerAdapter 时,创建了 ConfigurableWebBindingInitializer 对象并设置到 RequestMappingHandlerAdapter 中。

此外,SpringMVC 自动配置类还注册了转换服务 ConversionService 到容器中。

参考博客

SpringMVC处理请求源码分析 —— part2 场景分析 > 2. 表单提交 > 2.1 源码分析

Spring官网阅读(十四)Spring中的BeanWrapper及类型转换