Spring数据绑定之DataBinder篇---01

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring数据绑定之DataBinder篇---01相关的知识,希望对你有一定的参考价值。

Spring数据绑定之DataBinder篇---01


前言

数据绑定对于一个成熟的Web框架而言十分的重要,通过将Http中的请求参数或者请求体中的Json字符串绑定到对应实体对象上,可以大大提高开发人员的效率。

传统Servelt编程中,仅仅是将Http数据报文中的相关请求参数封装到了Request对象中,这样做的好处是给了开发人员足够的自由性,可以自由取出相关参数进行操作。

坏处是增加了重复编码劳作,例如: 重复的数据绑定工作和数据校验工作。

一般在请求参数比较多的情况下,会采用一个专门的Model对象来封装这些请求参数,因此,这也是为什么需要数据绑定的原因。而在将请求参数绑定到Model对象上时,需要对请求参数值进行校验,判断是否符合逻辑,因此也就引出了数据校验。

对于Spring来说,我们只需要在Controller类负责接收请求的方法中,将Model对象作为方法参数给出,就可以完成request参数到Model对象的自动数据绑定。


Spring数据绑定体系

对于Spring的数据绑定体系来说,是以DataBinder为基础设施开展。并且DataBinder不仅仅只会做数据绑定,还会进行数据校验Validation。

下面我们先来看一下使用DataBinder完成数据绑定的案例:

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告知用户具体是哪个对象出现了什么样子的错误。

   	//如果用户没有指定objectName,那么默认值为target
   	public static final String DEFAULT_OBJECT_NAME = "target";
   	
	@Nullable
	private final Object target;

	private final String objectName;


DataBinder类型转换靠谁?

public class DataBinder implements PropertyEditorRegistry, TypeConverter 

DataBinder继承了有关类型转换的上层接口,说明DataBinder 本身具备了以下能力:

  • 向外界提供Spring统一的类型转换接口: TypeConverter
  • 可以新增用户自定义的类型转化器: PropertyEditorRegistry
  • 因为并没有继承ConverterRegistry接口,因此并没有提供可以新增Converter的方法

但是DataBinder内部依靠的是Spring已经完善好的类型转换体系: PropertyEditor和ConversionService来具体完成类型转换的任务。

这是Spring中常用的代理思想,我继承了接口,只是告诉外界我提供了这种功能,但是具体功能实现,我依靠的是代理对象。
在进行模块化开发时,会很有用,无论是上层模块调用底层模块实现具体功能。还是在没有严格分层关系的模块中,某个模块需要另一个模块的功能支持,也可以向外界提供实现接口,但是该模块通过调用另一个模块完成接口的实现。


关于类型转换,DataBinder类中维护了以下两个变量:

	@Nullable
	private ConversionService conversionService;	 
	@Nullable
	private SimpleTypeConverter typeConverter;

首先关于ConversionService,DataBinder中并没有继承ConverterRegistry接口,因此没有向用户暴露新增Converter的方法。

但是DataBinder内部却维护了一个ConversionService成员属性,并且提供了sette和getter方法:

    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 类定义如下:

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是哪里来的呢?

	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来完成的。

	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的时候,调用以下方法即可:

	void reject(String errorCode);

	void reject(String errorCode, String defaultMessage);

	void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);

注册字段错误:

    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);

提供给用户判断是否产生相关错误的方法有下面这些,因为关于这部分的方法很多,我只列举一部分,更多细节,大家查阅源码即可:

    boolean hasErrors();
	
	boolean hasGlobalErrors();
	
	boolean hasFieldErrors();
	
	boolean hasFieldErrors(String field);
	
	List<FieldError> getFieldErrors(String field);
	
	....

Error接口还提供了设置嵌套路径的方法如下:

	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接口。

public interface BindingResult extends Errors 

在DataBinder获取转换器的方法中,都会首先去BindingResult中获取,如果获取不到,再寻找自身默认提供的SimpleTypeConverter,因此BindingResult 接口中需要提供有关转换器的方法。

	PropertyEditor findEditor(@Nullable String field, @Nullable Class<?> valueType);

	@Nullable
	PropertyEditorRegistry getPropertyEditorRegistry();

因为BindResult中还需要在数据绑定完毕后,进行数据校验,然后保存校验中产生的失败字段和失败信息,因此需要提供相关方法:

    //记录当前字段的值
	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进行解析的方法:

	String[] resolveMessageCodes(String errorCode);

	String[] resolveMessageCodes(String errorCode, String field);

BindingResult只是对外提供了接口,但是具体异常码解析则是交给了MessageCodesResolver来完成。


AbstractBindingResult实现

AbstractBindingResult是BindingResult接口的直接实现,并且继承了AbstractErrors,相当于将Error接口的实现,通过继承AbstractErrors间接完成。

AbstractErrors仅仅只是对Error接口进行了实现,感兴趣的可以自己去看看具体实现过程是怎样的

public abstract class AbstractBindingResult extends AbstractErrors implements BindingResult, Serializable 

AbstractBindingResult中提供了相关工具类和容器的具体实现:

    //要操作的对象名
	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来完成。

    //添加全局异常---针对当前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

public interface MessageCodesResolver 
	String[] resolveMessageCodes(String errorCode, String objectName);
	StringSpring官网阅读(十七)Spring中的数据校验

Spring MVC Data Binding

WebDataBinderServletRequestDataBinderWebBindingInitializer

数据绑定

数据绑定流程分析

数据绑定流程分析