前言
数据填充
将验证视为业务逻辑有利有弊,Spring为验证(和数据绑定)提供了一种不排除其中任何一种的设计。具体来说,验证不应该绑定到web层,而且应该易于本地化,而且应该可以插入任何可用的验证器。考虑到这些问题,Spring提供了一个Validator
契约,它既基本又在应用程序的每一层都非常有用。
数据绑定对于将用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring提供了恰当命名的DataBinder
来完成这一任务。Validator
和DataBinder
组成了validation
包,它主要用于但不限于web层。
BeanWrapper
是Spring框架中的一个基本概念,在很多地方都被使用。但是,您可能不需要直接使用BeanWrapper
。然而,因为这是参考文档,我们觉得一些解释可能是合适的。我们将在本章中解释BeanWrapper
,因为如果您打算使用它,那么您最有可能在尝试将数据绑定到对象时使用它。
Spring的DataBinder
和低级的BeanWrapper
都使用PropertyEditorSupport
实现来解析和格式化属性值。PropertyEditor
和PropertyEditorSupport
类型是javabean规范的一部分,在本章中也会进行解释。Spring 3引入了一个core.convert
包,它提供了一个通用类型转换工具,以及一个用于格式化UI字段值的高级“格式”包。您可以使用这些包作为PropertyEditorSupport
实现的更简单的替代方案。本章也将对它们进行讨论。
Spring通过设置基础设施和一个适配Spring自己的Validator
契约的适配器来支持Java Bean验证。应用程序可以全局启用一次Bean验证,如Java Bean验证中所述,并专门用于所有验证需求。在web层中,应用程序可以进一步为每个DataBinder
注册控制器本地Spring Validator
实例,如配置一个DataBinder中所述,这对于插入自定义验证逻辑非常有用。
通过使用Spring的Validator接口进行验证
Spring提供了一个Validator
接口,您可以使用它来验证对象。Validator
接口通过使用一个Errors
对象工作,这样,在进行验证时,验证器可以向Errors
对象报告验证失败。
考虑下面这个小数据对象的例子:
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
下一个示例通过实现org.springframework.validation.Validator
接口的以下两个方法来为Person
类提供验证行为:
-
supports(Class)
:这个Validator
可以验证所提供的Class
的实例吗? -
validate(Object, org.springframework.validation.Errors)
:验证给定的对象,如果验证错误,将其注册到给定的Errors
对象中。
实现Validator
相当简单,特别是当您知道Spring框架还提供了ValidationUtils
helper类时。下面的示例实现了Person
实例的Validator
:
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
类上的static
rejectIfEmpty(..)
方法用于在name
属性为null
或空字符串时拒绝它。看看ValidationUtils
javadoc,看看它除了前面显示的示例之外还提供了哪些功能。
虽然可以实现单个Validator
类来验证富对象中的每个嵌套对象,但最好是将每个嵌套对象类的验证逻辑封装在它自己的验证器实现中。富对象的一个简单示例是Customer
,它由两个String
属性(第一个和第二个名称)和一个复杂的Address
对象组成。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("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中找到。
解析代码到错误消息
我们讨论了数据绑定和验证。本节介绍输出与验证错误相对应的消息。在上一节所示的示例中,我们拒绝了name
和age
字段。如果我们希望通过使用MessageSource
输出错误消息,我们可以使用拒绝字段时提供的错误代码(在本例中是“name”和“age”)来实现这一点。当您(直接或间接地,通过使用,例如,ValidationUtils
类)从Errors
接口调用rejectValue
或其他reject
方法之一时,底层实现不仅注册您传入的代码,而且还注册许多额外的错误代码。MessageCodesResolver
确定Errors
接口注册的错误代码。默认情况下,使用DefaultMessageCodesResolver
,它(例如)不仅用您提供的代码注册消息,而且还注册包含您传递给reject方法的字段名的消息。所以,如果你通过使用rejectValue("age", "too.darn.old")
拒绝一个字段,Spring还注册了too.darn.old.age
和too.darn.old.age.int
(第一个包含字段名,第二个包含字段类型)。这样做是为了方便帮助开发人员定位错误消息。
关于MessageCodesResolver
和默认策略的更多信息可以分别在MessageCodesResolver
和DefaultMessageCodesResolver
的javadoc中找到。
Bean操作和BeanWrapper
org.springframework.beans
包遵循JavaBeans标准。JavaBean是一个具有默认无参数构造函数的类,它遵循命名约定,其中(例如)名为bingoMadness
的属性具有setter方法setBingoMadness(..)
和getter方法getBingoMadness()
。有关JavaBeans和规范的更多信息,请参见JavaBeans。
bean包中一个非常重要的类是BeanWrapper
接口及其相应的实现(BeanWrapperImpl
)。从javadoc中可以看到,BeanWrapper
提供了设置和获取属性值(单个或批量)、获取属性描述符和查询属性(以确定它们是可读还是可写)的功能。此外,BeanWrapper
还提供了对嵌套属性的支持,支持将子属性上的属性设置为无限深度。BeanWrapper
还支持添加标准JavaBeans PropertyChangeListeners
和VetoableChangeListeners
的能力,而不需要支持目标类中的代码。最后但并非最不重要的是,BeanWrapper
提供了设置索引属性的支持。BeanWrapper
通常不是由应用程序代码直接使用,而是由DataBinder
和BeanFactory
使用。
BeanWrapper
的工作方式部分由其名称表示:它包装一个bean以在该bean上执行操作,例如设置和检索属性。
设置和获取基本属性和嵌套属性
设置和获取属性是通过BeanWrapper
重载的setPropertyValue
和getPropertyValue
方法变量来完成的。详情请参阅他们的Javadoc。下表展示了这些约定的一些例子:
表达式 | 解释 |
| 指示与getName()或isName()和setName(…)方法对应的属性 |
| 指示与(例如) |
| 指示已索引的属性 |
| 指示 |
(如果您不打算直接使用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());
// 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
实现
Spring使用PropertyEditor
的概念来实现Object
和String
之间的转换。用不同于对象本身的方式表示属性是很方便的。例如,Date
可以以人类可读的方式表示(如String
:'2007-14-09'
),而我们仍然可以将人类可读的形式转换为原始日期(或者,更好的是,将任何以人类可读形式输入的日期转换为Date
对象)。这个行为可以通过注册java.beans.PropertyEditor
类型的自定义编辑器来实现。在BeanWrapper
上注册自定义编辑器,或者在特定的IoC容器中注册(如前一章所述),可以让它了解如何将属性转换为所需的类型。有关PropertyEditor
的更多信息,请参见来自Oracle的java.bean
包的javadoc。
Spring中使用属性编辑的几个例子:
- 通过使用
PropertyEditor
实现来设置bean上的属性。当您使用String
作为您在XML文件中声明的某个bean的属性的值时,Spring(如果相应属性的setter有一个Class
参数)使用ClassEditor
尝试将该参数解析为一个Class
对象。 - 解析Spring MVC框架中的HTTP请求参数是通过使用各种
PropertyEditor
实现来完成的,这些实现可以手动绑定到CommandController
的所有子类中。
Spring有许多内置的PropertyEditor
实现,可以简化工作。它们都位于org.springframework.beans.propertyeditors
包中。默认情况下,大多数(但不是全部,如下表所示)是由BeanWrapperImpl
注册的。在属性编辑器以某种方式可配置的情况下,您仍然可以注册自己的变体以覆盖默认的变体。下表描述了Spring提供的各种PropertyEditor
实现:
类 | 解释 |
| 字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认情况下由 |
| 将表示类的字符串解析为实际类,反之亦然。当没有找到类时,抛出 |
| 可自定义的 |
| 集合的属性编辑器,将任何源 |
| 可定制的 |
| 可定制的属性编辑器,用于任何 |
| 将字符串解析为 |
| 单向属性编辑器,可以接受一个字符串并产生(通过中间的 |
| 可以将字符串解析为 |
| 可以将字符串解析为 |
| 可以将字符串(使用 |
| 修剪字符串的属性编辑器。可选允许将空字符串转换为 |
| 可以将URL的字符串表示解析为实际的 |
Spring使用java.beans.PropertyEditorManager
为可能需要的属性编辑器设置搜索路径。搜索路径还包括sun.bean.editors
,它包括Font
、Color
和大多数基本类型的PropertyEditor
实现。还要注意,如果PropertyEditor
类与它们处理的类在同一个包中,并且具有与该类相同的名称,并附加Editor
,标准JavaBeans基础设施就会自动发现它们(无需显式注册)。例如,可以有以下类和包结构,这足以识别SomethingEditor
类并将其用作某些类型属性的PropertyEditor
。
com
chank
pop
Something
SomethingEditor // the PropertyEditor for the Something class
请注意,您也可以在这里使用标准的BeanInfo
javabean机制(在这里进行了一定程度的描述)。下面的示例使用BeanInfo
机制显式地将一个或多个PropertyEditor
实例注册到关联类的属性中:
com
chank
pop
Something
SomethingBeanInfo // the BeanInfo for the Something class
下面引用的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());
}
}
}
注册额外的自定义PropertyEditor
实现
当将bean属性设置为字符串值时,Spring IoC容器最终使用标准的JavaBeans PropertyEditor
实现将这些字符串转换为复杂类型的属性。Spring预注册了许多自定义PropertyEditor
实现(例如,将表示为字符串的类名转换为类对象)。此外,Java标准的JavaBeans PropertyEditor
查找机制允许对类的PropertyEditor
进行适当的命名,并将其放置在与其提供支持的类相同的包中,这样就可以自动找到它。
如果需要注册其他自定义propertyeditor,可以使用几种机制。最手动的方法(通常不方便也不推荐)是使用ConfigurableBeanFactory
接口的registerCustomEditor()
方法,假设您有一个BeanFactory
引用。另一种(稍微方便一些的)机制是使用名为CustomEditorConfigurer
的特殊bean工厂后处理器。虽然可以使用bean工厂后处理器BeanFactory
实现,CustomEditorConfigurer
有一个嵌套的属性设置,所以我们强烈建议您使用它与ApplicationContext
,以类似的方式,您可以将其部署到任何其他bean,它可以自动检测和应用。
请注意,所有bean工厂和应用程序上下文自动使用许多内置的属性编辑器,通过它们使用BeanWrapper
来处理属性转换。前一节列出了BeanWrapper
注册的标准属性编辑器。此外,ApplicationContexts
还覆盖或添加其他编辑器,以适合特定应用程序上下文类型的方式处理资源查找。
标准的JavaBeans PropertyEditor
实例用于将表示为字符串的属性值转换为属性的实际复杂类型。您可以使用CustomEditorConfigurer
(bean工厂后处理器)方便地向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定义显示了如何建立这种关系:
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor
的实现可能类似于以下内容:
// converts string representation to ExoticType object
package example;
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
// converts string representation to ExoticType object
package example;
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
最后,下面的示例展示了如何使用CustomEditorConfigurer
将新的PropertyEditor
注册到ApplicationContext
中,这样ApplicationContext
就可以根据需要使用它了:
<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)实现的。当与CustomEditorConfigurer(此处描述)一起使用时,PropertyEditorRegistrar实例特别方便,后者公开了一个名为setPropertyEditorRegistrars(…)的属性。以这种方式添加到CustomEditorConfigurer中的PropertyEditorRegistrar实例可以很容易地与DataBinder和Spring MVC控制器共享。此外,它避免了自定义编辑器上的同步需求:PropertyEditorRegistrar将为每个bean创建尝试创建新的propertyedititor实例。
下面的例子展示了如何创建你自己的PropertyEditorRegistrar
实现:
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// you could register as many custom property editors as are required here...
}
}
参见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
与数据绑定控制器(如SimpleFormController
)结合使用是非常方便的。以下示例在initBinder(..)
方法的实现中使用了PropertyEditorRegistrar
:
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);
}
// other methods to do with registering a User
}
这种类型的PropertyEditor
注册可以导致简洁的代码(initBinder(..)
的实现只有一行),并允许将通用的PropertyEditor
注册代码封装在一个类中,然后根据需要在多个Controllers
之间共享。
Spring类型转换
Spring 3引入了一个core.convert
包,它提供了一个通用的类型转换系统。系统定义了一个SPI来实现类型转换逻辑,以及一个API来在运行时执行类型转换。在Spring容器中,您可以使用这个系统作为PropertyEditor实现的替代,将外部化的bean属性值字符串转换为所需的属性类型。您还可以在应用程序中任何需要类型转换的地方使用公共API。
转换器SPI
实现类型转换逻辑的SPI是简单且强类型的,如下面的接口定义所示:
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
要创建自己的转换器,请实现Converter
接口,并将S
参数化为要转换的类型,将T
参数化为要转换的类型。如果S
的集合或数组需要转换为T
的集合或数组,您也可以透明地应用这样的转换器,前提是委托的数组或集合转换器也已经注册(默认情况下,DefaultConversionService
会这样做)。
对于每个convert(S)
的调用,source参数保证不为null。如果转换失败,您的Converter
可能会抛出任何未检查的异常。具体来说,它应该抛出一个IllegalArgumentException
来报告一个无效的源值。请注意确保您的Converter
实现是线程安全的。
为了方便起见,core.convert.support
包中提供了几个转换器的实现。其中包括从字符串到数字和其他常见类型的转换器。下面的清单显示了StringToInteger
类,它是一个典型的Converter
实现:
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
,如下面的示例所示:
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>)
,其中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());
}
}
}
使用GenericConverter
当您需要一个复杂的Converter
实现时,请考虑使用GenericConverter
接口。与Converter
相比,GenericConverter
具有更灵活但类型较弱的签名,它支持在多个源类型和目标类型之间进行转换。此外,在实现转换逻辑时,可以使用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)
来包含转换逻辑。源TypeDescriptor
提供了对保存被转换值的源字段的访问。目标TypeDescriptor
提供了对要设置转换后的值的目标字段的访问。
GenericConverter
的一个很好的例子是在Java数组和集合之间进行转换的转换器。这样的ArrayToCollectionConverter
会内省声明目标集合类型的字段,以解析该集合的元素类型。这使得源数组中的每个元素都可以在目标字段上设置集合之前转换为集合元素类型。
因为
GenericConverter
是一个更复杂的SPI接口,所以您应该只在需要时使用它。为基本类型转换需求,请选择Converter
或ConverterFactory
。
使用ConditionalGenericConverter
有时,您希望仅在特定条件为真时才运行Converter
。例如,您可能希望仅在目标字段上存在特定注释时运行Converter
,或者您可能希望仅在目标类上定义了特定方法(例如static valueOf
方法)时运行Converter
。ConditionalGenericConverter
是GenericConverter
和ConditionalConverter
接口的结合,让你可以定义这样的自定义匹配条件:
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
ConditionalGenericConverter
的一个很好的例子是在持久实体标识符和实体引用之间进行转换的EntityConverter
。只有当目标实体类型声明了一个静态查找器方法(例如,findAccount(Long)
)时,这样的EntityConverter
才可能匹配。您可以在matches(TypeDescriptor, TypeDescriptor)
的实现中执行这样的查找器方法检查。
ConversionService
API
ConversionService
定义了在运行时执行类型转换逻辑的统一API。转换器通常在以下门面接口后面运行:
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
,请添加以下id
为conversionService
的bean定义:
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的ConversionService
可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。若要使用自己的自定义转换器补充或覆盖默认转换器,请设置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
要以编程方式使用ConversionService
实例,您可以像对任何其他bean一样注入对它的引用。下面的例子展示了如何做到这一点:
@Service
public class MyService {
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
对于大多数用例,您可以使用指定targetType
的convert
方法,但是它不能用于更复杂的类型,例如参数化元素的集合。例如,如果希望以编程方式将Integer
List
转换为String
List
,则需要提供源类型和目标类型的正式定义。
幸运的是,TypeDescriptor
提供了各种选项,让这一过程变得简单,如下面的例子所示:
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
会自动注册适合大多数环境的转换器。这包括集合转换器、标量转换器和基本的Object
到String
转换器。通过使用DefaultConversionService
类上的静态addDefaultConverters
方法,可以向任何ConverterRegistry
注册相同的转换器。
值类型的转换器可用于数组和集合,因此不需要创建特定的转换器来将S
的Collection
转换为T
的Collection
,假设标准集合处理是合适的。
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.Date
和Long
之间进行转换)时,可以使用Converter
SPI。当您在客户机环境(例如web应用程序)中工作,并且需要解析和打印本地化字段值时,您可以使用Formatter
SPI。ConversionService
为这两个spi提供了统一的类型转换API。
Formatter
SPI
用于实现字段格式化逻辑的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;
}
要创建自己的格式化程序,请实现前面所示的Formatter
接口。参数化T
为您希望格式化的对象类型—例如,java.util.Date
。实现print()
操作打印T
的一个实例,以便在客户端区域设置中显示。实现parse()
操作,从客户机区域设置返回的格式化表示解析T
的实例。如果解析失败,格式化程序应该抛出ParseException
或IllegalArgumentException
。请确保Formatter
程序实现是线程安全的。
为了方便起见,格式化子包提供了几种格式化器实现。number
包提供NumberStyleFormatter
、CurrencyStyleFormatter
和PercentStyleFormatter
来格式化使用java.text.NumberFormat
的Number
对象。datetime
包提供了一个DateFormatter
来格式化java.util.Date
对象,使用java.text.DateFormat
。
下面的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问题进行贡献。
注解驱动的格式
可以通过字段类型或注释配置字段格式。要将注释绑定到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;
}
格式注释API
org.springframework.format.annotation
包中有一个可移植的格式注释API。您可以使用@NumberFormat
来格式化诸如Double
和Long
之类的Number
字段,并且使用@DateTimeFormat
来格式化java.util.Date
、java.util.Calendar
、Long
(用于毫秒时间戳)以及JSR-310 java.time
。
以@DateTimeFormat
将java.util.Date
格式化为ISO日期(yyyy-MM-dd)为例:
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
FormatterRegistry
SPI
FormatterRegistry
是一个用于注册格式化器和转换器的SPI。FormattingConversionService
是FormatterRegistry
的一个实现,适用于大多数环境。您可以通过编程方式或声明方式将此变体配置为Spring bean,例如通过使用FormattingConversionServiceFactoryBean
。因为这个实现也实现了ConversionService
,所以您可以直接配置它,以便与Spring的DataBinder
和Spring表达式语言(SpEL)一起使用。
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
是一个SPI,用于通过FormatterRegistry注册格式化器和转换器。下面的清单显示了它的接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
当为给定的格式化类别(如日期格式化)注册多个相关的转换器和格式化器时,FormatterRegistrar
非常有用。在声明性注册不足的情况下,它也很有用——例如,当格式化程序需要在与它自己的<T>
不同的特定字段类型下建立索引时,或者在注册Printer
/Parser
对时。下一节提供有关转换器和格式化程序注册的更多信息。
在Spring MVC中配置格式化
请参阅Spring MVC章节中的转换和格式化。
配置全局日期和时间格式
默认情况下,没有使用@DateTimeFormat
注释的日期和时间字段将使用DateFormat.SHORT
样式从字符串转换。如果您愿意,您可以通过定义自己的全局格式来改变这一点。
要做到这一点,请确保Spring没有注册默认格式化程序。相反,在以下方法的帮助下手动注册格式化器:
org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
org.springframework.format.datetime.DateFormatterRegistrar
例如,以下Java配置注册了全局的yyyyMMdd
格式:
@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
。下面的例子展示了如何做到这一点:
<?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.standard.DateTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.standard.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
</bean>
</property>
</bean>
</set>
</property>
</bean>
</beans>
注意:在web应用程序中配置日期和时间格式时,还有一些额外的考虑事项。
Java Bean验证
Bean验证概述
Bean验证为Java应用程序提供了一种通过约束声明和元数据进行验证的通用方法。要使用它,您可以使用声明性验证约束来注释域模型属性,然后运行时强制执行这些约束。有内置的约束,您也可以定义自己的自定义约束。
考虑下面的示例,该示例显示了一个具有两个属性的简单PersonForm
模型:
public class PersonForm {
private String name;
private int age;
}
Bean验证允许您声明约束,示例如下:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
Bean验证验证器然后根据声明的约束验证该类的实例。有关API的一般信息,请参阅Bean验证。有关特定的约束,请参阅Hibernate验证器文档。要学习如何将bean验证提供程序设置为Spring bean,请继续阅读。
配置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 Validation API,则可以注入对org.springframework.validation.Validator
的引用,如下面的示例所示:
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来进行依赖注入:
@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
实现可以将其依赖项@Autowired
作为任何其他Spring bean。
Spring驱动的方法验证
您可以通过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验证器和Bean验证1.1提供程序的设置细节
方法验证依赖于目标类周围的AOP代理,要么是接口上方法的JDK动态代理,要么是CGLIB代理。使用代理有一定的限制,在理解AOP代理中描述了一些限制。此外,记住总是在代理类上使用方法和访问器;直接访问字段将不起作用。
额外的配置选项
默认的LocalValidatorFactoryBean
配置可以满足大多数情况。对于各种Bean验证构造,有许多配置选项,从消息插值到遍历解析。有关这些选项的更多信息,请参阅LocalValidatorFactoryBean
javadoc。
配置一个DataBinder
从Spring 3开始,您可以使用Validator
配置DataBinder
实例。一旦配置好,您就可以通过调用binder.validate()
来调用Validator
。任何验证Errors
都会自动添加到绑定器的BindingResult
中。
下面的例子展示了如何通过编程方式使用DataBinder
在绑定到目标对象后调用验证逻辑:
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.addValidators
和dataBinder.replaceValidators
使用多个Validator
实例来配置一个DataBinder
。这在将全局配置的bean验证与在DataBinder
实例上本地配置的Spring Validator
相结合时非有用。参见Spring MVC验证配置。