Spring Framework核心技术-中文文档(校验、数据绑定、类型转换)
考虑将校验作为逻辑的一部有有点也有缺点,Spring提供了一种设计,既不排除校验也不排除数据绑定。特别是,校验不应该与Web层紧耦合,应该更容易本地化,它应该可以插入任何可用的校验器中。考虑到这些问题,Spring提供了
Validator
约定,其又基本又极其适用于应用程序的每一个层面。
数据绑定让用户输入可以动态绑定到应用程序的域模型是非常有用的(或者你用于处理用户输入的任何对象)。Spring提供了名称为DataBinder
的工具来做这这件事。Validator
和DataBinder
组成validation
包,其主要用于但不限于web层。
BeanWrapper
是在Spring Framework中的基本概念并用在很多地方。但是,你可能不需要直接使用BeanWrapper
。因为这是一个参考文档,因此我们觉得可能需要一些解释。我们在本章节解释BeanWrapper
,如果你要使用它,最有可能的情况是,当你尝试将数据绑定到对象时,你差不多要这么做。
Spring的DataBinder
和低级别的BeanWrapper
同时使用PropertyEditorSupport
实现来转换和格式化属性值。PropertyEditor
和PropertyEditorSupport
类型是JavaBean规范的一部分并且也会在本章节进行说明。Spring的core.convert
包提供了通用类型转换工具,还有一个高级别format
包用于格式化UI字段值。你可以使用这些包作为对PropertyEditorSupport
实现的简化版替代方案。他们也会在本章节进行讨论。
Spring通过设置基础设施支持Java Bean检验并适配Spring的自己的Validator
约定。应用程序可以全局一次启用Bean Validation,正如在Java Bean Validation所描述的,将其作为所有验证需求的唯一选择。在web层,应用程序可以进一步通过DataBinder
为每一个本地控制器注册Spring Validator
实例,正如在Configuring a DataBinder,其对于插入自定义验证逻辑是非常有用的。
使用Spring的验证器接口验证
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 Framework也提供了ValidationUtils
助手类。以下示例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(..)
方法用于如果属性为null
或者空字符串则拒绝name
属性。可以看看ValidationUtils
来了解上面所展示之外的它提供的功能。
尽管它可以通过实现一个单独的Validator
类来在一个丰富对象中验证每一个内嵌的对象,在它自己Validator
实现中为每一个内嵌类封装验证可能更好。一个“丰富”对象的简单示例Customer
,它有两个String
属性(first和second名称)的组合和复杂的Address
对象。Address
对象是Customer
独立使用,所以一个独特的AddressValidator
已经实现。如果你想要你的CustomeValidator
重用在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
字段时提供的错误码这样做。当你调用来自Errors
接口的rejectValue
或者其他reject
方法的其中一个时,底层实现不仅注册你传入的编码,而且还注册多个额外的错误编码。MessageCodesResolver
决定Errors
接口注册的错误编码。默认情况下,使用DefaultMessageCodesResolver
,其不仅注册一个带有你给定的编码的信息,而且还注册包含你传入到拒绝方法的字段名称的信息。所以,如果你通过使用rejectValue("age", "too.darn.old")
拒绝一个字段,除了too.darn.old
编码之外,Spring也注册too.darn.old.age
和too.darn.old.age.int
(第一个包含字段名称,第二个包含字段类型)。这是为了方便开发人员定位错误信息而进行的。
关于MessageCodesResolver
的更多信息和默认策略,依次都可以在MessageCodesResolver
和DefaultMessageCodesResolver
找到。
Bean填充和BeanWrapper
org.springframework.beans
包遵守JavaBean标准。JavaBean是一个带有默认无参构造器的类并且遵循命名规范例如名称为bingoMadness
的属性,会有一个setter方法setBingoMadness(..)
和一个getter方法getBingoMadness()
。关于JavaBean的规范的更多信息,请查看javabeans
。
在bean包中一个非常重要的类是BeanWrapper
接口和它相对应的BeanWrapperImpl
实现。正如javadoc所述,BeanWrapper
提供set和get属性值的功能(逐个或者批量),获取属性描述符,查询属性他们是否可读和可写。而且,BeanWrapper
提供对内嵌属性的支持,可以对无限制深度的子属性设置属性值。BeanWrapper
也支持在目标类中无需支持代码的情况下添加标准JavaBean PropertyChangeListeners
和VetoableChangeListeners
的能力。最后但同样重要的是,BeanWrapper
提供对设置索引的属性的支持。BeanWrapper
通常不会被应用程序代码直接使用,通过DataBinger
和BeanFactory
使用。
BeanWrapper
的工作方式通过它的名称可以得到提示:它封装了一个bean来执行该bean的行为,例如set和检索属性。
set和get基本和嵌套属性
set和get属性通过BeanWrapper
重载的方法变体setPropertyValue
和getPropertyValue
完成。请查看他们的Javadoc了解细节。以下表格展示了这些规范的一些示例:
表达式 | 说明 |
---|---|
name | 表示属性name ,相对应的getName() 或者isName() 和setName(..) 方法。 |
account.name | 表示属性account 的内嵌属性name ,相对应的getAccount().setName() 或者getAccount().getName() 方法 |
account[2] | 表示索引属性account 的第三个元素。索引的属性可以是类型array ,list ,或者其他排序的集合。 |
account[COMPANYNAME] | 表示通过account Map 属性的COMPANYNAME 键的map条目的值 |
如果你不打算直接使用BeanWrapper
,那么下一部分对你没那么至关重要。如果你只使用DataBinder
和BeanFactory
和他们默认的实现,你应该跳到PropertyEditors
章节。
以下两个示例类使用BeanWrapper
来get和set属性:
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;
}
}
以下代码片段展示如何检索和填充实例化的Company
和Employee
的一些属性的示例:
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'
),而且我们仍可以将人类可读性转换为原始日期(或者更好的是,将人类可读的任何日期转换为日期对象)。此行为可以通过注册自定义类型为java.beans.PropertyEditor
的修改器来实现。在BeanWrapper
上注册自定义修改器或者替代方案是在一个特定的IoC容器中(正如在前一章提到的)注册自定义编辑器,使其具备将属性转换为所需类型的知识。要了解更多关于PropertyEditor
,请查看来自Oracle的java.beans
包的javadoc。
在Spring中使用属性修改的几个示例:
- 使用
PropertyEditor
实现完成bean的设置属性。当你使用String
作为一些在XML文件中声明的bean的属性的值时,Spring(如果响应的属性的setter有Class
参数)使用ClassEditor
来尝试解析Class
类的参数。 - 使用所有类型的
PropertyEditor
实现完成在Spring的MVC框架中解析HTTP请求参数,你可以在CommandController
的所有子类中手动绑定。
类 | 说明 |
---|---|
ByteArrayPropertyEditor | 字节数组修改器。将对应的字符串转换为字节表示。通过BeanWrapperImpl 默认注册。 |
ClassEditor | 将表示类的字符日字符串解析为实际类,反之亦然。当类没有被发现,IllegalArgumentException 抛出。默认情况下,通过BeanWrapperImpl 注册。 |
CustomBooleanEditor | 可自定义的属性修改器用于Boolean 属性。默认情况下,通过BeanWrapperImpl 注册,但是可以通过它的自定义实例作为一个自定义修改器来重写。 |
CustomCollectionEditor | 自定义的属性修改器用于集合。将任何源Collection 转换为给定目标的Collection 类型 |
CustomDateEditor | 可自定义的属性修改器用于java.util.Date ,支持自定义DateFormat 。默认情况下,不会注册。必须用户按需注册合适的格式。 |
CustomNumberEditor | 可自定义的属性修改器用于任何Number 子类,例如Ingeter ,Long ,Float 和Double 。默认情况下,通过BeanWrapperImpl 注册,但是可以通过注册一个自定义实例进行重写。 |
FileEditor | 将字符串解析为java.io.File 对象。默认情况下,BeanWrapperImpl 注册。 |
InputStreamEditor | 单向属性修改器,可以接受一个字符串并产生(通过中间ResourceEditor 和Resource )一个InputStream ,以便InputStream 属性可以作为字符串直接设置。注意默认用法不会关闭InputStream 。默认情况下通过BeanWrapperImpl 注册。 |
LocaleEditor | 可以将字符串解析为Locale 对象,反之亦然(字符串格式是[language]_[country]_{variant] ,与Local 的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
实现,和大多数原生类型的实现。注意,如果他们与他们处理的类在相同的包路径下并且与该类有相同的名称,只是在末尾添加了Editor
,则标准的JavaBean底层结构自动发现PropertyEditor
类(无需你必须显性地注册他们)。例如,一个存在以下类和包结构的示例,其将满足SomethingEditor
类被识别,并当做处理Something
-类型的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源代码关联了一个具有Something
类的age
属性的CustomNumberEditor
:
public class SomethingBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
@Override
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
}
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
注册额外的自定义PropertyEditor
实现
当将bean属性设置为字符串值时,Spring IoC容器最终使用标准的JavaBean PropertyEditor
实现来讲这些字符串转换为属性的复杂类型。Spring预注册多个自定义的PropertyEditor
实现(例如,以字符串表示的类名称转换为Class
对象)。除此之外,Java的标准JavaBean PropertyEditor
查找机制允许为类命名适当的PropertyEditor
,并将其放置到与它提供支持的类相同的包路径,这样它可以自动发现它。
如果有需求注册其他自定义PropertyEditors
,几个机制是可以使用的。最手动方式,不太方便并且不太推荐,是使用ConfigurableBeanFactory
接口的registerCustomEditor()
方法,假设你有一个BeanFactory
引用的话。另外一种机制是使用称为CustomEditorConfigurer
的特殊的bean后置处理器。尽管你可以使用BeanFactory
的bean工厂后置处理器,CustomEditorConfigurer
有一个内置的属性设置,所以我们强烈推荐你在ApplicationContext
中使用它,你可以以任何其他bean类似的方式部署它,并且自动检测和提供它。
注意所有的bean工厂和应用程序上下文自动使用多个内建的属性修改器,通过使用BeanWrapper
来处理属性转换。标准的属性修改器,在上一章节列出了BeanWrapper
注册的。除此之外,ApplicationContext
也重写或者添加额外的处理器,以便根据特定的应用上下文类型以适当的方式处理资源查找。
标准的JavaBean PropertyEditor
实例用于将以字符串表达的属性值转换为实际的属性的复杂类型。你可以使用CustomEditorConfigurer
,一个bean工厂后置处理器,要方便地将额外PropertyEditor
实例的支持添加到一个ApplicationContext
:
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;
}
}
当已经完全设置,我们想要可以将类型属性赋值为一个字符串,其PropertyEditor
转换为实际ExoticeType
实例。以下bean定义展示了如果设置此关系:
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor
实现可以类似于以下内容:
package example;
import java.beans.PropertyEditorSupport;
// converts string representation to ExoticType object
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
(DataBingder
)实现的接口。PropertyEditorRegistrar
实例与CustomEditorConfigurer
(在此描述)结合使用是相当方便的,其暴露了一个称为setPropertyEditorRegistrars(..)
的属性。以这种方式添加到CustomEditorConfigurer
的PropertyEditorRegistrar
实例可以容易地与DataBinder
和Spring MVC共享。进一步说,它避免自定义修改器上同步的需要:PropertyEditorRegistrar
预计为每一个bean创建尝试创建一个全新的PropertyEditor
实例。
以下示例展示了如何创建你自己的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框架的这些,使用PropertyEditorRegistrar
与数据绑定web控制器结合可能非常方便。以下示例在@InitBinder
方法的实现中使用了PropertyEditorRegistrar
:
@Controller
public class RegisterUserController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
@InitBinder
void initBinder(WebDataBinder binder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// other methods related to registering a User
}
PropertyEditor
注册风格可以导致简洁的代码(@InitBinder
方法的实现仅仅一行)并且让通用的PropertyEditor
注册代码封装到一个类,然后可以被所需的多个控制器之间进行共享。
Spring类型转换
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)
的每一个调用,源参数保证不能为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
有时候,只有当特定的条件处理为true是才有你想要运行的Converter
。例如,只有当特定的注解在目标字段存在时,你可能想要运行一个Converter
,或者只有当特定的方法(例如static valueOf
方法)在目标类定义时你可能才想要运行一个Converter
。ConditionalGenericConverter
是GenericConverter
和ConditionalConverter
接口的结合,允许你定义这样的常用匹配标准:
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
ConditionalGenericConverter
一个好的示例是IdToEntityConverter
,它在持久化实体标识符和实体引用之间转换。这样一个IdToEntityConverter
只有在目标实体类型申明了一个车静态finder方法(例如,findAccount(Long)
)时才会匹配。你可以在matchs(TypeDescriptor,TypeDescriptor)
中执行这样的一个finder方法检查。
ConversionService
API
ConversionService
定义了一个一致性的,用于在运行时执行类型转换逻辑的API。Converters通常在以下外观接口运行:
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
实现委托给他的注册的转换器来执行类型转换逻辑。
稳固的ConversionService
实现在core.convert.support
包中提供。GenericConversionService
是常用目标实现在大多数环境中适用于使用。ConversionServiceFactory
提供一个方便的工厂来创建通用的ConversionService
配置。
配置ConversionService
ConversionService
是一个无状态的对象,在应用程序启动时实例化然后在多个线程共享。在Spring应用程序中,你通常为每一个Spring容器(例如ApplicationContext
)配置一个ConversionService
实例。Spring拿起ConversionService
并在需要框架执行类型转换时使用它。你也可以将ConversionService
注入到任意你的bean和直接调用它。
如果没有
ConversionService
在Spring注册,使用原有的基于PropertyEditor
系统。
要Spring注册一个默认的ConversionService
,添加以下id
为conversionService
的bean定义。
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的ConversionService
可以在字符串,数组,枚举,集合,map和其他常用的类型之间转换。要使用你自己定义的转换器来补充或者重写默认的转换器,设置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章节中的转换和格式化。
在某些场景中,你可能希望在转换期间进行格式化。请查看FormatterRegistry
SPI了解更多关于使用FormattingConversionServiceFactoryBean
细节。
程序化使用ConversionService
要程序化与ConversionService
一起工作,你可以注入一个它的引用,就像你需要的其他bean一样。以下示例展示了如何这样做:
@Service
public class MyService {
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
对于大多数使用情况,你可以使用convert
方法指定targetType
,但是它不能以更加灵活的方式工作了,例如一个参数化元素的集合。例如如果你想要程序化地将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
集合转换为T
集合,假设标准的集合处理是合理的。
Spring字段格式化
正如在前章节讨论的,core.convert
是多用途的类型转换系统。它提供了一整套ConversionService
API以及强类型的Converter
SPI用于实现从一个类型到另外一个类型的转换逻辑。Spring容器使用此系统来绑定bean属性值。除此之外,Spring表达式语言(SpEL)和DataBinder
使用此系统来绑定字段值。例如,当SpEL需要强制Short
转换为Long
来完成expression.setValue(Object bean, Object value)
尝试,core.convert
系统执行强制转换。
现在考虑典型的客户端环境的类型转换需求,例如web或者桌面应用程序。在这样的环境中,你通常将字符串转换为支持客户端回传过程的类型,以及返回String
为支持展示呈现过程。除此之外,你经常需要本地化String
值。更加常用的core.convert
Convert
SPI不需要直接解决这样格式化需求。要直接处理他们,Spring提供了方便的Formatter
SPI,其提供了一个简单的和强大的用于客户端环境的PropertyEditor
实现的替代方案。
通常,当你需要实现多功能的类型转换逻辑时,你可以使用Converter
SPI–例如,用于在java.util.Date
和Long
之间的转换。当你在一个客户端程序(例如web应用程序)工作并且需要转换和打印本地化字段值时,你可以使用Formatter
SPI。ConvertsionService
提供了统一的类型转换API用于两个SPI。
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
,实现正如早前展示的Formatter
接口。参数化T
是你希望格式化的对象类型–例如,java.util.Date
。实现print()
操作来打印一个T
实例用于在客户端环境进行展示。实现parse()
操作来转换来自客户端环境格式化表示的T
实例。你的如果转换尝试失败,Formatter
应该抛出一个ParseException
或者一个IllegalArgumentException
。注意确保你的Formatter
实现是线程安全的。
为方便起见,format
子包提供了几个Formatter
实现。number
包提供了NumberStyleFormatter
,CurrencyStyleFormater
和PercentStyleFormatter
来格式化Number
对象,其使用了java.text.NumberFormat
。datetime
包提供了DateFromatter
来使用java.text.DateFormat
格式化对象`java.util.Date对象。
以下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 Issue来进行贡献。
注解驱动的格式化
字段格式化可以通过字段类型或者注解进行配置。要将一个注解绑定到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
。 getFiledTypes()
返回被使用注解的字段类型。getPrinter()
返回Printer
来打印注解字段的值。getParser()
返回Parser
来转换注解字段的一个clientValue
。
以下示例AnnotationFormatterFactory
实现将@NumberFormat
注解绑定到一个formatter,以允许指定的数字风格或者格式。
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {
private static final Set<Class<?>> FIELD_TYPES = Set.of(Short.class,
Integer.class, Long.class, Float.class, Double.class,
BigDecimal.class, BigInteger.class);
public Set<Class<?>> getFieldTypes() {
return FIELD_TYPES;
}
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
return switch(annotation.style()) {
case Style.PERCENT -> new PercentStyleFormatter();
case Style.CURRENCY -> new CurrencyStyleFormatter();
default -> new NumberStyleFormatter();
};
}
}
要触发格式,你可以使用NumberFormat
注解字段,正如以下示例所示:
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
格式注解API
方便的格式化注解API存在于org.springframework.format.annotation
包。你可以使用@NumberFormat
来格式化Number
字段,例如Double
和Long
,并且@DateTimeFormat
来格式化java.util.Date
,java.util.Calender
,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
实现,适用于大多数环境。你可以通过FormattingConversionServiceFactoryBean
程序化或者声明式地配置这个作为Spring Bean的变体,等等。因为此实现也实现了ConversionService
,你可以直接配置它与Spring的DataBinder
和Spring Expression Language(SpEL)使用。
以下列表展示了FormatterRegistry
SPI:
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}
正如前面列表所展示的,你可以通过字段类型或者注解注册格式化器。
FormatterRegistry
SPI允许你集中配置格式化规则,而不是在控制器之间重复这样的配置。例如,你可以想要强制所有的时间字段以某种方式被格式化或者使用特定注解的字段以某种方式被格式化。使用共享的FormatterRegistry
,你定义了这些规则一次,无论何时需要格式化都能应用他们。
FormatterRegistrar
SPI
FormatterRegistrar
是用于通过FormatterRegistry注册格式化器和转换器的SPI。以下列表展示了它的接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
当对于一个给定的格式化分类注册多个相关的转换器和格式化器,FormatterRegistrar
是非常有用的,例如时间格式化。当声明式注册不满足时,它也可以非常有用–例如,当需要将一个格式化器索引到一个与它自己的<T>
不同的特定的字段类型下或者当注册一个Pringer
/Parser
对。下一章节提供了关于朱让其和格式化器注册的更多信息。
在Spring MVC 中配置格式化
请在Spring MVC章节中查看Conversion和Formatting
配置一个全局Date和Time格式
默认情况下,日期和时间字段不是使用@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 dateTimeRegistrar = new DateTimeFormatterRegistrar();
dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
dateTimeRegistrar.registerFormatters(conversionService);
// Register date conversion with a specific global format
DateFormatterRegistrar dateRegistrar = new DateFormatterRegistrar();
dateRegistrar.setFormatter(new DateFormatter("yyyyMMdd"));
dateRegistrar.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应用程序中配置日期和时间格式时有一些额外的注意事项。请查看WebMVC转换和格式化或者WebFlux转换和格式化。
Java Bean校验
Spring Framework提供对Java Bean Validation API的支持。
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 Validation 验证器,然后基于声明的约束校验此类的实例。请查看关于API通用信息的Bean Validation。请查看Hibernate Validator文档了解特定的约束。要学习如何设置bean验证提供者作为一个Spring bean,请继续阅读。
配置一个Bean Validation提供者
Spring提供对Bean Validation API包括Bean Validation提供者作为Spring bean启动的完整支持。这允许你注入jakarta.validation.ValidatorFactory
或者jakarta.validation.Validator
,在应用程序中无论哪里需要校验。
你可以使用LocalValidatorFactoryBean
来配置作为Spring Bean默认的校验器,正如以下示例所示:
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class AppConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
通过使用它的默认引导机制,在前面示例中的基本配置触发了bean校验来进行初始化。Bean Validation提供者,例如Hibnernate Validator,应该在类路径中存在则自动化监测。
注入一个Validator
LocalValidatorFactoryBean
同时实现jakarta.validation.ValidatorFactory
和jakarta.validation.Validator
,以及Spring的org.springframework.validation.Validator
。你可以将特写接口任意一个引用注入到需要调用校验逻辑的bean。
如果你更倾向于直接与Bean Validation API一起工作,你可以注入jakarta.validation.Validator
的引用,正如以下示例所示:
import jakarta.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
注解,声明了约束和它的可配置属性。jakarta.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 jakarta.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
注解进行注解,可以自选声明要使用的验证组。请查看MethodValidationPostProcessor
来了解Hibernate Validator和Bean Validation 1.1提供者设置详情。
方法验证依赖围绕目标类的AOP代理,或者用于在接口方法上JDK动态代理或者CGLIB代理。代理的使用有一些限制,一些限制在理解AOP代理有所描述。除此之外,记住要使用在代理的类的方法和访问器;直接字段方法将不起作用。
额外的配置选项
默认的LocalValidatorFactoryBean
配置满足大多数场景。有多个配置选项用于各种不同的Bean Validation构造器,从消息插值到遍历解析。请查看LocalValidatorFactoryBean
javadoc了解关于这些选项的更多信息。
配置一个DataBinder
你可以配置一个带有Validator
的DataBinder
实例。一旦配置,你可以通过调用binder.validate()
调用Validator
。任何验证Errors
会自动添加到binder的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验证配置。
Spring MVC 3 Validation
请查看在Spring MVC章节的Validation。