Spring 学习指南大全
Spring 核心技术
官方文档版本 Version 5.2.22.RELEASE
Validation, Data Binding, and Type Conversion : (验证/校验)、数据绑定和类型转换
将验证视为业务逻辑有利也有弊,Spring 提供的 validation (验证)(和数据绑定)设计不排除其中任何一种。具体来说,验证不应该绑定到 web 层,应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个 Validator(验证器)契约,它既是基本的,又非常适用于应用程序的每一层。
数据绑定对于将用户输入动态绑定到 application (应用程序)的 domain(域)model (模型) (或用于处理用户输入的任何对象)非常有用。Spring 提供了名副其实的 DataBinder 来做这件事。Validator(验证器)和 DataBinder(数据绑定器) 组成了 validation(验证)包,主要用于但不限于 web 层。
BeanWrapper 是 Spring 框架中的一个基本概念,在很多地方都有使用。但是,您可能不需要直接使用 BeanWrapper。但是,因为这是参考文档,所以我们认为应该做一些解释。我们将在本章中解释 BeanWrapper,因为如果您要使用它,您很可能会在试图将数据绑定到对象时使用它。
Spring 的 DataBinder 和底层的 BeanWrapper 都使用 PropertyEditorSupport 实现来解析和格式化属性值。PropertyEditor 和PropertyEditorSupport 类型是 JavaBeans 规范的一部分,也将在本章中解释。Spring 3 引入了一个 core.convert 包,它提供了一个通用的类型转换工具,以及一个用于格式化 UI 字段值的高级 “format(格式)” 包。您可以使用这些包作为 PropertyEditorSupport 实现的更简单的替代方案。本章也对它们进行了讨论。
Spring 通过设置基础设施和 Spring 自己的 Validator(验证器)契约的适配器来支持 Java Bean 验证。应用程序可以全局启用一次 Bean 验证,如【Java Bean 验证中所述】,并专门使用它来满足所有验证需求。在 web 层,应用程序可以进一步为每个 DataBinder(数据绑定器)注册 controller-local(控制器本地)的Spring Validator(验证器)实例,如【配置数据绑定器】中所述,这对于插入自定义验证逻辑非常有用。
Spring 提供了一个 Validator(验证器)接口,可以用来验证对象。Validator(验证器)接口通过使用一个 Errors 对象来工作,因此在验证时,验证器可以向 Errors 对象报告验证失败。
考虑下面这个小数据对象的例子:
public class Person {
private String name;
private int age;
// 通常的getters和setters...
}
下一个示例通过实现 org.springframework.validation.Validator
接口的以下两个方法为 Person 类提供验证行为:
supports(Class)
:这个 Validator(验证器)可以验证所提供的 Class(类)的实例吗?validate(Object, org.springframework.validation.Errors)
:验证给定的对象,如果出现验证错误,则用给定的 Errors 对象注册这些错误。实现 Validator(验证器) 相当简单,尤其是当您知道 Spring 框架也提供了 ValidationUtils 帮助类时。以下示例为 Person(人员)实例实现了 Validator(验证器):
public class PersonValidator implements Validator {
/**
* 这个验证器只验证 Person 人员实例
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.空的");// 验证 name 是否为空
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "负值");
} else if (p.getAge() > 110) {
e.rejectValue("age", "太过年老的");
}
}
}
static rejectIfEmpty(..)
方法用于在 name 属性为 null 或空字符串时拒绝该属性。看看 ValidationUtils
javadoc,看看除了前面给出的例子之外,它还提供了什么功能。
虽然当然可以实现单个 Validator(验证器) 类来验证丰富的对象中的每个嵌套对象,但是最好将每个嵌套对象类的验证逻辑封装在其自己的验证器实现中。“丰富的” 对象的一个简单例子是由两个字符串属性(第一个和第二个名字)和一个复杂的地址对象组成的客户。Address 对象可以独立于 Customer 对象使用,因此实现了一个独特的 AddressValidator。如果希望 CustomerValidator 重用 AddressValidator 类中包含的逻辑,而不采用复制粘贴的方法,可以在 CustomerValidator 中依赖注入或实例化 AddressValidator,如下例所示:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("提供的[Validator(验证程序)]是" +
"必需且不得为空。");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("提供的[Validator(验证程序)]必须" +
"支持[Address(地址)]实例的验证。");
}
this.addressValidator = addressValidator;
}
/**
* 这个验证器验证客户实例,以及客户的任何子类
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.必需的");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.必需的");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
验证错误被报告给传递给验证器的 errors 对象。对于 Spring Web MVC,您可以使用
标记来检查错误消息,但是您也可以自己检查 Errors 对象。关于它提供的方法的更多信息可以在 javadoc 中找到。
我们讨论了数据绑定和验证。本节介绍输出与验证错误相对应的消息。在上一节显示的示例中,我们拒绝了 name(姓名) 和 age(年龄) 字段。如果我们想通过使用 MessageSource 输出错误消息,我们可以使用拒绝字段时提供的错误代码(在本例中是“name(姓名)”和“age(年龄)”)来实现。当您从 Errors 接口调用 rejectValue 或其他 reject 方法之一(直接或间接,例如使用 ValidationUtils 类)时,底层实现不仅会注册您传入的代码,还会注册许多其他错误代码。MessageCodesResolver 决定 Errors(错误)接口寄存器的错误代码。默认情况下,使用DefaultMessageCodesResolver,例如,它不仅用您给出的代码注册消息,还注册包含您传递给 reject 方法的字段名的消息。因此,如果您使用 rejectValue("age", "too.darn.old")
拒绝一个字段,除了 too.darn.old
代码,Spring 还注册 too.darn.old.age
和too.darn.old.age.int
(第一个包括字段名,第二个包括字段的类型)。这样做是为了方便开发人员定位错误消息。
关于 MessageCodesResolver 和默认策略的更多信息可以分别在 MessageCodesResolver
和 DefaultMessageCodesResolver
的 javadoc中找到。
Bean Manipulation and the
BeanWrapper
: Bean 操作和 BeanWrapper
org.springframework.beans 包遵循 JavaBeans 标准。JavaBean 是一个具有默认无参数构造函数的类,它遵循一个命名约定,例如,一个名为 bingoMadness 的属性有一个 setter 方法 setBingoMadness(…) 和一个 getter 方法 getBingoMadness()。有关 JavaBeans 和规范的更多信息,请参见 javabeans。
beans 包中一个非常重要的类是 BeanWrapper 接口及其相应的实现( BeanWrapperImpl )。正如 javadoc 中引用的那样,BeanWrapper 提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是可读还是可写的功能。此外,BeanWrapper 还提供了对嵌套属性的支持,允许将子属性的属性设置为无限制的深度。BeanWrapper 还支持添加标准 JavaBeans PropertyChangeListeners 和VetoableChangeListeners 的能力,而不需要目标类中的支持代码。最后但同样重要的是,BeanWrapper 提供了对设置索引属性的支持。应用程序代码通常不直接使用 BeanWrapper,而是由 DataBinder 和 BeanFactory 使用。
BeanWrapper 的工作方式部分由其名称表明:它包装一个 bean 来对该 bean 执行操作,比如设置和检索属性。
设置和获取属性是通过 BeanWrapper 的 setPropertyValue 和 getPropertyValue 重载方法变量完成的。详见他们的 Javadoc。下表显示了这些约定的一些示例:
表达式 | 说明 |
---|---|
name | 指示对应于 getName() 或 isName() 和 setName(…) 方法。 |
account.name | 指示对应于(例如) getAccount() 的属性 account(帐户)的嵌套属性名称。getAccount().setName() 或 getAccount().getName() 方法。 |
account[2] | 指示索引属性帐户的第三个元素。索引属性可以是 array(数组)、list(列表) 或其他自然排序的集合类型。 |
account[COMPANYNAME] | 指示由 account(帐户)map(映射)属性的 COMPANYNAME(公司名称) 键索引的 Map(映射) 条目的值。 |
(如果您不打算直接使用 BeanWrapper,那么下一节对您来说并不重要。如果您只使用 DataBinder 和 BeanFactory 以及它们的默认实现,那么您应该跳到关于 PropertyEditors 的部分。)
以下两个示例类使用 BeanWrapper 来获取和设置属性:
public class Company {
private String name;
private Employee managingDirector;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Employee getManagingDirector() {
return this.managingDirector;
}
public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
public class Employee {
private String name;
private float salary;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
以下代码片段显示了如何检索和操作实例化 Companies(公司)和 Employees(雇员)的一些属性的一些示例:
BeanWrapper company = new BeanWrapperImpl(new Company());
// 设置公司名称..
company.setPropertyValue("name", "某公司。");
// ...也可以这样做:
PropertyValue value = new PropertyValue("name", "某公司。");
company.setPropertyValue(value);
// 好,让我们创建导演并将其与公司联系起来:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "吉姆·斯特拉文斯基");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// 通过公司检索经理的工资
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
Spring 使用 PropertyEditor 的概念来实现对象和字符串之间的转换。用不同于对象本身的方式表示属性会很方便。例如,日期可以以人类可读的方式表示(如 String(字符串):“2007-14-09”),而我们仍然可以将人类可读的形式转换回原始日期(或者,更好的是,将以人类可读的形式输入的任何日期转换回日期对象)。这种行为可以通过注册 java.beans.PropertyEditor 类型的自定义编辑器来实现。在BeanWrapper 上注册自定义编辑器,或者在特定的 IoC 容器中注册自定义编辑器(如前一章所述),可以让它知道如何将属性转换为所需的类型。有关 PropertyEditor 的更多信息,请参见 Oracle 的 java.beans 包的 javadoc。
在 Spring 中使用 property editing(属性编辑)的几个例子:
Spring 有许多内置的 PropertyEditor 实现来简化工作。它们都位于 org.springframework.beans.propertyeditors
包中。默认情况下,大多数(但不是全部,如下表所示)由 BeanWrapperImpl 注册。当属性编辑器可以以某种方式配置时,您仍然可以注册自己的变量来覆盖默认变量。下表描述了 Spring 提供的各种 PropertyEditor 实现:
Class | 说明 |
---|---|
ByteArrayPropertyEditor | 字节数组编辑器。将字符串转换为相应的字节表示形式。默认情况下由 BeanWrapperImpl 注册。 |
ClassEditor | 将表示类的字符串解析为实际的类,反之亦然。当找不到类时,将引发 IllegalArgumentException。默认情况下,由 BeanWrapperImpl 注册。 |
CustomBooleanEditor | 布尔属性的可自定义属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。 |
CustomCollectionEditor | Collection(集合)的属性编辑器,将任何源 Collection(集合)转换为给定的目标 Collection(集合)类型。 |
CustomDateEditor | java.util.Date 的可自定义属性编辑器,支持自定义 DateFormat(日期格式)。默认情况下不注册。必须根据需要用适当的格式进行用户注册。 |
CustomNumberEditor | 任何 Number(数字)子类的可定制属性编辑器,例如 Integer、Long、Float 或 Double。默认情况下,由BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。 |
FileEditor | 将字符串解析为 java.io.File 对象。默认情况下,由 BeanWrapperImpl 注册 |
InputStreamEditor | 单向属性编辑器,可以接受字符串并生成(通过中间 ResourceEditor (资源编辑器)和 Resource(资源))InputStream,以便 InputStream 属性可以直接设置为字符串。请注意,默认用法不会为您关闭InputStream。默认情况下,由 BeanWrapperImpl 注册。 |
LocaleEditor | 可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [country][variant] ,与 Locale 的 toString() 方法相同)。默认情况下,由 BeanWrapperImpl 注册。 |
PatternEditor | 可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然。 |
PropertiesEditor | 可以将字符串(格式为 java.util.Properties 类的 javadoc 中定义的格式)转换为 Properties 对象。默认情况下,由 BeanWrapperImpl 注册。 |
StringTrimmerEditor | 修剪字符串的属性编辑器。可选地允许将空字符串转换为 null(空) 值。默认情况下未注册—必须由用户注册。 |
URLEditor | 可以将 URL 的字符串表示形式解析为实际的 URL 对象。默认情况下,由 BeanWrapperImpl 注册。 |
Spring 使用 java.beans.PropertyEditorManager
为可能需要的属性编辑器设置搜索路径。搜索路径还包括 sun.bean.editors,其中包括Font(字体)、Color(颜色) 和大多数基本类型的 PropertyEditor 实现。还要注意,如果 PropertyEditor 类与它们处理的类在同一个包中,并且与该类同名,并附加了 Editor,则标准 JavaBeans 基础设施会自动发现这些类(无需显式注册它们)。例如,可以有下面的类和包结构,这足以让 SomethingEditor 类被识别并用作 Something 类型属性的 PropertyEditor。
com
chank
pop
Something
SomethingEditor // Something 类的属性编辑器
注意,在这里也可以使用标准的 BeanInfo JavaBeans 机制(在这里有一定程度的描述)。以下示例使用 BeanInfo 机制显式注册一个或多个具有关联类属性的 PropertyEditor 实例:
com
chank
pop
Something
SomethingBeanInfo // Something 类的 BeanInfo
以下引用的 SomethingBeanInfo 类的 Java 源代码将 CustomNumberEditor 与 Something 类的 age 属性相关联:
public class SomethingBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
};
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor 实现将这些字符串转换为复杂类型的属性。Spring 预注册了许多自定义 PropertyEditor 实现(例如,将表示为字符串的类名转换为类对象)。此外,Java 的标准 JavaBeans PropertyEditor 查找机制允许适当地命名一个类的 PropertyEditor,并将其与它所支持的类放在同一个包中,以便可以自动找到它。
如果需要注册其他自定义 PropertyEditors(属性编辑器),有几种机制可供使用。最手动的方法是使用 ConfigurableBeanFactory 接口的registerCustomEditor() 方法,假设您有一个 BeanFactory 引用,这种方法通常不方便,也不推荐使用。另一种(稍微方便一点的)机制是使用一种特殊的 bean factory 后处理器,称为 CustomEditorConfigurer。虽然您可以将 BeanFactory 后处理器与 bean factory 实现一起使用,但 CustomEditorConfigurer 有一个嵌套的属性设置,因此我们强烈建议您将它与 ApplicationContext 一起使用,在 application context 中,您可以以与任何其他 bean 相似的方式部署它,并且可以自动检测和应用它。
注意,所有 bean 工厂和应用程序上下文都自动使用许多内置的属性编辑器,通过它们使用 BeanWrapper 来处理属性转换。前一节列出了 BeanWrapper 注册的标准属性编辑器。此外,ApplicationContexts 还覆盖或添加额外的编辑器,以适合特定应用程序上下文类型的方式处理资源查找。
标准 JavaBeans PropertyEditor 实例用于将表示为字符串的属性值转换为属性的实际复杂类型。您可以使用CustomEditorConfigurer(bean factory后处理器) 来方便地向 ApplicationContext 添加对附加 PropertyEditor 实例的支持。
考虑下面的示例,它定义了一个名为 ExoticType 的用户类和另一个名为 DependsOnExoticType 的类,后者需要将 ExoticType 设置为属性:
package example;
public class ExoticType {
private String name;
public ExoticType(String name) {
this.name = name;
}
}
public class DependsOnExoticType {
private ExoticType type;
public void setType(ExoticType type) {
this.type = type;
}
}
正确设置后,我们希望能够将 type 属性指定为字符串,PropertyEditor 将该字符串转换为实际的 ExoticType 实例。以下 bean definition (定义) 显示了如何设置这种关系:
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
bean>
PropertyEditor 实现可能如下所示:
// 将字符串表示形式转换为 ExoticType 对象
package example;
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
最后,下面的示例显示了如何使用 CustomEditorConfigurer 向 ApplicationContext 注册新的 PropertyEditor,这样就可以根据需要使用它了:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
map>
property>
bean>
使用 PropertyEditorRegistrar
向 Spring 容器注册属性编辑器的另一种机制是创建和使用 PropertyEditorRegistrar。当您需要在几种不同的情况下使用同一套属性编辑器时,这个接口特别有用。您可以编写相应的注册器,并在每种情况下重用它。PropertyEditorRegistrar 实例与名为 PropertyEditorRegistry 的接口协同工作,该接口由 Spring BeanWrapper (和 DataBinder )实现。PropertyEditorRegistrar 实例在与 CustomEditorConfigurer (在此【注册其他自定义 PropertyEditor 实现】描述)结合使用时特别方便,后者公开了一个名为 setPropertyEditorRegistrars(…) 。以这种方式添加到 CustomEditorConfigurer 的PropertyEditorRegistrar 实例可以很容易地与 DataBinder 和 Spring MVC controllers(控制器) 共享。此外,它避免了对定制编辑器进行同步的需要:PropertyEditorRegistrar 应该为每个 bean 创建尝试创建新的 PropertyEditor 实例。
以下示例显示了如何创建自己的 PropertyEditorRegistrar 实现:
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// 预计将创建新的 PropertyEditor 实例
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// 您可以根据需要注册任意数量的自定义属性编辑器...
}
}
另请参阅 org.springframework.beans.support.ResourceEditorRegistrar
以获取 PropertyEditorRegistrar 实现的示例。注意在 registerCustomEditors(…) 方法,它创建每个属性编辑器的新实例。
下一个示例显示了如何配置 CustomEditorConfigurer 并将 CustomPropertyEditorRegistrar 的实例注入其中:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
list>
property>
bean>
<bean id="customPropertyEditorRegistrar"
class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最后(对于那些使用 Spring 的 MVC web 框架的人来说,这有点偏离了本章的重点),将 PropertyEditorRegistrars 与数据绑定 Controllers(控制器)(如 SimpleFormController)结合使用会非常方便。以下示例在 initBinder(…) 方法:
public final class RegisterUserController extends SimpleFormController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// 注册用户的其他方法
}
PropertyEditor 注册的这种风格可以产生简洁的代码( initBinder(…) 只有一行长)并允许将公共 PropertyEditor 注册代码封装在一个类中,然后根据需要在尽可能多的 Controllers(控制器) 之间共享。
Spring 3 引入了一个 core.convert 包,它提供了一个通用的类型转换系统。系统定义了一个 SPI 来实现类型转换逻辑,并定义了一个 API 来在运行时执行类型转换。在 Spring 容器中,您可以使用该系统作为 PropertyEditor 实现的替代,将外部化的 bean 属性值字符串转换为所需的属性类型。您还可以在应用程序中需要类型转换的任何地方使用公共 API。
实现类型转换逻辑的 SPI 是简单且强类型的,如下面的接口定义所示:
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
若要创建自己的转换器,请实现 Converter(转换器) 接口,并将 S 参数化为要转换的类型,将 T 参数化为要转换的类型。如果需要将 S 的集合或数组转换为 T 的数组或集合,您也可以透明地应用这样的转换器,前提是还注册了委托数组或集合转换器(默认情况下是DefaultConversionService)。
对于每个 convert(S) 要转换的调用,保证源参数不为空。如果转换失败,您的 Converter(转换器) 可能会抛出任何未检查的异常。具体来说,它应该抛出一个 IllegalArgumentException 来报告无效的源值。注意确保您的 Converter(转换器) 实现是线程安全的。
为了方便起见,core.convert.support 包中提供了几个转换器实现。这些包括从字符串到数字和其他常见类型的转换器。下面的列表显示了 StringToInteger 类,这是一个典型的转换器实现:
package org.springframework.core.convert.support;
final class StringToInteger implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
当需要集中整个类层次结构的转换逻辑时(例如,当从 String(字符串)转换为 Enum(枚举)对象时),可以实现 ConverterFactory,如下例所示:
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
将 S 参数化为要转换的类型,将 R 参数化为定义可转换的类范围的基类型。然后实现 getConverter(Class
,其中 T 是 r 的子类。
以StringToEnumConverterFactory为例:
package org.springframework.core.convert.support;
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnumConverter(targetType);
}
private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
public T convert(String source) {
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
当需要复杂的 Converter(转换器) 实现时,可以考虑使用 GenericConverter 接口。与 Converter 相比,GenericConverter 具有更灵活但没有 Converter 那么强的类型签名,它支持多种源类型和目标类型之间的转换。此外,GenericConverter 提供了源和目标字段上下文,您可以在实现转换逻辑时使用这些上下文。这种上下文允许类型转换由字段注解或字段签名上声明的通用信息驱动。下面的清单显示了GenericConverter 的接口定义:
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
若要实现 GenericConverter,请让 getConvertibleTypes() 返回支持的源→目标类型对。然后实现 convert(Object,TypeDescriptor,TypeDescriptor) 来包含您的转换逻辑。source TypeDescriptor 提供对保存被转换值的源字段的访问。target (目标) TypeDescriptor 提供对要设置转换值的目标字段的访问。
GenericConverter 的一个好例子是在 Java 数组和集合之间进行转换的转换器。这样的 ArrayToCollectionConverter 会对声明目标集合类型的字段进行内部检查,以解析集合的元素类型。这使得在目标字段上设置集合之前,源数组中的每个元素都被转换为集合元素类型。
因为 GenericConverter 是一个更复杂的 SPI 接口,所以应该只在需要的时候使用它。支持 Converter 或 ConverterFactory 以满足基本类型转换需求。
有时,您希望转换器仅在特定条件成立时运行。例如,您可能希望仅当目标字段上存在特定的注解时才运行转换器,或者您可能希望仅当目标类上定义了特定的方法(如静态 valueOf 方法)时才运行转换器。ConditionalGenericConverter 是 GenericConverter 和 ConditionalConverter 接口的联合,它允许您定义这样的自定义匹配条件:
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
ConditionalGenericConverter 的一个很好的例子是 IdToEntityConverter,它在持久实体标识符和实体引用之间进行转换。这样的 IdToEntityConverter 只有在目标实体类型声明了静态查找方法(例如 findAccount(Long) )时才可能匹配。您可以在matches(TypeDescriptor,TypeDescriptor) 的实现中执行这样的 finder 方法检查。
ConversionService 定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常运行在以下 facade 接口之后:
package org.springframework.core.convert;
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
大多数 ConversionService 实现还实现 ConverterRegistry,它为注册转换器提供 SPI。在内部,ConversionService 实现委托其注册的转换器来执行类型转换逻辑。
core.convert.support 包中提供了一个健壮的 ConversionService 实现。GenericConversionService 是适用于大多数环境的通用实现。ConversionServiceFactory 为创建通用 ConversionService 配置提供了一个方便的工厂。
ConversionService 是一个无状态对象,设计用于在应用程序启动时进行实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常为每个 Spring 容器(或 ApplicationContext )配置一个 ConversionService 实例。Spring 选择 ConversionService,并在框架需要执行类型转换时使用它。您还可以将这个 ConversionService 注入到您的任何 beans 中,并直接调用它。
如果没有向 Spring 注册 ConversionService,则使用最初的基于 PropertyEditor 的系统。
要向 Spring 注册一个默认的 ConversionService,添加下面的 bean definition (定义),id 为 conversionService:
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的 ConversionService 可以在字符串、数字、枚举、集合、maps(映射) 和其他常见类型之间进行转换。若要用您自己的自定义转换器补充或重写默认转换器,请设置 converters 属性。属性值可以实现 Converter、ConverterFactory 或 GenericConverter 接口中的任何一个。
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
set>
property>
bean>
在 Spring MVC 应用程序中使用 ConversionService 也很常见。请参阅 Spring MVC 一章中的【转换和格式化】。
在某些情况下,您可能希望在转换过程中应用格式。有关使用 FormattingConversionServiceFactoryBean 的详细信息,请参见下一章【FormatterRegistry(格式注册表) SPI】。
要以编程方式使用 ConversionService 实例,可以像对任何其他 bean 一样注入对它的引用。以下示例显示了如何实现这一点:
@Service
public class MyService {
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
对于大多数用例,您可以使用指定 targetType 的 convert 方法,但它不适用于更复杂的类型,如参数化元素的集合。例如,如果您想以编程方式将整数列表转换为字符串列表,您需要提供源和目标类型的正式定义。
幸运的是,TypeDescriptor 提供了各种选项来简化这一过程,如下例所示:
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ...
cs.convert(input,
TypeDescriptor.forObject(input), // List 类型描述符
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
请注意,DefaultConversionService 会自动注册适用于大多数环境的转换器。这包括集合转换器、标量转换器和基本的对象到字符串转换器。通过对 DefaultConversionService 类使用静态 addDefaultConverters 方法,可以向任何 ConverterRegistry 注册相同的转换器。
值类型的转换器可在数组和集合中重用,因此无需创建特定的转换器来从集合 S 转换为集合 T,假设标准集合处理是适当的。
如前一节所述,core.convert 是一个通用类型转换系统。它提供了统一的 ConversionService API 和 Converter(强类型转换器)SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用这个系统来绑定 bean 属性值。此外,Spring Expression Language (SpEL) 和 DataBinder 都使用这个系统来绑定字段值。例如,当 SpEL 需要将一个 Short(短整型)强制转换为 Long(长整型)以完成expression.setValue(Object bean,Object value) 尝试时,core.convert 系统会执行强制转换。
现在考虑典型客户机环境(如 web 或桌面应用程序)的类型转换要求。在这种环境中,通常从字符串转换为支持客户端回发过程,以及从字符串转换为支持视图渲染过程。此外,通常需要本地化字符串值。更一般的核心。convert Converter SPI 不能直接满足此类格式要求。为了直接解决这些问题,Spring 3 引入了一个方便的 Formatter(格式化程序)SPI,它为客户端环境的 PropertyEditor 实现提供了一个简单而健壮的替代方案。
通常,当需要实现通用类型转换逻辑时,可以使用 Converter(转换器)SPI,例如,在 java.util.Date 和 Long之间进行转换。当您在客户端环境(如 web 应用程序)中工作并需要解析和打印本地化字段值时,可以使用 Formatter(格式化程序)SPI。ConversionService 为这两种 SPI 提供了统一的类型转换 API。
实现字段格式化逻辑的 Formatter(格式化程序)SPI 很简单,并且是强类型的。下面的列表显示了 Formatter(格式化程序)接口定义:
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter(格式化程序)从 Printer(打印)和 Parser(解析器)构造块接口扩展而来。下面的清单显示了这两个接口的定义:
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
import java.text.ParseException;
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
要创建自己的格式化程序,请实现前面显示的格式化程序接口。将 T 参数化为您希望格式化的对象类型,例如 java.util.Date。实现 print()操作来打印 T 的一个实例,以便在客户端语言环境中显示。实现 parse() 操作,从客户端区域设置返回的格式化表示中解析 T 的实例。如果解析尝试失败,您的 Formatter(格式化程序)应引发ParseException 或 IllegalArgumentException。注意确保您的 Formatter(格式化程序)实现是线程安全的。
为了方便起见,format 子包提供了几个 Formatter(格式化程序)实现。number(数字包)提供了 NumberStyleFormatter、CurrencyStyleFormatter 和 PercentStyleFormatter 来格式化使用 java.text.NumberFormat 的 Number(数字)对象。datetime 包提供了一个 DateFormatter,用于使用 java.text.DateFormat 格式化 java.util.Date 对象。datetime.joda 包基于 Joda-Time 库 提供了全面的datetime 格式支持。
下面的 DateFormatter 是 Formatter(格式化程序)实现的一个示例:
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
Spring 团队欢迎社区驱动的 Formatter(格式化程序) 贡献。见 GitHub Issues 投稿。
可以通过字段类型或注解来配置字段格式。若要将注解绑定到 Formatter(格式化程序),请实现 AnnotationFormatterFactory。以下清单显示了 AnnotationFormatterFactory 接口的定义:
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
创建实现:将 A 参数化为您希望与格式化逻辑相关联的字段 annotationType,例如 org.springframework.format.annotation.DateTimeFormat
。让 getFieldTypes() 返回可以使用注解的字段类型。让 getPrinter() 返回一个Printer(打印机) 来打印注解字段的值。让 getParser() 返回一个 Parser(解析器)来解析带注解字段的 clientValue。
以下示例 AnnotationFormatterFactory 实现将 @NumberFormat 注解绑定到格式化程序,以允许指定数字样式或模式:
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class,
Double.class, BigDecimal.class, BigInteger.class }));
}
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberStyleFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentStyleFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyStyleFormatter();
} else {
return new NumberStyleFormatter();
}
}
}
}
若要触发格式化,可以用 @NumberFormat 对字段进行注解,如下例所示:
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
org.springframework.format.annotation
包中有一个可移植的格式注解 API。您可以使用 @NumberFormat 格式化数字字段,如 Double 和 Long,使用 @DateTimeFormat 格式化 java.util.Date、java.util.Calendar、Long (毫秒时间戳)以及 JSR-310 java.time 和 Joda-Time 值类型。
以下示例使用 @DateTimeFormat 将 java.util.Date 格式化为 ISO 日期(yyyy-MM-dd):
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
FormatterRegistry 是用于注册格式化程序和转换器的 SPI。FormattingConversionService 是适用于大多数环境的 FormatterRegistry 的实现。您可以通过编程或声明的方式将此变体配置为 Spring bean,例如通过使用 FormattingConversionServiceFactoryBean。因为这个实现还实现了 ConversionService,所以您可以直接将其配置为与 Spring 的 DataBinder 和 Spring Expression Language (SpEL) 一起使用。
以下清单显示了 FormatterRegistry SPI:
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Formatter<?> formatter);
void addFormatterForAnnotation(AnnotationFormatterFactory<?> factory);
}
如前面的清单所示,您可以通过字段类型或注解来注册格式化程序。
FormatterRegistry SPI 允许您集中配置格式化规则,而不是在您的控制器之间复制这样的配置。例如,您可能希望强制所有日期字段都以某种方式设置格式,或者带有特定注解的字段以某种方式设置格式。使用共享的 FormatterRegistry,您只需定义一次这些规则,就可以在需要格式化时应用它们。
FormatterRegistrar 是一个SPI,用于通过 FormatterRegistry 注册格式化程序和转换器。下面的清单显示了它的接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
为给定的格式类别(如日期格式)注册多个相关的转换器和格式化程序时,FormatterRegistrar 非常有用。在声明性注册不充分的情况下,它也很有用——例如,当格式化程序需要在不同于它自己的
的特定字段类型下进行索引时,或者当注册 Printer(打印机)/Parser(解析器)对时。下一节提供了有关转换器和格式化程序注册的更多信息。
请参阅 Spring MVC 一章中的【转换和格式化】。
默认情况下,未用 @DateTimeFormat 注解的日期和时间字段是使用 DateFormat.SHORT 从字符串转换而来的。如果您愿意,可以通过定义自己的全局格式来更改这一点。
为此,确保 Spring 不注册默认格式化程序。相反,请借助以下工具手动注册格式化程序:
org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
org.springframework.format.datetime.DateFormatterRegistrar
, or org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
for Joda-Time.例如,以下 Java 配置注册了一个全局 yyyyMMdd 格式:
@Configuration
public class AppConfig {
@Bean
public FormattingConversionService conversionService() {
// 使用 DefaultFormattingConversionService,但不注册默认值
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
// 请确保仍支持 @NumberFormat
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// 用特定的全球格式注册 JSR-310 日期转换
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
registrar.registerFormatters(conversionService);
// 用特定的全局格式注册日期转换
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);
return conversionService;
}
}
如果您喜欢基于 XML 的配置,可以使用 FormattingConversionServiceFactoryBean。以下示例显示了如何实现这一点(这次使用 Joda Time):
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd>
" conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="registerDefaultFormatters" value="false" />
<property name="formatters">
<set>
<bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
set>
property>
<property name="formatterRegistrars">
<set>
<bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
bean>
property>
bean>
set>
property>
bean>
beans>
注意在 web 应用程序中配置日期和时间格式时,有一些额外的注意事项。请参见后续 【WebMVC转换和格式化】或 【WebFlux转换和格式化】。
Spring 框架提供了对 Java Bean 验证 API 的支持。
Bean Validation (验证) 通过约束声明和元数据为 Java 应用程序提供了一种通用的验证方式。要使用它,您可以用声明性验证约束来注解domain (域)模型属性,然后由运行时强制执行这些约束。有内置的约束,您也可以定义自己的自定义约束。
考虑下面的例子,它展示了一个简单的 PersonForm 模型,该模型有两个属性:
public class PersonForm {
private String name;
private int age;
}
Bean Validation (验证) 允许您声明约束,如下例所示:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
然后,Bean 验证器根据声明的约束验证该类的实例。有关 API 的一般信息,请参见 Bean 验证。有关特定的约束,请参见 Hibernate Validator 文档。要了解如何将 bean 验证提供者设置为 Spring bean,请继续阅读。
Spring 提供了对 Bean 验证 API 的全面支持,包括将 Bean 验证提供者引导为 Spring bean。这允许您在应用程序中需要验证的任何地方注入 javax.validation.ValidatorFactory
或 javax.validation.Validator
。
可以使用 LocalValidatorFactoryBean 将默认验证器配置为 Spring bean,如下例所示:
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class AppConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
前面示例中的基本配置通过使用其默认引导机制来触发 bean 验证初始化。Bean 验证提供程序(如 Hibernate 验证程序)应该出现在类路径中,并且会被自动检测到。
LocalValidatorFactoryBean 实现了 javax.validation.ValidatorFactory
和 javax.validation.Validator
,以及 Spring 的 org.springframework.validation.Validator
。您可以将对这些接口的引用注入到需要调用验证逻辑的 Bean 中。
如果您喜欢直接使用 Bean 验证 API,可以注入对 javax.validation.Validator 的引用,如下例所示:
import javax.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}
如果您的 bean 需要 Spring 验证 API,您可以注入对 org.springframework.validation.Validator
的引用,如下例所示:
import org.springframework.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}
每个 bean validation(验证)约束由两部分组成:
javax.validation.ConstraintValidator
接口的实现,用于实现约束的行为。为了将声明与实现相关联,每个 @Constraint 注解引用一个相应的 ConstraintValidator 实现类。在运行时,当在您的 domain(域)模型中遇到约束注解时,ConstraintValidatorFactory 实例化引用的实现。
默认情况下,LocalValidatorFactoryBean 配置一个 SpringConstraintValidatorFactory,它使用 Spring 创建 ConstraintValidator 实例。这让您的定制 ConstraintValidators 像任何其他 Spring bean 一样受益于依赖注入。
以下示例显示了一个自定义 @Constraint 声明,后面是一个关联的 ConstraintValidator 实现,该实现使用 Spring 进行依赖注入:
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
@Autowired;
private Foo aDependency;
// ...
}
如前面的示例所示,ConstraintValidator 实现可以像任何其他 Spring bean 一样拥有其依赖项 @Autowired。
您可以通过 MethodValidationPostProcessor Bean 定义将 Bean Validation 1.1 支持的方法验证特性(作为自定义扩展,Hibernate Validator 4.3 也支持)集成到 Spring 上下文中:
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
@Configuration
public class AppConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
为了符合 Spring 驱动的方法验证的条件,所有的目标类都需要用 Spring 的 @Validated 注解进行注解,这也可以选择声明要使用的验证组。有关 Hibernate Validator 和 Bean Validation 1.1 提供程序的设置细节,请参见 MethodValidationPostProcessor
。
方法验证依赖于目标类周围的 AOP 代理,要么是接口上方法的 JDK 动态代理,要么是 CGLIB 代理。代理的使用有一定的限制,其中一些在【理解 AOP 代理】中有描述。此外,记住总是在代理类上使用方法和访问器;直接进入现场是行不通的。
默认的 LocalValidatorFactoryBean 配置足以满足大多数情况。从消息插值到遍历解析,各种 Bean 验证构造都有许多配置选项。有关这些选项的更多信息,请参见 LocalValidatorFactoryBean
javadoc。
从 Spring 3 开始,可以用 Validator(验证器) 配置 DataBinder(数据绑定器) 实例。配置完成后,您可以通过调用 binder.validate() 来调用 Validator(验证器)。任何验证错误都会自动添加到绑定器的 BindingResult 中。
下面的示例演示如何以编程方式使用 DataBinder 在绑定到目标对象后调用验证逻辑:
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
// 绑定到目标对象
binder.bind(propertyValues);
// 验证目标对象
binder.validate();
// 获取包含任何验证错误的 BindingResult
BindingResult results = binder.getBindingResult();
您还可以通过 dataBinder.addValidators 和 dataBinder.replaceValidators 配置具有多个 Validator(验证器) 实例的 dataBinder。这在将全局配置的 bean 验证与在 DataBinder 实例上本地配置的 Spring 验证器相结合时非常有用。请参见【Spring MVC验证配置】。
请参阅 Spring MVC 一章中的验证。