前面的话
Spring 的核心特性包括 IOC 容器、事件、资源管理、国际化、校验、数据绑定、类型转换、EL 表达式、AOP。其他特性可以轻易的在网络上找到很多资料,而数据绑定这个特性即便在 Spring 官网描述却也不太多。这是因为数据绑定主要应用于 Spring 内部,对于用户而言直接使用的场景并不多。如果想要深入理解 Spring 内部的运行机制,数据绑定是必须了解的一块内容。
理解数据绑定
数据绑定允许将用户输入的内容动态绑定到应用程序中的领域模型中。对于 Spring 而言,用于输入具体的场景主要包括 xml 中 bean 的定义、web 环境下的请求参数。
数据绑定在 Spring 中的核心类是 org.springframework.validation.DataBinder,可以看到这个类位于 validation 包中,数据绑定时也往往伴随着参数校验。先看看如何手动使用这个类。
@Data public class LoginDTO { private String username; private String password; } public class App { public static void main(String[] args) { LoginDTO dto = new LoginDTO(); Map<String, Object> input = new HashMap<>(); input.put("username", "hkp"); input.put("password", "123"); PropertyValues propertyValues = new MutablePropertyValues(input); DataBinder dataBinder = new DataBinder(dto, "loginDTO"); dataBinder.bind(propertyValues); // LoginDTO(username=hkp, password=123) System.out.println(dto); } }
DataBinder#bind 是 DataBinder 类的核心方法,它接收 PropertyValues 类型的参数,并将参数中的数据设置到对象的属性中。
PropertyValues
PropertyValues 可以简单的理解为 Map,它包含更多的属性信息。接口核心方法定义如下。
public interface PropertyValues extends Iterable<PropertyValue> PropertyValue[] getPropertyValues(); PropertyValue getPropertyValue(String propertyName); boolean contains(String propertyName); boolean isEmpty(); }
PropertyValues 继承接口 Iterable,可以理解为 PropertyValue 的容器,PropertyValue 表示对象中的某一个属性值,包含的主要字段如下。
public class PropertyValue extends BeanMetadataAttributeAccessor implements Serializable { private final String name; private final Object value; private boolean optional = false; // 是否进行过类型转换 private boolean converted = false; // 类型转换后的值 private Object convertedValue; }
PropertyValue 包括原始值和转换后的值,也就意味着 Spring 可能会对用于输入的值进行类型转换。
PropertyValues 接口常用实现如下。
MutablePropertyValues:标准的 PropertyValues 实现,除了属性获取,还支持对属性的设置。
ServletConfigPropertyValues:支持 web 环境获取 servlet 上下文配置的 PropertyValues。
ServletRequestParameterPropertyValues:支持 web 环境获取请求参数的 PropertyValues。
DataBinder
DataBinder 是数据绑定的核心类,它内部的属性可以分为两类,一类是数据绑定的配置,另一类用于数据绑定,理解这些属性后我们就能大概知道它内部的机制。
数据绑定配置有关的字段如下。
public class DataBinder implements PropertyEditorRegistry, TypeConverter { // 是否忽略未知字段 private boolean ignoreUnknownFields = true; // 是否忽略无效字段,如这些字段的值在目标对象中不可访问(如嵌套路径中的字段对应值为空) private boolean ignoreInvalidFields = false; // 是否自动增长包含空值的嵌套路径 private boolean autoGrowNestedPaths = true; // 自动增长的数组或集合的大小的限制 private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT; // 绑定字段白名单 private String[] allowedFields; // 绑定字段黑名单 private String[] disallowedFields; // 必须绑定的字段 private String[] requiredFields; }
数据绑定功能实现的字段如下。
public class DataBinder implements PropertyEditorRegistry, TypeConverter { // 需要进行数据绑定的目标对象 private final Object target; // 需要进行数据绑定的目标对象名称 private final String objectName; // 数据绑定结果 private AbstractPropertyBindingResult bindingResult; // 类型转换 private SimpleTypeConverter typeConverter; // 类型转换服务 private ConversionService conversionService; // 将错误码转换为消息编码的解析器 private MessageCodesResolver messageCodesResolver; // 错误处理器,用于处理字段缺失和进行异常转换 private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); // 参数校验器 private final List<Validator> validators = new ArrayList<>(); }
数据绑定功能实现的字段又分为几大类:数据绑定的目标对象、负责类型转换的转换器、负责参数校验的校验器、保存参数校验结果的 BindingResult。
DataBinder 进行数据绑定时,需要将属性值转换为目标对象属性的类型,还会把校验结果存至 BindingResult。由于 DataBinder 实现了接口 PropertyEditorRegistry、TypeConverter,还会将实现委托到内部的 typeConverter。
至于校验器则用于额外的校验方法使用,在上篇《Spring 参数校验最佳实践及原理解析》有进行分析 web 请求参数的校验会调用 DataBinder 的校验方法,感兴趣可以自行阅读。
也就是说 DataBinder 兼具对象属性设置、类型转换、校验的能力。#bind 方法实现代码如下。
public void bind(PropertyValues pvs) { MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs)); doBind(mpvs); }
这里对属性进行了简单处理后就进入了真正数据绑定的方法。
protected void doBind(MutablePropertyValues mpvs) { checkAllowedFields(mpvs); checkRequiredFields(mpvs); applyPropertyValues(mpvs); }
数据绑定时会对属性必要的校验和处理。让我们把重点放到属性的设置。
protected void applyPropertyValues(MutablePropertyValues mpvs) { try { // Bind request parameters onto target object. getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); } catch (PropertyBatchUpdateException ex) { // Use bind error processor to create FieldErrors. for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) { getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult()); } } }
属性设置直接调用 PropertyAccessor 设置属性的方法,遇到异常后则使用错误处理器处理。那么 PropertyAccessor 是从哪里来的呢?继续跟踪代码。
protected ConfigurablePropertyAccessor getPropertyAccessor() { return getInternalBindingResult().getPropertyAccessor(); }
这里可以看出,PropertyAccessor 是从内部的 BindingResult 获取到的,继续跟踪代码则会发现这里 BindingResult 的实现是 BeanPropertyBindingResult,至于 PropertyAccessor 的实现使用的则是 BeanWrapper。也是说真正进行数据绑定的实现由 BeanWrapper 完成,这里不再具体分析。
Spring XML 配置中属性设置分析
我们已经对 Spring 数据绑定的能力有了一定的了解,那 Spring 是怎么运用这项能力的呢?这里进行一些简单分析,先看 Spring 如何将 XML 中的属性配置设置到 bean 的属性上的。
数据绑定中,属性使用 PropertyValues 表示,Spring 会将 xml 中的属性配置转换为 PropertyValues 并存储至表示 bean 元数据的 BeanDefinition。核心代码如下。
public class BeanDefinitionParserDelegate { public void parsePropertyElement(Element ele, BeanDefinition bd) { String propertyName = ele.getAttribute(NAME_ATTRIBUTE); ... 省略部分代码 try { Object val = parsePropertyValue(ele, bd, propertyName); PropertyValue pv = new PropertyValue(propertyName, val); bd.getPropertyValues().addPropertyValue(pv); } finally { this.parseState.pop(); } } }
有了 BeanDefinition,Spring 就可以根据其内部的元数据实例化 bean,并设置 bean 的属性。核心代码如下。
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { ... 省略部分代码 try { bw.setPropertyValues(new MutablePropertyValues(deepCopy)); } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } }
可以看到,bean 属性的设置和 DataBinder 数据绑定的底层实现一样,也是委托给 BeanWrapper 设置属性值,至于这里的 BeanWrapper,则是 Spring 实例化 bean 之后创建的。
WEB 请求参数数据绑定分析
Web 环境下,Spring 需要把请求中的参数或其他参数绑定到 Controller 方法参数值中,这是由 HandlerMethodArgumentResolver 接口来处理的,它可以根据 Controller 方法参数定义解析出参数值。
对于请求到 Model 参数的绑定,实现类为 ModelAttributeMethodProcessor 有关参数绑定的核心代码如下。
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @Override @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { ... 省略部分代码 WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { // 数据绑定 bindRequestParameters(binder, webRequest); } // 参数校验 validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } if (!parameter.getParameterType().isInstance(attribute)) { // 类型转换 attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); ... 省略部分代码 } protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { ((WebRequestDataBinder) binder).bind(request); } }
可以看到,这里直接调用了WebRequestDataBinder#bind方法将 request 转换为方法参数中的属性,此外数据绑定后还进行了参数校验与类型转换。
总结
Spring 数据绑定的核心类是 DataBinder,其底层使用 BeanWrapper 实现对象属性值的设置,对于数据绑定,往往又伴随着数据校验、类型转换。Spring 中的类型转换同样具有多种实现,下篇将进行介绍。