Spring数据绑定之DataBinder篇---01
- 前言
- Spring数据绑定体系
- 数据绑定需要考虑哪些事情 ?
- DataBinder需要知道target对象是哪个
- DataBinder类型转换靠谁?
- BindingResult干啥用的 ?
- Error接口
- BindingResult 接口
- AbstractBindingResult实现
- MessageCodesResolver解析错误码
- AbstractPropertyBindingResult---加入类型转换处理
- DirectFieldBindingResult---返回DirectFieldAccessor
- BeanPropertyBindingResult---返回BeanWrapper
- DataBinder如何使用BindingResult
- 关于字段的限制
- 进入bind核心方法
- 开始进行字段限制检查
- BindingErrorProcessor如何处理相关字段错误的
- applyPropertyValues真正开始数据绑定
- close方法判断是否产生了异常
- 关于数据校验
- 数据绑定需要考虑哪些事情 ?
- Spring数据绑定体系
前言
数据绑定对于一个成熟的Web框架而言十分的重要,通过将Http中的请求参数或者请求体中的Json字符串绑定到对应实体对象上,可以大大提高开发人员的效率。
传统Servelt编程中,仅仅是将Http数据报文中的相关请求参数封装到了Request对象中,这样做的好处是给了开发人员足够的自由性,可以自由取出相关参数进行操作。
坏处是增加了重复编码劳作,例如: 重复的数据绑定工作和数据校验工作。
一般在请求参数比较多的情况下,会采用一个专门的Model对象来封装这些请求参数,因此,这也是为什么需要数据绑定的原因。而在将请求参数绑定到Model对象上时,需要对请求参数值进行校验,判断是否符合逻辑,因此也就引出了数据校验。
对于Spring来说,我们只需要在Controller类负责接收请求的方法中,将Model对象作为方法参数给出,就可以完成request参数到Model对象的自动数据绑定。
Spring数据绑定体系
对于Spring的数据绑定体系来说,是以DataBinder为基础设施开展。并且DataBinder不仅仅只会做数据绑定,还会进行数据校验Validation。
下面我们先来看一下使用DataBinder完成数据绑定的案例:
代码语言:javascript复制public class TestDataBinder {
@Test
public void testDataBinder01() throws BindException {
People people = new People();
MutablePropertyValues mutablePropertyValues = new MutablePropertyValues();
mutablePropertyValues.add("name","狗蛋儿");
mutablePropertyValues.add("age",18);
mutablePropertyValues.add("mother.name","小朋友");
mutablePropertyValues.add("mother.age",20);
mutablePropertyValues.add("father.name","大忽悠");
mutablePropertyValues.add("father.age",20);
DataBinder dataBinder = new DataBinder(people);
dataBinder.bind(mutablePropertyValues);
dataBinder.close();
System.out.println("数据绑定结果为: " people);
}
}
数据绑定需要考虑哪些事情 ?
学习一件事情,最好方法是多问为什么?
我们需要将一堆Key-Value键值对绑定到对应Object对象上,那么这个数据绑定过程,我们需要考虑哪些事情呢?
- 首先需要知道,要把key-value键值对绑定到哪个target对象上
- key—>target对象的某个属性上,这个映射过程怎么完成
- value设置到对象的某个属性上,类型是否一致,是否需要进行类型转换
- 如果绑定过程中抛出异常,该怎么处理
- 数据校验是绑定过程中完成的,还是绑定结束后完成的
- 数据校验失败和成功的结果,如何告知用户
- …
上面列举的是重点需要考虑的地方,其实数据绑定过程中还有很多需要考虑的地方,那么既然提出了这些问题,下面就来一一解答:
DataBinder需要知道target对象是哪个
DataBinder中通过target来保存目标对象,objectName是目标对象的名字,方便在出现相关错误时,通过objectName告知用户具体是哪个对象出现了什么样子的错误。
代码语言:javascript复制 //如果用户没有指定objectName,那么默认值为target
public static final String DEFAULT_OBJECT_NAME = "target";
@Nullable
private final Object target;
private final String objectName;
DataBinder类型转换靠谁?
代码语言:javascript复制public class DataBinder implements PropertyEditorRegistry, TypeConverter {
DataBinder继承了有关类型转换的上层接口,说明DataBinder 本身具备了以下能力:
- 向外界提供Spring统一的类型转换接口: TypeConverter
- 可以新增用户自定义的类型转化器: PropertyEditorRegistry
- 因为并没有继承ConverterRegistry接口,因此并没有提供可以新增Converter的方法
但是DataBinder内部依靠的是Spring已经完善好的类型转换体系: PropertyEditor和ConversionService来具体完成类型转换的任务。
这是Spring中常用的代理思想,我继承了接口,只是告诉外界我提供了这种功能,但是具体功能实现,我依靠的是代理对象。 在进行模块化开发时,会很有用,无论是上层模块调用底层模块实现具体功能。还是在没有严格分层关系的模块中,某个模块需要另一个模块的功能支持,也可以向外界提供实现接口,但是该模块通过调用另一个模块完成接口的实现。
关于类型转换,DataBinder类中维护了以下两个变量:
代码语言:javascript复制 @Nullable
private ConversionService conversionService;
@Nullable
private SimpleTypeConverter typeConverter;
首先关于ConversionService,DataBinder中并没有继承ConverterRegistry接口,因此没有向用户暴露新增Converter的方法。
但是DataBinder内部却维护了一个ConversionService成员属性,并且提供了sette和getter方法:
代码语言:javascript复制 public void setConversionService(@Nullable ConversionService conversionService) {
Assert.state(this.conversionService == null, "DataBinder is already initialized with ConversionService");
this.conversionService = conversionService;
if (this.bindingResult != null && conversionService != null) {
this.bindingResult.initConversion(conversionService);
}
}
@Nullable
public ConversionService getConversionService() {
return this.conversionService;
}
因此,可以看出,如果想要让DataBinder用上conversionService 提供的服务的话,就需要我们手动设置conversionService才行。
SimpleTypeConverter 类定义如下:
代码语言:javascript复制public class SimpleTypeConverter extends TypeConverterSupport {
public SimpleTypeConverter() {
//初始化typeConverterDelegate
this.typeConverterDelegate = new TypeConverterDelegate(this);
//将PropertyEditorRegistrySupport中的defaultEditorsActive设置为true
//可以理解为会去注册默认的PropertyEditor
registerDefaultEditors();
}
}
SimpleTypeConverter继承了TypeConverterSupport类,TypeConverterSupport内部依靠TypeConverterDelegate完成类型转换,这也是为什么需要先初始化TypeConverterDelegate的原因。
关于类型转换不清楚的,去看本专栏之前类型转换系列文章
DataBinder提供的新增类型转换器的方法有以下这些:
- 首先是新增CustomFormatter ---- Formatter也是Spring 3.0全新类型转换体系中的一员,主要负责格式转换,但是由于和ConversionService侧重点不同,因此两者是区分开的
public void addCustomFormatter(Formatter<?> formatter) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter);
}
public void addCustomFormatter(Formatter<?> formatter, String... fields) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
Class<?> fieldType = adapter.getFieldType();
if (ObjectUtils.isEmpty(fields)) {
getPropertyEditorRegistry().registerCustomEditor(fieldType, adapter);
}
else {
for (String field : fields) {
getPropertyEditorRegistry().registerCustomEditor(fieldType, field, adapter);
}
}
}
public void addCustomFormatter(Formatter<?> formatter, Class<?>... fieldTypes) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
if (ObjectUtils.isEmpty(fieldTypes)) {
getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter);
}
else {
for (Class<?> fieldType : fieldTypes) {
getPropertyEditorRegistry().registerCustomEditor(fieldType, adapter);
}
}
}
添加formatter的核心逻辑是通过适配器将formatter转换为PropertyEditor后,加入PropertyEditorRegistry的CustomEditor集合中。
我们也可以通过适配器,将formatter加入ConversionService中
- 注册自定义的PropertyEditor
@Override
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor);
}
@Override
public void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String field, PropertyEditor propertyEditor) {
getPropertyEditorRegistry().registerCustomEditor(requiredType, field, propertyEditor);
}
- 查询PropertyEditor
@Override
@Nullable
public PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath) {
return getPropertyEditorRegistry().findCustomEditor(requiredType, propertyPath);
}
上面无论是注册formatter也好,还是注册customEditor也好,还是查询customEditor也好,其实都可以归为是DataBinder继承了PropertyEditorRegistry接口后,向外界提供注册和查询PropertyEditor的能力。
那么,我们可以看到关于PropertyEditorRegistry接口功能的实现,都是通过getPropertyEditorRegistry()方法获取到PropertyEditorRegistry后提供的,这个PropertyEditorRegistry是哪里来的呢?
代码语言:javascript复制 protected PropertyEditorRegistry getPropertyEditorRegistry() {
if (getTarget() != null) {
return getInternalBindingResult().getPropertyAccessor();
}
else {
return getSimpleTypeConverter();
}
}
如果目标对象存在的话,会去寻找InternalBindingResult来拿到PropertyEditorRegistry。
否则,靠的还是SimpleTypeConverter
InternalBindingResult这里暂时不说,下面再讲
- 因为继承了TypeConverter接口,因此势必需要向外界提供关于类型转换的接口,具体如下:
@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException {
return getTypeConverter().convertIfNecessary(value, requiredType);
}
...
一共四个重载的convertIfNecessary方法,但是本质都是调用getTypeConverter返回的TypeConverter来完成的。
代码语言:javascript复制 protected TypeConverter getTypeConverter() {
if (getTarget() != null) {
return getInternalBindingResult().getPropertyAccessor();
}
else {
return getSimpleTypeConverter();
}
}
BindingResult干啥用的 ?
绑定的结果集可以被检查是否有问题,这个功能就是BindingResult提供的。
BindingResult的还扩展了Errors接口,这样可以通过Errors接口提供的方法,判断是否出现了相关异常,例如: 字段缺少错误和属性访问错误,这些错误都会被转换为FieldErrors,然后收集在Errors中:
Error接口
对于数据绑定和数据校验过程中会抛出error错误,需要一个将这些error错误存储并暴露给用户,因此就有了Error接口。该接口向外提供的功能就是记录错误然后提供给用户获取错误的方法。
在Spring中,将Error分类了两大类: global Error 和 field Error
- global error针对的是整个target object对象产生的错误
- field error针对的是target oject对象中某个字段属性产生的错误
当需要添加全局Error的时候,调用以下方法即可:
代码语言:javascript复制 void reject(String errorCode);
void reject(String errorCode, String defaultMessage);
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
注册字段错误:
代码语言:javascript复制 void rejectValue(@Nullable String field, String errorCode);
void rejectValue(@Nullable String field, String errorCode, String defaultMessage);
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
提供给用户判断是否产生相关错误的方法有下面这些,因为关于这部分的方法很多,我只列举一部分,更多细节,大家查阅源码即可:
代码语言:javascript复制 boolean hasErrors();
boolean hasGlobalErrors();
boolean hasFieldErrors();
boolean hasFieldErrors(String field);
List<FieldError> getFieldErrors(String field);
....
Error接口还提供了设置嵌套路径的方法如下:
代码语言:javascript复制 void setNestedPath(String nestedPath);
String getNestedPath();
void pushNestedPath(String subPath);
void popNestedPath() throws IllegalStateException;
为什么需要设置嵌套路径呢?
- 例如: AddressValidator需要对Peo类中的Home属性的address属性进行校验,那么我们传入的字段名是address,这个校验器怎么会知道address是去Peo类中找呢,还是去Peo类中的Home属性中找呢?
因此,就引出了嵌套路径的设置,我们设置了NestedPath为home.,那么当传入字段名为address时,AddressValidator就知道是去Peo类的Home属性中寻找address字段,然后进行数据校验。
包括在显示错误信息的时候,需要告诉用户具体是哪个字段出现了问题,那么也需要NestedPath的协助。
BindingResult 接口
BindingResult 作为对DataBinder数据校验结果的封装类,但是对于数据校验结果而言,如果成功了,其实不需要存储什么结果,关键是如果失败了,才需要存储数据校验失败的信息,因此BindingResult 继承了Error接口。
代码语言:javascript复制public interface BindingResult extends Errors {
在DataBinder获取转换器的方法中,都会首先去BindingResult中获取,如果获取不到,再寻找自身默认提供的SimpleTypeConverter,因此BindingResult 接口中需要提供有关转换器的方法。
代码语言:javascript复制 PropertyEditor findEditor(@Nullable String field, @Nullable Class<?> valueType);
@Nullable
PropertyEditorRegistry getPropertyEditorRegistry();
因为BindResult中还需要在数据绑定完毕后,进行数据校验,然后保存校验中产生的失败字段和失败信息,因此需要提供相关方法:
代码语言:javascript复制 //记录当前字段的值
default void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
}
//记录发生校验异常的字段
default void recordSuppressedField(String field) {
}
//获取所有发生校验异常的字段
default String[] getSuppressedFields() {
return new String[0];
}
//留给用户添加自定义Error的接口
void addError(ObjectError error);
为了区分字段发生的异常,DataBinder通过errorCode来完成,因此在BindingResult中就需要提供对errorCode进行解析的方法:
代码语言:javascript复制 String[] resolveMessageCodes(String errorCode);
String[] resolveMessageCodes(String errorCode, String field);
BindingResult只是对外提供了接口,但是具体异常码解析则是交给了MessageCodesResolver来完成。
AbstractBindingResult实现
AbstractBindingResult是BindingResult接口的直接实现,并且继承了AbstractErrors,相当于将Error接口的实现,通过继承AbstractErrors间接完成。
代码语言:javascript复制AbstractErrors仅仅只是对Error接口进行了实现,感兴趣的可以自己去看看具体实现过程是怎样的
public abstract class AbstractBindingResult extends AbstractErrors implements BindingResult, Serializable {
AbstractBindingResult中提供了相关工具类和容器的具体实现:
代码语言:javascript复制 //要操作的对象名
private final String objectName;
//错误码解析器
private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
//存放Error的集合
private final List<ObjectError> errors = new ArrayList<>();
//保存当前目标对象的字段类型的集合
private final Map<String, Class<?>> fieldTypes = new HashMap<>();
//保存当前目标对象的值的集合
private final Map<String, Object> fieldValues = new HashMap<>();
//保存发生错误的字段的集合
private final Set<String> suppressedFields = new HashSet<>();
因为AbstractErrors中并没有定义存放Error的集合,因此Error接口中相关reject方法的实现,最终还是需要AbstractBindingResult来完成。
代码语言:javascript复制 //添加全局异常---针对当前target目标对象
@Override
public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) {
addError(new ObjectError(getObjectName(), resolveMessageCodes(errorCode), errorArgs, defaultMessage));
}
//添加字段异常,针对target对象中某个字段产生的异常
@Override
public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs,
@Nullable String defaultMessage) {
//如果嵌套属性不存在,并且field为空的话,说明当前异常是全局异常
if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) {
// We're at the top of the nested object hierarchy,
// so the present level is not a field but rather the top object.
// The best we can do is register a global error here...
//调用上面的reject方法进行全局异常注册
reject(errorCode, errorArgs, defaultMessage);
return;
}
//如果是字段异常注册的haul
//fixedField就是获取当前字段的full path,即当前字段名加上嵌套路径
//例如: 字段名为: name ,full path为dog.name
String fixedField = fixedField(field);
//获取真实的字段值
Object newVal = getActualFieldValue(fixedField);
//构造字段错误
FieldError fe = new FieldError(getObjectName(), fixedField, newVal, false,
//解析错误码,返回错误信息列表
resolveMessageCodes(errorCode, field), errorArgs, defaultMessage);
//加入Errors集合
addError(fe);
}
AbstractBindingResult重点还是放在了对错误信息记录和查询的处理上,具体大家可以自行查看源码。
MessageCodesResolver解析错误码
MessageCodesResolver是负责对错误码进行解析,然后返回一个String数组,里面保存了具体的错误细节,方便用户获取查看。
MessageCodesResolver是一个接口,它下面只有一个具体实现: DefaultMessageCodesResolver
代码语言:javascript复制public interface MessageCodesResolver {
String[] resolveMessageCodes(String errorCode, String objectName);
String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class<?> fieldType);
}
对于我们而言,只需要关心返回的具体错误细节究竟是怎么个肥事就行了。
- 首先是针对object error ,即全局异常处理
1.: code "." object name
2.: code
我们传入错误码之后,返回的数组包含的是上面两条记录,格式如上
- 如果是针对field error
1.: code "." object name "." field
2.: code "." field
3.: code "." field type
4.: code
会返回四条错误信息记录,格式如上
- 假设错误码是 “typeMismatch”, 对象名为 “user”, 字段名为 “age”,那么通过解析后返回的异常信息如下:
1. try "typeMismatch.user.age"
2. try "typeMismatch.age"
3. try "typeMismatch.int"
4. try "typeMismatch"
- 如果是针对集合中某个属性的错误,那么返回的错误信息格式大致如下:
1. try "typeMismatch.user.groups[0].name"
2. try "typeMismatch.user.groups.name"
3. try "typeMismatch.groups[0].name"
4. try "typeMismatch.groups.name"
5. try "typeMismatch.name"
6. try "typeMismatch.java.lang.String"
7. try "typeMismatch"
这里错误码的解析可能没有大家想的那样复杂,仅仅只是对错误信息进行整合后返回
AbstractPropertyBindingResult—加入类型转换处理
AbstractPropertyBindingResult名字中的Property就表名了当前类的侧重点会放在属性上,而我们知道BindingResult提供了类型转换接口,但是到目前为止一直没看到具体实现,因此这里肯定就是对类型转换进行支持了。
- 关于对类型转换器的获取,AbstractPropertyBindingResult内部提供了一个具体的方法,而ConversionService成员属性则是配合 ConfigurablePropertyAccessor使用的
@Nullable
private transient ConversionService conversionService;
public abstract ConfigurablePropertyAccessor getPropertyAccessor();
- 内部initConversion方法可以看出,当我们需要ConversionService支持时,其实是交给了ConfigurablePropertyAccessor进行管理
public void initConversion(ConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService must not be null");
this.conversionService = conversionService;
if (getTarget() != null) {
getPropertyAccessor().setConversionService(conversionService);
}
}
该类内部提供了一系列关于查找类型转换器,通过查找到的类型转换器获取和设置字段的值和类型等。
细节就不多提了,具体大家可以自行去看该类的源码。
重点是该类下面还有两个具体的实现类,这两个类的区别其实一眼就可以看出来了
- DirectFieldBindingResult的getPropertyAccessor()方法返回的是DirectFieldAccessor
- BeanPropertyBindingResult的getPropertyAccessor()方法返回的是BeanWrapper
对于DirectFieldAccessor和BeanWrapper不了解的建议先去看看我之前的文章。
DirectFieldBindingResult—返回DirectFieldAccessor
代码语言:javascript复制public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
//目标对象
@Nullable
private final Object target;
//是否支持级联属性
private final boolean autoGrowNestedPaths;
//创建出来的directFieldAccessor
@Nullable
private transient ConfigurablePropertyAccessor directFieldAccessor;
public DirectFieldBindingResult(@Nullable Object target, String objectName) {
this(target, objectName, true);
}
public DirectFieldBindingResult(@Nullable Object target, String objectName, boolean autoGrowNestedPaths) {
super(objectName);
this.target = target;
this.autoGrowNestedPaths = autoGrowNestedPaths;
}
@Override
@Nullable
public final Object getTarget() {
return this.target;
}
@Override
public final ConfigurablePropertyAccessor getPropertyAccessor() {
if (this.directFieldAccessor == null) {
//createDirectFieldAccessor返回的是DirectFieldAccessor
this.directFieldAccessor = createDirectFieldAccessor();
this.directFieldAccessor.setExtractOldValueForEditor(true);
this.directFieldAccessor.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
}
return this.directFieldAccessor;
}
protected ConfigurablePropertyAccessor createDirectFieldAccessor() {
if (this.target == null) {
throw new IllegalStateException("Cannot access fields on null target instance '" getObjectName() "'");
}
return PropertyAccessorFactory.forDirectFieldAccess(this.target);
}
}
DirectFieldBindingResult提供的PropertyAccessor实现是DirectFieldAccessor。
BeanPropertyBindingResult—返回BeanWrapper
代码语言:javascript复制public class BeanPropertyBindingResult extends AbstractPropertyBindingResult implements Serializable {
@Nullable
private final Object target;
//支持级联属性
private final boolean autoGrowNestedPaths;
//集合自增长最大长度
private final int autoGrowCollectionLimit;
@Nullable
private transient BeanWrapper beanWrapper;
public BeanPropertyBindingResult(@Nullable Object target, String objectName) {
this(target, objectName, true, Integer.MAX_VALUE);
}
public BeanPropertyBindingResult(@Nullable Object target, String objectName,
boolean autoGrowNestedPaths, int autoGrowCollectionLimit) {
super(objectName);
this.target = target;
this.autoGrowNestedPaths = autoGrowNestedPaths;
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
}
@Override
@Nullable
public final Object getTarget() {
return this.target;
}
@Override
public final ConfigurablePropertyAccessor getPropertyAccessor() {
if (this.beanWrapper == null) {
this.beanWrapper = createBeanWrapper();
this.beanWrapper.setExtractOldValueForEditor(true);
this.beanWrapper.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
this.beanWrapper.setAutoGrowCollectionLimit(this.autoGrowCollectionLimit);
}
return this.beanWrapper;
}
protected BeanWrapper createBeanWrapper() {
if (this.target == null) {
throw new IllegalStateException("Cannot access properties on null bean instance '" getObjectName() "'");
}
return PropertyAccessorFactory.forBeanPropertyAccess(this.target);
}
}
BeanPropertyBindingResult提供的propertyAccessor实现是BeanWrapper.
DataBinder如何使用BindingResult
因为对于DataBinder而言,BindingResult的具体实现有两种,那么为了区分使用哪一种实现,就提供了一个标记来决定。
代码语言:javascript复制 @Nullable
private AbstractPropertyBindingResult bindingResult;
//该标记为真的时候,表示使用DirectFieldBindingResult作为实现
//这里可以看出,默认使用BeanPropertyBindingResult
private boolean directFieldAccess = false;
获取BindingResult的方法如下:
代码语言:javascript复制 protected AbstractPropertyBindingResult getInternalBindingResult() {
if (this.bindingResult == null) {
//通过标记决定创建哪一个具体实现
this.bindingResult = (this.directFieldAccess ?
createDirectFieldBindingResult(): createBeanPropertyBindingResult());
}
return this.bindingResult;
}
默认是BeanPropertyBindingResult :
代码语言:javascript复制 protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
BeanPropertyBindingResult result = new BeanPropertyBindingResult(getTarget(),
getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit());
if (this.conversionService != null) {
result.initConversion(this.conversionService);
}
if (this.messageCodesResolver != null) {
result.setMessageCodesResolver(this.messageCodesResolver);
}
return result;
}
对于getPropertyAccessor方法而言,其实也是寻求BindingResult的帮助:
代码语言:javascript复制 protected ConfigurablePropertyAccessor getPropertyAccessor() {
return getInternalBindingResult().getPropertyAccessor();
}
关于字段的限制
DataBinder在进行数据绑定工作前,会先对需要进行数据绑定的字段进行检查,判断是否符合相关字段限制,例如: 必须填充的字段是否存在,是否存在被禁止的字段填充,等等…
- 像这种必填字段,大家都用过@RequiredParam注解,相信都清楚
通过这种字段限制,可以阻止类似于恶意字段提交攻击等等。
DataBinder为我们提供对字段处理的方式如下:
代码语言:javascript复制//是否忽略不知道的字段---默认忽略
private boolean ignoreUnknownFields = true;
//是否忽略无效字段-----默认不忽略
private boolean ignoreInvalidFields = false;
//只有该集合中的字段才是被允许的填充的
@Nullable
private String[] allowedFields;
//存在于该集合中的字段都是被禁止的
@Nullable
private String[] disallowedFields;
//存在于该集合中的字段都是必须存在的
@Nullable
private String[] requiredFields;
至于字段处理的具体应用,那就需要走入DataBinder的核心,bind方法了
进入bind核心方法
bind方法接收一个PropertyValues,然后调用doBind方法,完成PropertyValues 到对应Target对象的属性绑定工作。
代码语言:javascript复制 public void bind(PropertyValues pvs) {
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ?
(MutablePropertyValues) pvs : new MutablePropertyValues(pvs));
doBind(mpvs);
}
doBind方法执行具体数据绑定前,会先对字段限制进行一波检查:
代码语言:javascript复制 protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
开始进行字段限制检查
代码语言:javascript复制 protected void checkAllowedFields(MutablePropertyValues mpvs) {
PropertyValue[] pvs = mpvs.getPropertyValues();
//挨个遍历每个需要进行绑定的propertyValue信息
for (PropertyValue pv : pvs) {
String field = PropertyAccessorUtils.canonicalPropertyName(pv.getName());
//判断字段是否合法
if (!isAllowed(field)) {
//如果不合法,就从mpvs中移除
mpvs.removePropertyValue(pv);
//并且记录到SuppressedField集合中
getBindingResult().recordSuppressedField(field);
if (logger.isDebugEnabled()) {
logger.debug("Field [" field "] has been removed from PropertyValues "
"and will not be bound, because it has not been found in the list of allowed fields");
}
}
}
}
isAllowed方法判断对应的字段名是否存在于allowed集合中,并且不存在于disallowed集合中:
代码语言:javascript复制 protected boolean isAllowed(String field) {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field)));
}
如果allow或者disallowed为空,那么会跳过对该集合的检查
对必须存在的字段进行检查:
代码语言:javascript复制 protected void checkRequiredFields(MutablePropertyValues mpvs) {
String[] requiredFields = getRequiredFields();
if (!ObjectUtils.isEmpty(requiredFields)) {
Map<String, PropertyValue> propertyValues = new HashMap<>();
PropertyValue[] pvs = mpvs.getPropertyValues();
for (PropertyValue pv : pvs) {
String canonicalName = PropertyAccessorUtils.canonicalPropertyName(pv.getName());
propertyValues.put(canonicalName, pv);
}
//遍历requiredFields集合
for (String field : requiredFields) {
//从propertyValues中尝试取出对应field,如果取出来了,说明当前必填字段存在,否则说明当前必填字段缺失了
PropertyValue pv = propertyValues.get(field);
//判断当前必填字段是否缺失
boolean empty = (pv == null || pv.getValue() == null);
if (!empty) {
//如果对应属性值是字符串或者数组
//那么如果字符串或者数组的值为空,也算缺失
if (pv.getValue() instanceof String) {
empty = !StringUtils.hasText((String) pv.getValue());
}
else if (pv.getValue() instanceof String[]) {
String[] values = (String[]) pv.getValue();
empty = (values.length == 0 || !StringUtils.hasText(values[0]));
}
}
//如果当前必填字段缺失了,那么进行错误记录
if (empty) {
//通过绑定错误处理器来处理字段缺失错误。
getBindingErrorProcessor().processMissingFieldError(field, getInternalBindingResult());
//如果pv不为空,从mpvs集合中移除该必填字段
if (pv != null) {
mpvs.removePropertyValue(pv);
propertyValues.remove(field);
}
}
}
}
}
BindingErrorProcessor如何处理相关字段错误的
从checkRequiredFields方法可以看出来,如果出现了必填字段的缺失,那么会将当前错误处理交给BindingErrorProcessor来完成, 那么BindingErrorProcessor是怎么处理的呢?
代码语言:javascript复制public interface BindingErrorProcessor {
void processMissingFieldError(String missingField, BindingResult bindingResult);
void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult);
}
BindingErrorProcessor接口本身只提供了两个方法,一个是字段缺失错误处理,一个是字段访问异常错误处理。
它的具体实现也只有一个,即DefaultBindingErrorProcessor:
代码语言:javascript复制public class DefaultBindingErrorProcessor implements BindingErrorProcessor {
public static final String MISSING_FIELD_ERROR_CODE = "required";
@Override
public void processMissingFieldError(String missingField, BindingResult bindingResult) {
//拿到当前缺失字段的full path
String fixedField = bindingResult.getNestedPath() missingField;
//通过bindingResult的resolveMessageCodes方法来解析字段缺失错误码----required
String[] codes = bindingResult.resolveMessageCodes(MISSING_FIELD_ERROR_CODE, missingField);
Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), fixedField);
//构造一个字段错误
FieldError error = new FieldError(bindingResult.getObjectName(), fixedField, "", true,
codes, arguments, "Field '" fixedField "' is required");
//加入Error集合
bindingResult.addError(error);
}
@Override
public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
//如果出现了属性访问异常错误---拿到属性名
String field = ex.getPropertyName();
Assert.state(field != null, "No field in exception");
//解析错误码
String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field);
//拿到被拒绝的value值
Object rejectedValue = ex.getValue();
if (ObjectUtils.isArray(rejectedValue)) {
rejectedValue = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(rejectedValue));
}
//构造字段错误
FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true,
codes, arguments, ex.getLocalizedMessage());
//将异常也包装进去
error.wrap(ex);
//加入Error集合
bindingResult.addError(error);
}
protected Object[] getArgumentsForBindError(String objectName, String field) {
String[] codes = new String[] {objectName Errors.NESTED_PATH_SEPARATOR field, field};
return new Object[] {new DefaultMessageSourceResolvable(codes, field)};
}
}
BindingErrorProcessor会负责将相关错误构造成一个FieldError,然后加入Error集合。
applyPropertyValues真正开始数据绑定
applyPropertyValues是DataBinder的核心方法之一,是完成具体数据绑定过程的方法:
代码语言:javascript复制 protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
//本质是通过BeanWrapper或者DirectFieldAccessor来设置mpvs
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
//数据绑定过程中抛出的异常交给BindingErrorProcessor处理
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
其实数据绑定核心靠的是AbstractNestablePropertyAccessor完成的,该抽象类是BeanWrapper和DirectFieldAccessor的父类,该抽象类主要负责完成数据的类型转换,然后将转换的结果,再通过调用子类的实现,来完成属性的注入。
- BeanWrapper设置属性值,是通过提供的setter方法
- DirectFieldAccessor设置属性值,是通过反射直接设置
close方法判断是否产生了异常
当我们使用DataBinder完成了数据绑定工作后,还需要判断之前是否收集到了相关error,如果有的话,就抛出来,让用户感知到异常的发生:
代码语言:javascript复制 public Map<?, ?> close() throws BindException {
if (getBindingResult().hasErrors()) {
throw new BindException(getBindingResult());
}
return getBindingResult().getModel();
}
如果没有异常发生,会返回一个Model,getModel方法的具体实现在AbstractBindingResult中:
代码语言:javascript复制 @Override
public Map<String, Object> getModel() {
Map<String, Object> model = new LinkedHashMap<>(2);
//返回target目标对象
model.put(getObjectName(), getTarget());
// 返回当前BindingResult对象
model.put(MODEL_KEY_PREFIX getObjectName(), this);
return model;
}
关于数据校验
可以看到,在DataBinder的核心bind方法调用过程中,并没有涉及到对数据进行校验的内容,难道是DataBinder不提供对数据校验的支持吗?
并不是DataBinder不支持数据校验,而是将数据校验和数据绑定过程分开了,DataBinder提供了对数据校验的支持:
代码语言:javascript复制 private final List<Validator> validators = new ArrayList<>();
通过内部维护的validators 校验器集合,和下面提供的这些方法,我们可以在数据绑定完成后,进行数据校验工作:
代码语言:javascript复制 //新增校验器
public void setValidator(@Nullable Validator validator) {
assertValidators(validator);
this.validators.clear();
if (validator != null) {
this.validators.add(validator);
}
}
private void assertValidators(Validator... validators) {
Object target = getTarget();
//判断新增的校验器是否支持对当前目标对象的校验
for (Validator validator : validators) {
if (validator != null && (target != null && !validator.supports(target.getClass()))) {
throw new IllegalStateException("Invalid target for Validator [" validator "]: " target);
}
}
}
public List<Validator> getValidators() {
return Collections.unmodifiableList(this.validators);
}
public void validate() {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
//进行数据校验
for (Validator validator : getValidators()) {
validator.validate(target, bindingResult);
}
}
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}
可以看到DataBinder提供了对数据校验的支持,但是我们并没有在DataBinder进行数据绑定的流程中看到校验器的参与,这是因为具体校验器在哪里使用,是由子类来决定的。