【重写SpringFramework】DataBinder(chapter 3-15)

203 阅读9分钟

1. 前言

第一章介绍了类型转换和属性访问,本章又介绍了格式化器和数据校验,这些功能看似零散,实际上都是被一条主线串联起来,那就是数据处理。一个典型的例子是,网络接口方法的参数往往是一个对象,而数据来源则是 HTTP 请求参数。为了将网络请求的参数赋给一个对象,可能涉及如下操作。

首先是数据绑定,这一操作由属性访问来完成。在绑定的过程中,可能需要进行类型转换;此外,根据业务需要还要对数据进行校验。这些操作分别由不同的组件来提供,现在的问题是,有没有一个组件能够完成这些操作?DataBinder 就是带着这样的任务出现的,让我们来一窥究竟吧。

2. 组织结构

DataBinder 作为一个门面类,对外界提供了诸多实用的功能,从大的层面来说可以分为三种:属性访问、类型转换、数据校验。

  • BindingResult:绑定结果包括两部分,一是通过属性访问为对象赋值,二是负责存储数据绑定过程中赋值和校验两个阶段可能产生的错误。
  • TypeConverter:类型转换的功能由两部分组成,一是 Java 提供的属性编辑器,二是 Spring 核心包的转换服务。DefaultConversionService 的功能是由一系列转换器(Converter)实现的,FormattingConversionService 扩展了转换服务,通过 Formatter 提供了格式化的功能。
  • Validator:数据校验的功能是委托给 javax.validation.Validtor 接口实现的,比如 Hibernate 框架提供了具体的校验规则.

15.1 DataBinder组织结构.png

TypeConverter 是本教程的第一个通过代码实现的组件,我们指出,面向对象编程的核心理念是对已有资源的调配和使用,高效的编程是一门管理的学问。DataBinder 很好地体现了这一点,从组织结构图中可以看到,众多组件被有条不紊地组织起来,体现出秩序的美感。

这些组件来自不同的框架,涵盖了 JDK、第三方库以及 Spring 的多个模块。其中,PropertyEditorValidator 是 JDK 定义的,ValidatorImpl 是 Hibernate 框架提供的实现。其余组件都是 Spring 框架提供的,但分属不同的模块,具体情况如下:

  • ConverterConversionService 属于 core 模块
  • PropertyAccessorTypeConverter 属于 beans 模块
  • FormatterBindingResult 和 Spring 验证器属于 context 模块

3. DataBinder

3.1 概述

DataBinder 作为数据处理的集大成者,提供了三大功能,分别是类型转换、数据绑定和数据校验。一般来说,数据绑定是包括类型转换功能的,但是 DataBinder 将类型转换功能单独拿出来使用。此外,格式化也是类型转换的一部分,我们通过指定 conversionService 字段来实现。

  • target:表示目标对象,如果只是类型转换,目标对象可以不存在
  • bindingResult:表示绑定结果,用于存储目标对象以及绑定和校验阶段出现的异常
  • bindingErrorProcessor:用于处理绑定阶段时产生的异常
  • conversionService:默认的转换服务没有格式化的功能,需要单独指定
  • validators:Spring 验证器集合
public class DataBinder implements PropertyEditorRegistry, TypeConverter {
    private final Object target;
    private final String objectName;
    private AbstractPropertyBindingResult bindingResult;
    private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
    private SimpleTypeConverter typeConverter = new SimpleTypeConverter();
    private ConversionService conversionService = new DefaultConversionService();
    private final List<Validator> validators = new ArrayList<>();	//Spring验证器集合
}

3.2 类型转换

关于类型转换,主要关注两个方法。首先是 setConversionService 方法,可以指定 FormattingConversionService 实例来提供格式化的功能。其次是 convertIfNecessary 方法,一共有三个重载方法,底层由 TypeConverter 来处理。

public void setConversionService(ConversionService conversionService) {
    this.conversionService = conversionService;
    this.typeConverter.setConversionService(conversionService);

    if (bindingResult != null) {
        this.bindingResult.initConversion(conversionService);
    }
}

@Override
public <T> T convertIfNecessary(Object value, Class<T> requiredType) {
    return getTypeConverter().convertIfNecessary(value,requiredType);
}

3.3 数据绑定

bind 方法是属性访问的入口方法,具体的逻辑是由 doBind 方法处理的。之所以分成两个方法,是因为数据的来源是不确定的。PropertyValues 接口是 Spring 定义的标准数据来源,除此之外,数据的来源可以是网络请求或者配置文件等。对于这些情况,DataBinder 的子类提供同名的 bind 方法来处理,比如 WebDataBinder 子类的 bind 方法接受的参数类型为 NativeWebRequest,表明数据来自请求参数。

public void bind(PropertyValues pvs) {
    doBind((MutablePropertyValues) pvs);
}

不管什么情况,最终都要调用 doBind 方法。在对象赋值的过程中,需要捕获异常。上一节介绍了 BindingErrorProcessor 组件,作用是将异常包装成 FieldError,并保存到 BindingResult 中。这样一来,BindingResult 作为中间桥梁,将赋值和校验两个操作联系起来了。

protected void doBind(MutablePropertyValues mpvs) {
    try {
        getPropertyAccessor().setPropertyValues(mpvs);
    }catch (PropertyAccessException e){
        //处理属性访问时发生的错误
        this.bindingErrorProcessor.processPropertyAccessException(e, getInternalBindingResult());
    }
}

3.4 数据校验

validate 方法提供了数据校验的功能,遍历验证器集合进行处理,实际上只有 SpringValidatorAdapter 一个实例。需要注意的是,目标对象有两个来源,一是手动创建的,说明单独使用了数据校验的功能。二是来自属性访问,这意味着 BindingResult 可能持有绑定阶段的错误。

关于具体的校验逻辑已经详细讲解过。如果出现异常,则创建 FieldError 对象并保存到 BindingResult 中。BindingResult 如果存在错误,可能是在绑定阶段出现的,也有可能是在校验阶段产生的。无论如何,最终都能拿到 BindingResult 对象,至于目标对象与错误列表如何处理,由调用方自行决定。

public void validate() {
    for (Validator validator : this.validators) {
        validator.validate(getTarget(), getBindingResult());
    }
}

上一节我们以数据校验为主,顺带提及了数据绑定与校验之间的关系。DataBinder 更进一步,将绑定和校验两个操作有机地整合起来,既可以单独使用,也可以共同发挥作用。

4. 模型

ui 包在 context 模块中是一个独立的部分,主要是为 MVC 模式提供支持。一般来说,网络请求返回的是视图和模型。视图可以理解为一个网页,而模型则是动态填充的数据,视图和模型共同构成了网页渲染的内容。在 Spring MVC 中,我们可以声明 ModelModelMap 类型的参数或返回值,其他工作交给框架来处理。

Model 接口表示一个模型对象,允许以 Map 的方式访问模型。addAttribute 方法的作用是向模型中添加一个属性。

public interface Model {
    Model addAttribute(String attributeName, Object attributeValue);
}

ModelMap 继承了 LinkedHashMap,通过建造模式来构造实例。addAttribute 方法支持链式调用,containsAttribute 方法是对 containsKey 方法的简单包装。

public class ModelMap extends LinkedHashMap<String, Object>{

    public ModelMap addAttribute(String attributeName, Object attributeValue) {
        put(attributeName, attributeValue);
        return this;
    }

    public boolean containsAttribute(String attributeName) {
        return containsKey(attributeName);
    }
}

5. 测试

本测试是一个综合用例,测试了 DataBinder 的三大功能,分别是类型转换、属性访问和数据校验。

  1. 模拟请求参数,phone 字段不是 11 位,测试校验异常。age 字段不是数值类型,测试赋值(属性访问)异常。regDate 字段是字符串类型,而实体类对应字段的类型是 Date 类型,因此这里测试的是类型转换的特殊形式,即格式化的功能。
  2. 准备 DataBinder 实例。需要注意三点,一是在构造器中传入了一个空的 LoginModel 对象;二是设置 FormattingConversionService,拥有格式化功能;三是添加 Spring 校验器。
  3. 先执行数据绑定操作,然后对已绑定的数据进行校验。
//测试方法
@Test
public void testDataBinder() {
    //1. 模拟http请求
    Map<String, Object> requestParam = new HashMap<>();
    requestParam.put("name", "Tom");
    requestParam.put("phone", "12305");         //手机号不是11位,校验失败
    requestParam.put("age", "abc");             //非数字类型,绑定失败
    requestParam.put("regDate", "2024/11/22");  //字符串类型,需要格式化

    //2. 准备DataBinder实例
    DataBinder binder = new DataBinder(new LoginModel());
    FormattingConversionService conversionService = new FormattingConversionService();
    conversionService.addFormatter(new DateFormatter("yyyy/MM/dd"));
    binder.setConversionService(conversionService);         //添加带有格式化器的转换服务
    binder.setValidator(new SpringValidatorAdapter());      //添加校验器

    //3. 先绑定数据,再校验
    binder.bind(new MutablePropertyValues(requestParam));
    binder.validate();

    //4. 对BindingResult进行处理
    for (FieldError fieldError : binder.getBindingResult().getFieldErrors()) {
        System.out.println("字段[" + fieldError.getField() + "]错误: "  + fieldError.getDefaultMessage());
    }
    System.out.println("绑定后的对象: " + binder.getTarget());
}

从测试结果可以得到以下结论:

  • age 字段的错误是赋值阶段的,原因是 abc 不是数值类型
  • phone 字段的错误是校验阶段的,原因是 12305 不是合法的手机号
  • 打印绑定后的数据,age 字段为 0,说明绑定失败。reaDate 字段成功赋值,说明执行了格式化的操作。
字段[age]错误: 为Bean[context.data.LoginModel]的属性[age]赋值失败; nested exception is java.lang.NumberFormatException: For input string: "abc"
字段[phone]错误: 手机号必须是11位
绑定后的对象: LoginModel{name='Tom', phone='12305', age=0, regDate=Fri Nov 22 00:00:00 CST 2024}

6. 总结

本节介绍了一个强大的数据处理工具,DataBinder 本身没有提供新的功能,而是完成了对众多组件的整合。总的来说,DataBinder 提供了三大功能:

  • 数据绑定:Spring 通过属性访问将外部数据绑定到一个对象上,绑定结果则包含了绑定对象和异常信息
  • 数据校验:JDK 定义了验证器接口,Hibernate 框架提供了实现类,Spring 的验证器通过适配 Hibernate 验证器的方式发挥作用
  • 类型转换:之前介绍了属性编辑器和转换器两种方式,格式化器可以看作是转换器的特殊形式

15.2 DataBinder脑图.png

DataBinder 作为数据处理的集大成者,从某种程度上来说,可以看作是 context 模块的缩影。它们都是面向用户(开发者)的,通过对若干功能的整合,提供一站式的快捷服务。我们把 DataBinder 作为本章的压轴,也是为了说明这个问题。context 模块集成了 core、beans、aop、expression 等底层模块,同时还提供了非常多实用的功能。用户只需要与这一个模块打交道,屏蔽了底层的细节实现,有效地提高了开发的效率。

DataBinder 的故事还没有讲完,在后边的章节中还有用武之地。在第五章 web 模块中,DabaBinder 被用来处理请求参数。在第二部 Spring Boot 中,属性类作为自动配置的重要组件,底层也是通过 DataBinder 来实现的。

7. 项目信息

新增修改一览,新增(3),修改(1)。

context
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring
   │        ├─ ui
   │        │  ├─ Model.java (+)
   │        │  └─ ModelMap.java (+)
   │        └─ validation
   │           └─ DataBinder.java (+)
   └─ test
      └─ java
         └─ context
            └─ data
               └─ ValidatorTest.java (*)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。