Spring进阶 - 验证、数据绑定和类型转换

书接上文

接下来到 Spring framework core 的第三大块 —— 验证、数据绑定和类型转换

验证、数据绑定和类型转换

考虑将验证作为业务逻辑有利有弊,Spring 提供了一种验证(和数据绑定)设计。具体来说,验证不应与 Web 层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个 Validator 合同,该合同既基本又可以在应用程序的每个层中使用。

数据绑定对于使用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring 提供了恰当地命名为 DataBinder 的功能。 ValidatorDataBinder 组成了验证包,该验证包主要用于但不限于 Web 层。

BeanWrapper 是 Spring 框架中的基本概念,并在很多地方使用。但是,您可能不需要直接使用 BeanWrapper。但是,因为这是参考文档,所以我们认为可能需要进行一些解释。我们将在本章中解释 BeanWrapper,因为如果您要使用它,那么在尝试将数据绑定到对象时最有可能使用它。

Spring 的 DataBinder 和较低级别的 BeanWrapper 都使用 PropertyEditorSupport 实现来解析和格式化属性值。 PropertyEditorPropertyEditorSupport 类型是 JavaBeans 规范的一部分,本章还将对此进行说明。 Spring 3 引入了 core.convert 包,该包提供了常规的类型转换工具,以及用于格式化 UI 字段值的高级“format”包。您可以将这些包用作 PropertyEditorSupport 实现的更简单替代方案。本章还将对它们进行讨论。

Spring 通过设置基础结构和 Spring 自己的 Validator 合同的适配器来支持 Java Bean 验证。应用程序可以全局启用一次 Bean 验证,如 Java Bean 验证中所述,并将其专用于所有验证需求。在 Web 层中,应用程序可以每个 DataBinder 进一步注册控制器本地的 Spring Validator 实例,如配置 DataBinder 中所述,这对于插入自定义验证逻辑很有用。

使用 Spring 的 Validator 接口进行验证

Spring 具有 Validator 接口,可用于验证对象。 Validator 接口通过使用 Errors 对象来工作,以便验证器在验证时可以将验证失败报告给 Errors 对象。

1
2
3
4
5
6
7
public class Person {

private String name;
private int age;

// the usual getters and setters...
}

下一个示例通过实现 org.springframework.validation.Validator 接口的以下两个方法来提供 Person 类的验证行为:

  • supports(Class):此验证程序可以验证提供的 Class 的实例吗?
  • validate(Object, org.springframework.validation.Errors):验证给定的对象,并在发生验证错误的情况下,向给定的 Errors 对象注册这些对象。

实施 Validator 非常简单,尤其是当您知道 Spring Framework 也提供的 ValidationUtils 帮助器类时。 以下示例实现了用于 Person 实例的 Validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PersonValidator implements Validator {

/**
* This Validator validates only Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}

public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}

ValidationUtils 类上的静态 rejectIfEmpty(..)方法用于拒绝 name 属性(如果该属性为 null 或为空字符串)。查看 ValidationUtils javadoc,看看它除了提供前面显示的示例外还提供什么功能。

虽然可以实现单个 Validator 类来验证丰富对象中的每个嵌套对象,但最好在其自己的 Validator 实现中封装对象的每个嵌套类的验证逻辑。一个“丰富”对象的简单示例是一个 Customer,它由两个 String 属性(第一个和第二个名称)和一个复杂的 Address 对象组成。地址对象可以独立于客户对象使用,因此已实现了不同的 AddressValidator。如果希望 CustomerValidator 重用 AddressValidator 类中包含的逻辑而不求助于复制和粘贴,则可以在 CustomerValidator 中依赖注入或实例化一个 AddressValidator,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CustomerValidator implements Validator {

private final Validator addressValidator;

public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}

/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}

public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}

验证错误将报告给传递给验证器的 Errors 对象。 对于 Spring Web MVC,可以使用 <spring:bind/> 标记检查错误消息,但是也可以自己检查 Errors 对象。关于它提供的方法的更多信息可以在 javadoc 中找到。

将代码解析为错误消息

我们介绍了数据绑定和验证。本节介绍与验证错误相对应的输出消息。在上一节显示的示例中,我们拒绝了名称和年龄字段。如果要使用 MessageSource 输出错误消息,可以使用拒绝字段时提供的错误代码(在这种情况下为“名称”和“年龄”)来进行输出。当您从 Errors 接口调用(直接或间接通过使用诸如 ValidationUtils 类的直接或间接)rejectValue 或其他拒绝方法之一时,基础实现不仅注册您传入的代码,还注册许多其他错误代码。MessageCodesResolver 确定 Errors 接口寄存器中的哪个错误代码。默认情况下,使用 DefaultMessageCodesResolver,它(例如)不仅使用您提供的代码注册消息,而且还注册包含传递给拒绝方法的字段名称的消息。因此,如果您通过使用 rejectValue("age", "too.darn.old") 拒绝字段,除了 too.darn.old 代码外,Spring 还会注册 too.darn.old.agetoo.darn.old.age.int(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。

有关 MessageCodesResolver 和默认策略的更多信息,可以分别在 MessageCodesResolverDefaultMessageCodesResolver 的 javadoc 中找到。

Bean 操作和 BeanWrapper

org.springframework.beans 包遵循 JavaBeans 标准。 JavaBean 是具有默认无参数构造函数的类,并且遵循命名约定,在该命名约定下,例如,名为 bingoMadness 的属性将具有 setter 方法 setBingoMadness(..) 和 getter 方法 getBingoMadness()。有关 JavaBean 和规范的更多信息,请参见 javabeans

Bean 包中的一个非常重要的类是 BeanWrapper 接口及其相应的实现(BeanWrapperImpl)。就像从 Javadoc 引用的那样,BeanWrapper 提供了以下功能:设置和获取属性值(单独或批量),获取属性描述符以及查询属性以确定它们是否可读或可写。此外,BeanWrapper 还支持嵌套属性,从而可以将子属性上的属性设置为无限深度。 BeanWrapper 还支持添加标准 JavaBeans PropertyChangeListenersVetoableChangeListeners 的功能,而无需在目标类中支持代码。最后但并非最不重要的一点是,BeanWrapper 支持设置索引属性。 BeanWrapper 通常不直接由应用程序代码使用,而是由 DataBinderBeanFactory 使用。

BeanWrapper 的工作方式部分由其名称表示:它包装一个 Bean,以对该 Bean 执行操作,例如设置和检索属性。

设置和获取基本和嵌套属性

设置和获取属性是通过 BeanWrappersetPropertyValuegetPropertyValue 重载方法变体完成的。有关详细信息,请参见其 Javadoc。下表显示了这些约定的一些示例:

表达式 说明
name 表示与 getName()isName()setName(..) 方法相对应的 name 属性 。
account.name 表示与 getAccount().setName()getAccount().getName() 方法相对应的 account 属性的 name 嵌套属性。
account[2] 表示索引属性的第三个元素 account。索引属性可能是的 arraylist 或其它天然有序集合。
account[COMPANYNAME] 表示account 这个 MapCOMPANYNAME 键索引的条目的值。

(如果您不打算直接使用 BeanWrapper,那么下一部分对您而言并不是至关重要的。如果仅使用 DataBinderBeanFactory 及其默认实现,则应跳到 PropertyEditors 的部分。)

以下两个示例类使用 BeanWrapper 来获取和设置属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}
}

以下代码段显示了一些有关如何检索和操纵实例化的 CompaniesEmployees 的某些属性的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

内置的 PropertyEditor 实现

pring 使用 PropertyEditor 的概念来实现对象和字符串之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,日期可以用人类可读的方式表示(如字符串:”2007-14-09”),而我们仍然可以将人类可读的形式转换回原始日期(或者更好的是,转换任何日期以人类可读的形式输入到 Date 对象)。通过注册类型为 java.beans.PropertyEditor 的自定义编辑器,可以实现此行为。在 BeanWrapper 上或在特定的 IoC 容器中注册自定义编辑器(如上一章所述),使它具有如何将属性转换为所需类型的知识。有关 PropertyEditor 的更多信息,请参见 Oracle 的 java.beans 包的 javadoc

在 Spring 中使用属性编辑的两个示例:

  • 通过使用 PropertyEditor 实现在 bean 上设置属性。当使用 String 作为在 XML 文件中声明的某个 bean 的属性的值时,Spring(如果相应属性的设置器具有 Class 参数)将使用 ClassEditor 尝试将参数解析为 Class 对象。
  • 在 Spring 的 MVC 框架中,通过使用各种 PropertyEditor 实现来解析 HTTP 请求参数,您可以在 CommandController 的所有子类中手动绑定这些实现。

Spring 具有许多内置的 PropertyEditor 实现,以简化生活。它们都位于org.springframework.beans.propertyeditors 包中。默认情况下,大多数(但不是全部,如下表所示)由 BeanWrapperImpl 注册。如果可以通过某种方式配置属性编辑器,则仍可以注册自己的变体以覆盖默认变体。下表描述了 Spring 提供的各种 PropertyEditor 实现:

说明
ByteArrayPropertyEditor 字节数组的编辑器。将字符串转换为其相应的字节表示形式。默认情况下由 BeanWrapperImpl 注册。
ClassEditor 将代表类的字符串解析为实际类,反之亦然。当找不到类时,将抛出 IllegalArgumentException。默认情况下,由 BeanWrapperImpl 注册。
CustomBooleanEditor 布尔属性的可定制属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor 集合的属性编辑器,可将任何源 Collection 转换为给定的目标 Collection 类型。
CustomDateEditor java.util.Date 的可自定义属性编辑器,支持自定义 DateFormat。默认未注册。必须根据需要以适当的格式进行用户注册。
CustomNumberEditor 任何 Number 子类(例如 IntegerLongFloatDouble)的可自定义属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor 将字符串解析为 java.io.File 对象。默认情况下,由 BeanWrapperImpl 注册。
InputStreamEditor 单向属性编辑器,它可以采用字符串并生成(通过中间的 ResourceEditorResource)一个 InputStream,以便可以将 InputStream 属性直接设置为字符串。请注意,默认用法不会为您关闭 InputStream。默认情况下,由 BeanWrapperImpl 注册。
LocaleEditor 可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 _[country]_[variant],与 Locale 的 toString() 方法相同)。默认情况下,由 BeanWrapperImpl 注册。
PatternEditor 可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然。
PropertiesEditor 可以将字符串(以 java.util.Properties 类的 javadoc 中定义的格式格式化)转换为 Properties 对象。默认情况下,由 BeanWrapperImpl 注册。
StringTrimmerEditor 修剪字符串的属性编辑器。(可选)允许将空字符串转换为空值。默认情况下未注册——必须是用户注册的。
URLEditor 可以将 URL 的字符串表示形式解析为实际的 URL 对象。默认情况下,由 BeanWrapperImpl 注册

Spring 使用 java.beans.PropertyEditorManager 设置可能需要的属性编辑器的搜索路径。搜索路径还包括 sun.bean.editors,其中包括针对诸如 FontColor 和大多数基本类型的类型的 PropertyEditor 实现。 还要注意,如果标准 JavaBeans 基础结构与它们处理的类在同一包中,并且与该类具有相同的名称,并附加了 Editor,则标准 JavaBeans 基础结构将自动发现 PropertyEditor 类(无需显式注册它们)。例如,可能具有以下类和包结构,足以使 SomethingEditor 类被识别并用作 Something 类型的属性的 PropertyEditor

1
2
3
4
5
com
chank
pop
Something
SomethingEditor // the PropertyEditor for the Something class

注意,您也可以在此处使用标准的 BeanInfo JavaBeans 机制(在某种程度上进行了描述)。 以下示例使用 BeanInfo 机制使用关联类的属性显式注册一个或多个 PropertyEditor 实例:

1
2
3
4
5
com
chank
pop
Something
SomethingBeanInfo // the BeanInfo for the Something class

所引用的 SomethingBeanInfo 类的以下 Java 源代码将 CustomNumberEditorSomething 类的 age 属性相关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
}
}
}

注册其他自定义 PropertyEditor 实现

当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor 实现将这些字符串转换为属性的复杂类型。Spring 预注册了许多自定义的 PropertyEditor 实现(例如,将表示为字符串的类名称转换为 Class 对象)。此外,Java 的标准 JavaBeans PropertyEditor 查找机制允许适当地命名类的 PropertyEditor,并将其与提供支持的类放在同一包中,以便可以自动找到它。

如果需要注册其他自定义 PropertyEditor,则可以使用几种机制。最手动的方法(通常不方便或不建议使用)是使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法,并假设您有 BeanFactory 引用。另一种(稍微方便些)的机制是使用一种称为 CustomEditorConfigurer 的特殊 bean 工厂后处理器。尽管您可以将 Bean 工厂后处理器与 BeanFactory 实现一起使用,但 CustomEditorConfigurer 具有嵌套的属性设置,因此我们强烈建议您将其与 ApplicationContext 一起使用,在这里可以将其以与其他任何 Bean 相似的方式进行部署,并且可以在任何位置进行部署。自动检测并应用。

请注意,所有的 bean 工厂和应用程序上下文通过使用 BeanWrapper 来处理属性转换,都会自动使用许多内置的属性编辑器。上一节列出了 BeanWrapper 注册的标准属性编辑器。此外,ApplicationContext 还以适合特定应用程序上下文类型的方式重写或添加其他编辑器,以处理资源查找。

标准 JavaBeans PropertyEditor 实例用于将以字符串表示的属性值转换为该属性的实际复杂类型。您可以使用 bean 工厂的后处理器 CustomEditorConfigurer 来方便地将对其他 PropertyEditor 实例的支持添加到 ApplicationContext

考虑以下示例,该示例定义了一个名为 ExoticType 的用户类和另一个名为 DependsOnExoticType 的类,该类需要将 ExoticType 设置为属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 定义显示了如何建立这种关系:

1
2
3
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor 实现可能类似于以下内容:

1
2
3
4
5
6
7
8
9
// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}

Spring 类型转换

Spring 3 引入了 core.convert 包,该包提供了通用的类型转换系统。系统定义了一个用于实现类型转换逻辑的 SPI 和一个用于在运行时执行类型转换的 API。在 Spring 容器中,可以使用此系统作为 PropertyEditor 实现的替代方法,以将外部化的 bean 属性值字符串转换为所需的属性类型。 您还可以在应用程序中需要类型转换的任何地方使用公共 API。

转换器 SPI

如以下接口定义所示,用于实现类型转换逻辑的 SPI 非常简单且具有强类型:

1
2
3
4
5
6
package org.springframework.core.convert.converter;

public interface Converter<S, T> {

T convert(S source);
}

要创建自己的转换器,请实现 Converter 接口并将 S 设置为要转换的类型,并将 T 设置为要转换的类型。如果还需要注册一个委托数组或集合转换器(默认情况下 DefaultConversionService 会这样做),则也可以透明地应用此类转换器,如果需要将 S 的集合或数组转换为 T 的数组或集合。

对于每次对 convert(S)的调用,保证源参数不为 null。如果转换失败,您的转换器可能会引发任何未经检查的异常。具体来说,它应该抛出 IllegalArgumentException 以报告无效的源值。注意确保您的 Converter 实现是线程安全的。

为了方便起见,在 core.convert.support 软件包中提供了几种转换器实现。这些包括从字符串到数字和其他常见类型的转换器。下面的清单显示了 StringToInteger 类,它是一个典型的 Converter 实现:

1
2
3
4
5
6
7
8
package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

public Integer convert(String source) {
return Integer.valueOf(source);
}
}

使用 ConverterFactory

当需要集中整个类层次结构的转换逻辑时(例如,从 String 转换为 Enum 对象时),可以实现 ConverterFactory,如以下示例所示:

1
2
3
4
5
6
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 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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());
}
}
}

使用 GenericConverter

当您需要复杂的 Converter 实现时,请考虑使用 GenericConverter 接口。 与 Converter 相比,GenericConverter 具有比 Converter 更灵活但强度不高的签名,支持在多种源类型和目标类型之间进行转换。此外,GenericConverter 使您可以在实现转换逻辑时使用可用的源字段和目标字段上下文。 这种上下文允许类型转换由字段注释或在字段签名上声明的通用信息驱动。 以下清单显示了 GenericConverter 的接口定义:

1
2
3
4
5
6
7
8
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) 包含您的转换逻辑。 源 TypeDescriptor 提供对包含正在转换的值的源字段的访问。 使用目标 TypeDescriptor,可以访问要设置转换值的目标字段。

GenericConverter 的一个很好的例子是在 Java 数组和集合之间进行转换的转换器。 这样的 ArrayToCollectionConverter 会对声明目标集合类型的字段进行内省,以解析集合的元素类型。 这样就可以在将集合设置到目标字段上之前,将源数组中的每个元素转换为集合元素类型。

由于 GenericConverter 是一个更复杂的 SPI 接口,因此仅应在需要时使用它。 支持 ConverterConverterFactory 以满足基本的类型转换需求。

使用 ConditionalGenericConverter

有时,您希望 Converter 仅在满足特定条件时才运行。 例如,您可能只想在目标字段上存在特定注释时才运行 Converter,或者可能仅在目标类上定义了特定方法(例如静态 valueOf 方法)时才运行 Converter。 ConditionalGenericConverter 是 GenericConverter 和 ConditionalConverter 接口的联合,可让您定义以下自定义匹配条件:

1
2
3
4
5
6
7
public interface ConditionalConverter {

boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter 的一个很好的例子是 EntityConverter,它在持久实体标识符和实体引用之间进行转换。仅当目标实体类型声明静态查找器方法(例如 findAccount(Long))时,此类 EntityConverter 才可能匹配。 您可以在 matchs(TypeDescriptor,TypeDescriptor) 的实现中执行这种 finder 方法检查。

ConversionService API

ConversionService 定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常在以下外观接口后面执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

ConversionService 是无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常为每个 Spring 容器(或 ApplicationContext)配置一个 ConversionService 实例。当框架需要执行类型转换时,Spring 会调用该 ConversionService 并使用它。 您还可以将此 ConversionService 注入到任何 bean 中,然后直接调用它。

如果未向 Spring 注册任何 ConversionService,则使用原始的基于 PropertyEditor 的系统。

要向 Spring 注册默认的 ConversionService,请添加以下 bean 定义,其 id 为 conversionService

1
2
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认的 ConversionService 可以在字符串,数字,枚举,集合,映射和其他常见类型之间进行转换。 要用您自己的自定义转换器补充或覆盖默认转换器,请设置 converters 属性。 属性值可以实现 ConverterConverterFactoryGenericConverter 接口中的任何一个。

1
2
3
4
5
6
7
8
<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 编码

要以编程方式使用 ConversionService 实例,可以像对其他任何 bean 一样注入对该实例的引用。 以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyService {

public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}

public void doIt() {
this.conversionService.convert(...)
}
}

对于大多数用例,可以使用指定 targetType 的 convert 方法,但不适用于更复杂的类型,例如参数化元素的集合。 例如,如果要以编程方式将整数列表转换为字符串列表,则需要提供源类型和目标类型的正式定义。

幸运的是,如下面的示例所示,TypeDescriptor 提供了各种选项来使操作变得简单明了:

1
2
3
4
5
6
DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ...
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

请注意,DefaultConversionService 自动注册适用于大多数环境的转换器。 这包括集合转换器,标量转换器和基本的对象到字符串转换器。 您可以使用 DefaultConversionService 类上的静态 addDefaultConverters 方法向任何 ConverterRegistry 注册相同的转换器。

值类型的转换器可重用于数组和集合,因此,假设标准的集合处理适当,则无需创建特定的转换器即可将 S 的集合转换为 T 的集合。

Spring 字段格式

如上一节所述,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 或桌面应用程序)的类型转换要求。在这样的环境中,您通常会从 String 转换为支持客户端回发过程,然后又转换为 String 以支持视图渲染过程。另外,您通常需要本地化 String 值。更通用的 core.convert Converter SPI 不能直接满足此类格式化要求。为了直接解决这些问题,Spring 3 引入了方便的 Formatter SPI,它为客户端环境提供了 PropertyEditor 实现的简单而强大的替代方案。

通常,当您需要实现通用类型转换逻辑时(例如,用于在 java.util.DateLong 之间进行转换),可以使用 Converter SPI。在客户端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,可以使用 Formatter SPI。 ConversionService 为两个 SPI 提供统一的类型转换 API。

Formatter SPI

用于实现字段格式化逻辑的 Formatter SPI 非常简单且类型严格。 以下清单显示了 Formatter 接口定义:

1
2
3
4
package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

FormatterPrinterParser 构建块接口扩展。 以下清单显示了这两个接口的定义:

1
2
3
4
public interface Printer<T> {

String print(T fieldValue, Locale locale);
}
1
2
3
4
5
6
import java.text.ParseException;

public interface Parser<T> {

T parse(String clientValue, Locale locale) throws ParseException;
}

要创建自己的 Formatter,请实现前面显示的 Formatter 接口。将 T 参数化为您希望格式化的对象的类型(例如 java.util.Date)。实现 print() 操作以打印 T 的实例以在客户端语言环境中显示。实现 parse() 操作,以从客户端语言环境返回的格式化表示形式解析 T 的实例。如果解析尝试失败,则 Formatter 应该抛出 ParseExceptionIllegalArgumentException。注意确保您的 Formatter 实现是线程安全的。

format 子包为方便起见提供了几种 Formatter 实现。数字程序包提供 NumberStyleFormatter``,CurrencyStyleFormatterPercentStyleFormatter 来格式化使用 java.text.NumberFormatNumber 对象。datetime 包提供了一个 DateFormatter,用于使用 java.text.DateFormat 格式化 java.util.Date 对象。datetime.joda 包基于 Joda-Time 库提供了全面的日期时间格式支持。

以下 DateFormatterFormatter 实现的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 问题以做出贡献。

注释驱动的格式

可以通过字段类型或注释配置字段格式。要将注释绑定到 Formatter,请实现 AnnotationFormatterFactory。以下显示了AnnotationFormatterFactory 接口的定义:

1
2
3
4
5
6
7
8
9
10
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 参数化为要与格式逻辑关联的字段注解类型,例如 org.springframework.format.annotation.DateTimeFormat。让 getFieldTypes() 返回可在其上使用注释的字段类型。让 getPrinter() 返回 Printer 以打印带注释的字段的值。让 getParser() 返回解析器以解析带注释字段的 clientValue

以下示例 AnnotationFormatterFactory 实现将 @NumberFormat 批注绑定到格式化程序,以指定数字样式或模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 注释字段,如以下示例所示:

1
2
3
4
5
public class MyModel {

@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}

格式注释 API

org.springframework.format.annotation 包中存在一个可移植的格式注释 API。 您可以使用 @NumberFormat 格式化数字字段(例如 Double 和 Long),并使用 @DateTimeFormat 格式化 java.util.Datejava.util.CalendarLong(用于毫秒时间戳)以及 JSR-310 java.time 和 Joda-Time 值类型。

以下示例使用 @DateTimeFormatjava.util.Date 格式化为 ISO 日期(yyyy-MM-dd):

1
2
3
4
5
public class MyModel {

@DateTimeFormat(iso=ISO.DATE)
private Date date;
}

FormatterRegistry SPI

FormatterRegistry 是用于注册格式器和转换器的 SPI。FormattingConversionService 是适用于大多数环境的 FormatterRegistry 的实现。 您可以通过编程方式或声明方式将此变体配置为 Spring Bean,例如通过使用 FormattingConversionServiceFactoryBean。 由于此实现还实现了 ConversionService,因此您可以直接将其配置为与 Spring 的 DataBinder 和 Spring 表达式语言(SpEL)一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
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

FormatterRegistrar 是一个 SPI,用于通过 FormatterRegistry 注册格式器和转换器。 以下清单显示了其接口定义:

1
2
3
4
5
6
package org.springframework.format;

public interface FormatterRegistrar {

void registerFormatters(FormatterRegistry registry);
}

为给定的格式类别(例如日期格式)注册多个相关的转换器和格式器时,FormatterRegistrar 很有用。 在声明式注册不足的情况下(例如,当格式化程序需要在不同于其自身 <T> 的特定字段类型下建立索引或注册 Printer/Parser 对时),它也很有用。 下一节将提供有关转换器和格式化程序注册的更多信息。

在 Spring MVC 中配置格式

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-config-conversion

配置全局日期和时间格式

默认情况下,未使用 @DateTimeFormat 注释的日期和时间字段是使用 DateFormat.SHORT 样式从字符串转换的。 如果愿意,可以通过定义自己的全局格式来更改此设置。

为此,请确保 Spring 不注册默认格式器。 相反,可以借助以下方法手动注册格式化程序:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
  • org.springframework.format.datetime.DateFormatterRegistrar 或 joda-Time 的 org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar

例如,以下 Java 配置注册全局 yyyyMMdd 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class AppConfig {

@Bean
public FormattingConversionService conversionService() {

// Use the DefaultFormattingConversionService but do not register defaults
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

// Ensure @NumberFormat is still supported
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

// Register JSR-310 date conversion with a specific global format
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
registrar.registerFormatters(conversionService);

// Register date conversion with a specific global format
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);

return conversionService;
}
}

如果您喜欢基于 XML 的配置,则可以使用 FormattingConversionServiceFactoryBean。 以下示例显示了如何执行此操作(这次使用 Joda Time):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<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>

<bean id="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 转换和格式

Java Bean 验证

Spring 框架提供了对 Java Bean 验证 API 的支持。

Bean 验证概述

Bean 验证为 Java 应用程序提供了通过约束声明和元数据进行验证的通用方法。要使用它,您需要使用声明性验证约束对域模型属性进行注释,然后由运行时强制实施。有内置的约束,您也可以定义自己的自定义约束。

考虑以下示例,该示例显示了具有两个属性的简单 PersonForm 模型:

1
2
3
4
public class PersonForm {
private String name;
private int age;
}

Bean 验证使您可以声明约束,如以下示例所示:

1
2
3
4
5
6
7
8
9
public class PersonForm {

@NotNull
@Size(max=64)
private String name;

@Min(0)
private int age;
}

然后,Bean 验证验证器根据声明的约束来验证此类的实例。 有关该 API 的一般信息,请参见 Bean 验证。 有关特定限制,请参见 Hibernate Validator 文档。 要学习如何将 bean 验证提供程序设置为 Spring bean,请继续阅读。

配置 Bean 验证提供程序

Spring 提供了对 Bean 验证 API 的全面支持,包括将 Bean 验证提供程序作为 Spring Bean 进行引导。 这使您可以在应用程序中需要验证的任何地方注入 javax.validation.ValidatorFactoryjavax.validation.Validator

您可以使用 LocalValidatorFactoryBean 将默认的 Validator 配置为 Spring Bean,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration

public class AppConfig {

@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean;
}
}

前面示例中的基本配置触发 Bean 验证以使用其默认引导机制进行初始化。 Bean 验证提供程序,例如 Hibernate Validator,应该存在于类路径中并被自动检测到。

注入验证器

LocalValidatorFactoryBean 同时实现 javax.validation.ValidatorFactoryjavax.validation.Validator 以及 Spring 的 org.springframework.validation.Validator。 您可以将对这些接口之一的引用注入需要调用验证逻辑的 bean 中。

如果您希望直接使用 Bean Validation API,则可以注入对 javax.validation.Validator 的引用,如以下示例所示:

1
2
3
4
5
6
7
8
import javax.validation.Validator;

@Service
public class MyService {

@Autowired
private Validator validator;
}

如果您的 bean 需要使用 Spring Validation API,则可以注入对 org.springframework.validation.Validator 的引用,如以下示例所示:

1
2
3
4
5
6
7
8
import org.springframework.validation.Validator;

@Service
public class MyService {

@Autowired
private Validator validator;
}

配置自定义约束

每个 bean 验证约束都包括两个部分:

@Constraint 批注,用于声明约束及其可配置属性。

javax.validation.ConstraintValidator 接口的实现,用于实现约束的行为。

要将声明与实现相关联,每个 @Constraint 批注都引用一个对应的 ConstraintValidator 实现类。 在运行时,当在域模型中遇到约束注释时,ConstraintValidatorFactory实例化引用的实现。

默认情况下,LocalValidatorFactoryBean 配置一个 SpringConstraintValidatorFactory,该工厂使用 Spring 创建 ConstraintValidator 实例。 这使您的自定义 ConstraintValidators 像其他任何 Spring bean 一样受益于依赖项注入。

以下示例显示了一个自定义 @Constraint 声明,后跟一个关联的 ConstraintValidator 实现,该实现使用 Spring 进行依赖项注入:

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
1
2
3
4
5
6
7
8
9
import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

@Autowired;
private Foo aDependency;

// ...
}

如前面的示例所示,ConstraintValidator 实现可以像其他任何 Spring bean 一样具有其 @Autowired 依赖项。

Spring 驱动方法验证

您可以通过 MethodValidationPostProcessor bean 定义将 Bean Validation 1.1(以及作为自定义扩展,还包括 Hibernate Validator 4.3)支持的方法验证功能集成到 Spring 上下文中:

1
2
3
4
5
6
7
8
9
10
11
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

注册一个 DataBinder

从 Spring 3 开始,您可以使用 Validator 配置 DataBinder 实例。 配置完成后,您可以通过调用 binder.validate() 来调用 Validator。任何验证 Errors 都会自动添加到活页夹的 BindingResult 中。

下面的示例演示如何在绑定到目标对象后,以编程方式使用 DataBinder 来调用验证逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

您还可以通过 dataBinder.addValidatorsdataBinder.replaceValidators 配置具有多个 Validator 实例的 DataBinder。当将全局配置的 bean 验证与在 DataBinder 实例上本地配置的 Spring Validator 结合使用时,这很有用。 请参阅 Spring MVC 验证配置

Spring MVC 3 Validation

参见 Spring MVC 中的验证