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

关于自定义类型参数的参数解析场景,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,那么就会转换得到两个 PropertyValue:PropertyValue1(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及类型转换