自定义参数绑定原理(POJO 对象绑定)
一、是什么 ------ 什么是"自定义参数绑定"?
1. 场景描述
前端发来一个请求:POST /saveuser?userName=zhangsan&age=28&birth=2000-01-01&pet.name=Tom&pet.age=3
Controller 方法写成:
java
// 源码位置:springboot2-master/boot-05-web-01/.../controller/ParameterTestController.java
@PostMapping("/saveuser")
public Person saveuser(Person person) { // ★ person 没有 @RequestParam,也没 @RequestBody!
return person;
}
Person 是一个自定义的 POJO:
java
// 源码位置:springboot2-master/boot-05-web-01/.../bean/Person.java
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet; // 嵌套对象!
}
Pet 是嵌套对象:
java
// 源码位置:springboot2-master/boot-05-web-01/.../bean/Pet.java
@Data
public class Pet {
private String name;
private Integer age;
}
问题 :Spring 是怎么把 userName=zhangsan&age=28&pet.name=Tom 这些扁平字符串参数,变成一个 Person 对象(包括它里面的 Pet 子对象)的?
2. 一句话答案
Spring MVC 使用
ServletModelAttributeMethodProcessor参数解析器 +WebDataBinder数据绑定器,将 HTTP 请求中的键值对参数,通过反射和类型转换,逐步填充到目标 POJO 的属性中。
二、为什么 ------ 为什么需要 WebDataBinder?
1. HTTP 传过来的全是 String
HTTP 请求参数本质都是字符串:age=28 传到后端是 String "28",但 Person.age 是 Integer。
必须有一个组件负责类型转换(String → Integer、String → Date、String → 自定义对象)。
2. 嵌套属性需要递归处理
pet.name=Tom 需要先找到 Person.pet 属性,如果它是 null,要先 new Pet(),再给 Pet.name 赋值。
3. 数据校验、错误收集
如果 age=abc(无法转成 Integer),需要捕获这个错误,放到 BindingResult 中。
WebDataBinder 就是专门干这三件事的"包工头"。
三、怎么做 ------ 三个阶段深度拆解
第一阶段:入口与准备
1. supportsParameter() ------ 谁来处理 Person 参数?
java
// ServletModelAttributeMethodProcessor
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(ModelAttribute.class)
|| (this.annotationNotRequired
&& !BeanUtils.isSimpleProperty(parameter.getParameterType()));
}
核心判断 :!BeanUtils.isSimpleProperty(Person.class) → Person 不是简单类型 → 返回 true。
什么是"简单类型"?
java
public static boolean isSimpleValueType(Class<?> type) {
return Void.class != type && Void.TYPE != type
&& (ClassUtils.isPrimitiveOrWrapper(type) // int, Integer, boolean...
|| Enum.class.isAssignableFrom(type) // 枚举
|| CharSequence.class.isAssignableFrom(type) // String, StringBuilder...
|| Number.class.isAssignableFrom(type) // BigDecimal, Float...
|| Date.class.isAssignableFrom(type) // Date, java.sql.Date...
|| Temporal.class.isAssignableFrom(type) // LocalDate, LocalDateTime...
|| URI.class == type || URL.class == type
|| Locale.class == type || Class.class == type);
}
Person、Pet 这种自定义类不在简单类型列表中 → 由 ServletModelAttributeMethodProcessor 接管。
2. createAttribute() ------ 创建空对象
java
// 反射创建 Person 空壳
Object attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
// → new Person() 此时所有属性都是 null
3. createBinder() ------ 创建"指挥官" WebDataBinder
java
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder 是一个"包工头",装配了:
| 装备 | 作用 |
|---|---|
target(Person 空对象) |
绑定目标 |
ConversionService |
类型转换工具箱(含所有 Converter<String, T>) |
objectName |
对象名(默认为参数名 "person") |
autoGrowNestedPaths = true |
允许自动创建嵌套子对象 |
ignoreUnknownFields = true |
忽略请求中不存在对应属性的参数 |
重要澄清(来自闲聊) :
WebDataBinder不是单例!每个请求、每个参数都会通过WebDataBinderFactory实时new一个全新的实例。它是"有状态"的(存储了当前绑定的目标对象、错误结果等)。但它内部引用的ConversionService等核心工具是单例的,所以创建成本很低。
第二阶段:绑定与遍历
1. bindRequestParameters() ------ 启动绑定
java
this.bindRequestParameters(binder, webRequest);
内部:
java
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(servletRequest); // ★ 核心绑定
}
2. bind() ------ 提取请求参数
java
public void bind(ServletRequest request) {
// 把请求参数提取成 MutablePropertyValues(键值对集合)
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
// 进入绑定逻辑
this.doBind(mpvs);
}
此时 mpvs 里存放着:
{
"userName": "zhangsan",
"age": "28",
"birth": "2000-01-01",
"pet.name": "Tom",
"pet.age": "3"
}
3. applyPropertyValues() ------ 遍历并赋值
java
protected void applyPropertyValues(MutablePropertyValues mpvs) {
getPropertyAccessor().setPropertyValues(mpvs, ignoreUnknownFields, ignoreInvalidFields);
}
PropertyAccessor(实际是 BeanWrapperImpl)遍历所有键值对:
java
public void setPropertyValues(PropertyValues pvs, ...) {
for (PropertyValue pv : propertyValues) {
this.setPropertyValue(pv); // 逐个处理
}
}
4. 处理嵌套属性:autoGrowNestedPaths
当处理到 pet.name 时:
java
public void setPropertyValue(PropertyValue pv) {
String propertyName = pv.getName(); // 如 "pet.name"
// ★ 获取属性访问器:自动处理嵌套路径!
AbstractNestablePropertyAccessor nestedPa =
this.getPropertyAccessorForPropertyPath(propertyName);
// 遇到 "pet.name",分解为 "pet" 和 "name"
tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
// 对嵌套对象赋值
nestedPa.setPropertyValue(tokens, pv);
}
关键 :当 getPropertyAccessorForPropertyPath("pet.name") 被调用时:
- 检查 Person 是否有
pet属性 - 如果
person.pet当前是null - 且
autoGrowNestedPaths = true - Spring 会自动反射创建
new Pet(),并person.setPet(new Pet()) - 返回这个新 Pet 的属性访问器,继续处理
name
第三阶段:深度转换与反射赋值
1. processLocalProperty() ------ 处理单个属性
假设正在处理 age=28:
java
private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
// 获取 age 属性的处理器(封装了 setAge 方法)
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
Object originalValue = pv.getValue(); // "28" (String)
Object valueToApply = originalValue;
// 检查是否需要转换:String "28" vs Integer age
if (!Boolean.FALSE.equals(pv.conversionNecessary)) {
if (!pv.isConverted()) {
// ★ 核心转换
valueToApply = this.convertForProperty(
tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()
);
}
}
// ★ 反射赋值:等效于 person.setAge(28)
ph.setValue(valueToApply);
}
2. convertForProperty() → TypeConverterDelegate
java
// TypeConverterDelegate.convertIfNecessary()
ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
if (conversionService != null) {
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); // String
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
// ★ 找到并执行 String → Integer 转换器
return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
}
3. 找到转换器并执行
java
// GenericConversionService.find()
for (Class<?> sourceCandidate : getClassHierarchy(sourceType)) {
for (Class<?> targetCandidate : getClassHierarchy(targetType)) {
GenericConverter converter = getRegisteredConverter(sourceType, targetType, pair);
if (converter != null) {
return converter; // 如 StringToNumberConverterFactory
}
}
}
对于 String → Integer,命中 StringToNumberConverterFactory:
java
// StringToNumber 内部
public T convert(String source) {
return source.isEmpty() ? null
: (T) NumberUtils.parseNumber(source, this.targetType);
// "28" → Integer.valueOf("28") → Integer(28)
}
4. 反射赋值(终点)
java
ph.setValue(Integer(28));
// 底层:method.invoke(person, 28)
// 等效于:person.setAge(28)
四、完整流程总结
POST /saveuser?userName=zhangsan&age=28&pet.name=Tom&pet.age=3
↓
① supportsParameter(Person.class) → 不是简单类型 → ServletModelAttributeMethodProcessor 接管
↓
② createAttribute() → 反射 new Person()
↓
③ createBinder() → new WebDataBinder(person, conversionService)
↓
④ bindRequestParameters() → 提取请求参数为 MutablePropertyValues
↓
⑤ setPropertyValues() → for 循环遍历每个键值对
├──"userName" → 类型匹配(String→String) → 直接 setUserName("zhangsan")
├──"age" → 类型不匹配(String→Integer) → convertForProperty() → Integer(28) → setAge(28)
├──"pet.name" → 嵌套路径 → autoGrowNestedPaths 自动 new Pet() → setPetName("Tom")
└──"pet.age" → 嵌套路径 → convertForProperty() → Integer(3) → setPetAge(3)
↓
⑥ 返回填充完成的 Person 对象 → 传给 Controller 方法
四个核心执行者
| 组件 | 角色 |
|---|---|
ServletModelAttributeMethodProcessor |
入口判断 + 流程调度 |
WebDataBinder |
指挥官(装配配置 + 启动绑定) |
BeanWrapperImpl |
执行者(遍历属性 + 嵌套处理 + 反射赋值) |
ConversionService |
工具箱(String → 任意类型转换) |
五、与 @RequestBody 的对比
| 场景 | 谁来解析 | 谁来绑定/转换 |
|---|---|---|
@RequestParam String name |
RequestParamMethodArgumentResolver |
WebDataBinder + ConversionService |
saveuser(Person person) |
ServletModelAttributeMethodProcessor |
WebDataBinder + ConversionService |
@RequestBody Person person |
RequestResponseBodyMethodProcessor |
HttpMessageConverter(Jackson) |
关键区分 :@RequestBody 走的是
HttpMessageConverter(处理一整坨 JSON 流);POJO 表单绑定走的则是WebDataBinder+ConversionService(处理扁平 K-V 参数)。