1. 前言
第一章介绍了类型转换和属性访问,本章又介绍了格式化器和数据校验,这些功能看似零散,实际上都是被一条主线串联起来,那就是数据处理。一个典型的例子是,网络接口方法的参数往往是一个对象,而数据来源则是 HTTP 请求参数。为了将网络请求的参数赋给一个对象,可能涉及如下操作。
首先是数据绑定,这一操作由属性访问来完成。在绑定的过程中,可能需要进行类型转换;此外,根据业务需要还要对数据进行校验。这些操作分别由不同的组件来提供,现在的问题是,有没有一个组件能够完成这些操作?DataBinder 就是带着这样的任务出现的,让我们来一窥究竟吧。
2. 组织结构
DataBinder 作为一个门面类,对外界提供了诸多实用的功能,从大的层面来说可以分为三种:属性访问、类型转换、数据校验。
BindingResult:绑定结果包括两部分,一是通过属性访问为对象赋值,二是负责存储数据绑定过程中赋值和校验两个阶段可能产生的错误。TypeConverter:类型转换的功能由两部分组成,一是 Java 提供的属性编辑器,二是 Spring 核心包的转换服务。DefaultConversionService的功能是由一系列转换器(Converter)实现的,FormattingConversionService扩展了转换服务,通过 Formatter 提供了格式化的功能。Validator:数据校验的功能是委托给javax.validation.Validtor接口实现的,比如 Hibernate 框架提供了具体的校验规则.
TypeConverter 是本教程的第一个通过代码实现的组件,我们指出,面向对象编程的核心理念是对已有资源的调配和使用,高效的编程是一门管理的学问。DataBinder 很好地体现了这一点,从组织结构图中可以看到,众多组件被有条不紊地组织起来,体现出秩序的美感。
这些组件来自不同的框架,涵盖了 JDK、第三方库以及 Spring 的多个模块。其中,PropertyEditor 和 Validator 是 JDK 定义的,ValidatorImpl 是 Hibernate 框架提供的实现。其余组件都是 Spring 框架提供的,但分属不同的模块,具体情况如下:
Converter和ConversionService属于 core 模块PropertyAccessor和TypeConverter属于 beans 模块Formatter、BindingResult和 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 中,我们可以声明 Model 或 ModelMap 类型的参数或返回值,其他工作交给框架来处理。
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 的三大功能,分别是类型转换、属性访问和数据校验。
- 模拟请求参数,
phone字段不是 11 位,测试校验异常。age字段不是数值类型,测试赋值(属性访问)异常。regDate字段是字符串类型,而实体类对应字段的类型是Date类型,因此这里测试的是类型转换的特殊形式,即格式化的功能。 - 准备
DataBinder实例。需要注意三点,一是在构造器中传入了一个空的LoginModel对象;二是设置FormattingConversionService,拥有格式化功能;三是添加 Spring 校验器。 - 先执行数据绑定操作,然后对已绑定的数据进行校验。
//测试方法
@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 验证器的方式发挥作用
- 类型转换:之前介绍了属性编辑器和转换器两种方式,格式化器可以看作是转换器的特殊形式
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编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。