RedCloud Help

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

将验证视为业务逻辑有利有弊,而Spring提供的验证(和数据绑定)设计并不排斥其中任何一种。具体来说,验证不应与web层绑定,而且应易于本地化,并可插入任何可用的验证器。考虑到这个问题,Spring提供了一个Validator合约,它既是基础的,又能在应用程序的每层中轻松使用。 数据绑定对于将用户输入动态绑定到应用程序的领域模型(或用于处理用户输入的任何对象)非常有用。Spring提供了恰如其分的DataBinder来实现这一套。Validator和DataBinder构成了validation包,它主要用于但不限于web层。 BeanWrapper是Spring框架中一个基本概念,在很多地方都会用到。不过,你可能并不需要直接使用BeanWrapper。不过由于本文是参考文档,我们认为有必要进行一些解释。我们将在本章中解释BeanWrapper,因为如果使用BeanWrapper,你很可能在尝试将数据绑定到对象时使用它。 Spring的DataBinder和较低级别的BeanWrapper都使用PropertyEditorSupport实现来解析和格式化属性值。PropertyEditor和PropertyEditorSupport类型是JavaBeans规范的一部分,本章也将进行说明。Spring3引入一个core.convert包,它提供了一个通用类型转换工具,以及一个用于格式化UI字段值的更高级“format”包。你可以使用这些包来替代PropertyEditorSupport实现。本章也将讨论它们。 Spring通过设置基础框架和Spring本身Validator合约的是适配器来支持Java Bean验证。如Java Bean Validation中所述,应用程序可以在全局范围内启用Bean Validation,并将其专门用于满足所有验证需求。在Web层中,应用程序还可以根据DataBinder注册控制器本地Spring Validator实例,如配置DataBinder中所述,这对于插入自定义验证逻辑非常有用。

3.1 使用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框架还提供了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"); } } }

如果name属性为null或空字符串,则ValidationUtils类上的static rejectIfEmpty(..)方法将用于拒绝name属性。请查看ValidationUtils javadoc,了解除了前面显示的示例外,它还提供了哪些功能。 当然也可以实现一个Validator类来验证富对象中的每个嵌套对象,但更好的做法可能是将每个嵌套对象类的验证逻辑封装在自己的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。

3.2 将代码解析为错误信息

我们介绍了数据库的绑定和验证。本届将介绍与验证错误相对应的信息的输出。在上一届所示的示例中,我们拒绝了name和age字段。如果我们想使用MessageSource输出错误信息,可以使用拒绝字段时提供的错误代码(本例中为“姓名”和“年龄”)。当你调用rejectValue或来自Errors接口的其他reject方法时(无论是直接调用还是间接调用,例如探索过使用ValidationUtils类),底层实现不仅会注册你传入的代码,还会注册一些额外的错误代码。MessageCodesResolver决定了Errors接口注册的错误代码。默认情况下,使用DefaultMessageCodesResolver,它不仅会注册包含你提供的代码信息,还会注册包含你传递给reject方法的字段名称的信息。因此,如果你使用rejectValue( “age”,“too.darn.old”)拒绝一个字段,除了too.darn.old代码外,Spring还会注册too.darn.old.age和too.darn.old.age.int( 前者包括字段名称,后者包括字段类型)。这样做是为了方便开发人员定位错误信息。 有关MessageCodesResolver和默认策略的更多信息,请分别参阅MessageCodesResolver和DefaultMessageCodesResolver的javadoc。

3.3 Bean Manipulation and the BeanWrapper

org.springframework.beans包遵循JavaBean标准。JavaBean是一个类,它有一个默认的无参数构造函数,并遵循命名规范,例如,一个名为bingoMadness的属性将有一个setter方法setBingoMadness(..) 和一个getter方法getBind过Madness()。有关JavaBeans和规范的更多信息,请参阅javabeans. bean包装包里面一个相当重要的类是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。如javadoc所述,BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符一级查询属性以确定其是否可读或可写的功能。此外,BeanWrapper还支持嵌套属性,可在无限深和VetoableChangeListeners的功能,而无需在目标类中添加支持代码。最后单同样重要的是,BeanWrapper支持设置索引属性。应用程序代码通常不会直接使用BeanWrapper,但会使用DataBinder和BeanFactory。 BeanWrapper的工作方式一定程度上可以从名称中看出:它封装了一个Bean,以便对该Bean执行操作,例如设置和检索属性。

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

设置和获取属性通过BeanWrapper的setPropertyValue和getPropertyValue重载方法变体江湾城。详情参见他们的javadoc。下表列出了这些约定的一些实例:

expression

explanation

name

表示与getName()或isName()和setName()方法相对应的属性name

account.name

表示与getAccount().setName()或getAccount().getName()方法(例如)相对应的属性account的嵌套属性 name

account[2]

表示索引account的第三个元素。索引属性的类型可以是array、list或其他自然有序的集合

account[COMPANYNAME]

表示由account Map属性的COMPANYNAME键索引的地图条目的值

(如果你不打算直接使用BeanWrapper,下一节对你来说并不重要)。如果你只使用DataBinder和BeanFactory及其默认实现,则应调至PropertyEditors部分)。 以下两个示例类使用BeanWrapper获取和设置属性:

ublic 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 S和Employee S的某些属性:

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");

3.3.2 内置PropertyEditor实现

Spring使用PropertyEditor的概念来实现Object和String之间的转换。用不同与对象本身的方式表示属性非常方便。例如,Date可以以人类可读的方法表示(如String:‘2007-14-09’),而我们仍然可以将人类可读的形式转换回原始日期(或者,更好的方法是将人类可读形式输入的任何日期转换回Date对象)这种行为可以通过注册java.beans.PropertyEditor类型的自定义编辑器来实现。在BeanWrapper上注册定义编辑器,或在特定IOC容器中注册自定义编辑器(如上一章所述),可使其了解如何将属性转换为所需的类型。有关PropertyEditor的更多信息,请参阅Oracle的java.beans包的javadoc. 在Spring中使用属性编辑的几个例子:

在bean上设置属性时通过使用PropertyEditor实现完成的。当你使用string作为在XML文件中声明的某个Bean的属性值时,Spring(如果相应属性的设置器有Class参数)会使用ClassEditor尝试将该参数解析为Class对象。

  • 在Spring的MVC框架中,HTTP请求参数的解析是通过使用各种PropertyEditor实现来完成的,你可以在CommandController的所有子类手动绑定这些实现。

Spring有许多内置的PropertyEditor实现,让生活变得简单。他们都位于org.springframework.beans.propertyedtors包中。大多数(但并非全部,如下表所示)默认由BeanWrapperImpl注册。如果属性编辑器可以通过某种方法进行配置,你仍可以注册自己的变量来覆盖默认变量。下表描述了Spring提供的各种PropertyEditor实现:

class

explanation

ByteArrayPropertyEditor

字节数组编辑器。将字符串转换为响应的字节表示。默认由BeanWrapperImpl注册

ClassEditor

将表示类的字符串解析为实际的类,反之亦然。找不到类时,会抛出IllegalArgumentException。默认情况下,用BeanWrapperImpl注册

CustomBooleanEditor

用于Boolean熟悉开的可定制属性编辑器。默认情况下,该编辑器有BeanWrapperImpl注册,但可以通过注册一个自定义编辑器实例来重载

CustomCollectionEditor

用于集合的属性编辑器,可将任何源Collection转换为给定目标Collection类型

CustomDateEditor

用于java.util.Date的可定制属性编辑器,支持自定义DateFormat。默认未注册。用户必须根据需要使用适当的格式注册

CustomNumberEditor

用于任何Number子类的可定制属性编辑器,例如Integer、Long、Float或Double .默认情况下,该编辑器由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来重写

FileEditor

将字符串解析为java.io.File对象。默认情况下,由BeanWrapperImpl

InputStreamEditor

单向属性编辑器,可以接受字符串并生成(通过中间ResourceEditor和Resource)InputeStream,这样InputStream属性可以直接设置为字符串。请注意,默认用法不会为你关闭InputStream。默认情况下,由BeanWrapperImpl注册

LocaleEditor

可以将字符串解析为Locale对象,反之亦然(字符串格式为[language] [country] [variant],与Locale的toSting()方法相同。还接受空格作为分隔符,以替代下划线。默认情况下,由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,其中包括PropertyEditor类型的实现,例如Font、Color和大多数基元类型。还请注意,如果PropertyEditor类与其处理的类在同一个包中,且名称与该类相同,并附加了Editor标记,那么标准JavaBeans基础架构会自动发现这些类(无需你显式注册)。例如,可以使用下面的类和包的结构,这样就足以识别SomethingEditor类,并将其作用Something类型属性的PropertyEditor。

com chank pop Something SomethingEditor // the PropertyEditor for the Something class

请注意,你也可以在此处使用标准的BeanInfo JavaBeans机制(此处有一定描述)。下面的实例使用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) { @Override 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的标准JavaBeansPropertyEditor查找机制允许对类的PropertyEditor进行适当的命名,并将其放置在与其提供支持的类相同的包中,以便可以自动找到它。 如果需要注册其他自定义PropertyEditor,有几种机制可以选择。手动的方法是使用ConfigurableBeanFactory接口的registerCustomEditor() 方法,假设你有BeanFactory引用,这种方法通常并不方便,也不推荐使用。另一种(稍微更方便的)机制是使用名为CustomEditorConfugurer的特殊Bean工厂后处理器。虽然你可以在BeanFactory实现中使用Bean工厂后处理器,但CustomEditorConfigurer具有嵌套属性设置,因此我们强烈建议你在ApplicationContext中使用它,在ApplicationContext中,你可以与任何其他Bean类似的方式部署它,而且它可以被自动检测和应用。 请注意,通过使用BeanWrapper来处理属性转换,所有Bean工厂和应用程序上下文会自动使用一些内置的属性编辑器。上一节列出了BeanWrapper注册的标准属性编辑器。此外,ApplicationContext还可以覆盖或添加额外的编辑器,以适合特定应用上下文类型的方式处理资源查找。 标准JavaBeans PropertyEditor实例用于将以字符串表示的属性值转换为属性的实际复合类型。你可以使用CustomEditorConfigurer这个Bean工厂后处理器,方便地为ApplicationContext添加对附加PropertyEditor实例的支持。 请看下面的示例,它定义了一个名为ExoticType的用户类和另一个名为DependsOnExoticType的类,后者需要ExoticType设置为属性:

ackage 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将其转换为实际的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())); } }

最后,下面的示例展示了如何使用CustomEditorConfigurer向ApplicationContext注册新的PropertyEditor,然后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实现。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 }

这种PropetyEditor注册方式可使代码更加简洁(@InitBinder方法的实现只有一行),并可将常用的PropertyEditor注册代码封装在一类中,然后根据需要在多个控制器中共享。

3.4 Spring Type Conversion

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

3.4.1 Converter 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)时,源参数都保证不为空,你的Converter可以抛出任何未选中的异常。具体来说,它应抛出IllegalArgumentException以报告无效源值。请注意确保你的Converter实现是线程安全的。 core.convert.support包中提供了多个转换器实现,以方便使用。其中包括从字符串数字和其他常见类型的转换器。下面的列表显示了StringToInterger类,他是一个单行的Converter实现:

package org.springframework.core.convert.support; final class StringToInteger implements Converter<String, Integer> { public Integer convert(String source) { return Integer.valueOf(source); } }

3.4.2 使用ConverterFactory

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

package org.springframework.core.convert.converter; public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); }

将S设置为你要转换的类型,将R设为定义你可以转换的类范围的基础类型。然后实现getConverter(Class ),其中T是R的子类。 以StringToEnumConverterFactory为例:

package org.springframework.core.convert.support; final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEnumConverter(targetType); } private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> { private Class<T> enumType; public StringToEnumConverter(Class<T> enumType) { this.enumType = enumType; } public T convert(String source) { return (T) Enum.valueOf(this.enumType, source.trim()); } } }

3.4.3 使用GenericConverter

如果需要复杂的Converter实现,可以考虑使用GenericConverter接口。GenericConverter具有比Converter更灵活但强类型更少的签名,支持在多个源和目标类型之间转换。此外genericConverter还提供了源字段和目标字段上下文,供你在实现转换逻辑时使用。这种上下文允许通过字段注释或在字段特征上声明的通用信息来驱动类型转换。下面的列表显示了GenericConverter的接口定义:

package org.springframework.core.convert.converter; public interface GenericConverter { public Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); }

要实现GenericConverter,请让getConvertibleTypes()返回支持的源-》目标类型对。然后实现convert( Object,TypeDescriptor,TypeDescriptor)以包括转换逻辑。源TypeDescriptor提供对持有转换值的源字段的访问。目标TypeDescriptor用于访问要设置转换值的目标字段。 在Java数组和集合之间进行转换器就是GenericConverter的一个很好的例子,这种ArrayToCollectionConverter内省声明的目标集合类型的字段,以解析集合的元素类型。这样,在将集合设置到目标字段之前,源数组中的每个元素都可以转换为集合的元素类型。

使用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的一个很好的例子是在持久实体标志符和实体引用之间进行转换的IdToEntityConverter。只有当目标实体类行声明了静态查找方法(例如:findAccount( Long))时,这种IdToEntiryConverter才可能匹配。你可以在matches(TypeDescriptor,TypeDescriptor)的实现中执行这样的查找方法检查。

3.4.4 ConversionService应用程序接口

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配置。

3.4.5 配置ConversionService

ConversionService是一个车无状态对象,设计用于在应用程序启动时实例化,然后点多个线程之间共享。在Spring应用程序中,你通常会为每一个Spring容器配置一个COnversionService实例(或ApplicationContext)。只要框架需要执行类型转换,Spring就会获取COnversionService并使用它。你还可以将此ConversionService注入到你的任何Bean中,并直接调用它。

要在Spring中注册默认的ConversionService,请天天加一下Bean定义,其中id为ConversionService:

<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章节中的转换和格式化。 在某些情况下,你可能希望在转换过程中应用格式化。有关使用FormattingConversionServiceFactoryBean的详细信息,请参阅FormatterRegistry SPI。

3.4.6 以编程方式使用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-TOString转换器。通过使用DefaultConversionService类上的静态addDefaultConverters方法,你可以为任何ConverterRegistry注册相同的转换器。 值类型的转换器可重复用于数组和集合,因此无需创建特定的转换器来将Collection的S转换为Collection的T,前提是标准的集合处理是适当的。

3.5 Spring Field Formatting

如上一节所述,core.convert是一个通用类型转换系统。它提供了统一的ConversionServiceApi以及强类型的ConverterSPI,用于实现从一种类型到另一种类型的转换逻辑。Spring容器使用该系统绑定字段。例如,当SpEL需要将Short强制转为Long以完成expression.setValue( Objcet bean,Object value)尝试时,core.convert系统会执行强制转换。 现在考虑一个典型客户端环境(如Web或桌面应用程序)的类型转换要求。在此类环境中,你通常需要从string转换为String以支持客户端回传过程,以及转换回String以支持试图呈现过程。此外,你还经常需要本地化String值。更通用的core.convert Converter SPI无法直接满足此类格式要求。为了直接解决这些问题,Spring3引入了方便的Formatter SPI,为客户端环境提供PropertyEditor实现之外的另一种简单而稳健的实现方式。 一般来说,当你需要实现通用类型转换逻辑(例如,在java.util.Date和Long之间进行转换)时,可以使用Converter SPI。在客户端环境(如网络应用程序)中工作,需要解析和打印本地化字段值时,可以使用Formatter SPI。ConversionService为这两个SPI提供统一的类型转换API。

3.5.1 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实例,以便在客户端本地显示。执行parese() 操作,从客户端本地语言返回的格式化表示解析T实例。如果解析尝试失败,你的Formatter应抛出ParseException或IllegalArgumentException。请注意确保你的Formatter实现是线程安全的。 作为一种便利,format子软件包提供多个Formatter实现。Number包提供了NumberStyleFormatter、CurrencyStyleFormatter和PercentStyleFormatter,用于格式化使用java.text.NumberFormat的Number对象。datetime软件包提供了DateFormatter来格式化使用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 Issues投稿。

3.5.2 注解驱动的格式化

字段格式可通过字段类型或注册进行配置。要将注释绑定到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); }

创建一个实施方案:

  1. 将A设置为字段annotationType,你希望将格式化逻辑与之关联,例如org.springframework.format.annotation.DateTimeFormat.

  2. 让getFiledTypes()返回可使用注释的字段类型。

  3. 让getPrinter()返回可使用注释的字段值。

  4. 让getParser()返回Parser以解析注释字段的clientValue。

下面的实例AnnotationFormatterFactory实现将@NumberFormat注解绑定到个事器,以便指定数字样式或模式:

public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<NumberFormat> { public Set<Class<?>> getFieldTypes() { return new HashSet<Class<?>>(asList(new Class<?>[] { Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, BigInteger.class })); } public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) { return configureFormatterFrom(annotation, fieldType); } public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) { return configureFormatterFrom(annotation, fieldType); } private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) { if (!annotation.pattern().isEmpty()) { return new NumberStyleFormatter(annotation.pattern()); } else { Style style = annotation.style(); if (style == Style.PERCENT) { return new PercentStyleFormatter(); } else if (style == Style.CURRENCY) { return new CurrencyStyleFormatter(); } else { return new NumberStyleFormatter(); } } } }

要触发格式化,可以使用NumberFormat对字段进行注解,如下例所示:

public class MyModel { @NumberFormat(style=Style.CURRENCY) private BigDecimal decimal; }

org.springframework.format.annotation包中有一个可一直格式注释API。你可以使用@NumberFormat来格式化Number字段,如Double和Long;使用@DateTimeFormat来格式化java.util.Date、java.util.Calendar、Long以及JSR-310java.time. 下面的实例使用DateTimeFormat将java.util.date格式化ISO日期(yyyy-MM-dd):

public class MyModel { @DateTimeFormat(iso=ISO.DATE) private Date date; }

3.5.3 FormatterRegistry SPi

FormatterRegistry是用于注册格式器和转换器的SPI。FormattingConversionService是FormatterRegistry的实现,适合于大多数环境。你可以通过变成或声明的方式将此变量配置为SpringBean,例如使用FormattingConversionServiceFactoryBean。由于实现还实现了ConversionService,因此你可以直接将器配置为Spring的DataBinder和Spring表达式语言(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时,你只需定义一次这些规则,他们就会在需要格式化时被应用。

3.5.4 FormatterRegistrarSPI

FormatterRegistrar是通过FormatterRegistry注册格式器和转换器的SPI。下面的列表显示了其接口定义:

package org.springframework.format; public interface FormatterRegistrar { void registerFormatters(FormatterRegistry registry); }

FormatterRegistrar在为特定格式化类别(如日期格式化)注册多个相关转换器和格式器非常有用。在声明式注册不充分的情况下,它也很有用,例如,当格式器需要在不同于自身 的特定字段类型下进行索引时,或者当注册Printer/Parser对时。下一节将提供有关转换器和格式器注册的更多信息。

3.5.5 在SpringMVC中配置格式化

请参阅Spring MVC章节中的转换和格式化。

3.6 配置全局日期和时间的格式

默认情况下,为使用@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>

请注意,在网络应用程序中配置日期和时间格式时需要额外的考虑。请参阅WebMvC转换和格式化或WebFlux转换和格式化。

3.7 Java Bean验证

Spring Framework提供对JAVA Bean Validation Api 支持。

3.7.1 Bean 校验

Bean Validation通过JAVA应用程序的约束声明和元数据提供一种通用的验证方法。要使用它,你需要用声明式验证约束注解领域模型属性,然后由运行时强制执行。有内置的约束,你也可以定义自己的自定义约束。 请看下面的示例,它展示了一个具有两个属性的简单PersonForm模型:

public class PersonForm { private String name; private int age; }

正如下面的实力所示,Bean Validation可以让你声明约束:

ublic class PersonForm { @NotNull @Size(max=64) private String name; @Min(0) private int age; }

然后,Bean验证器会根据声明的越是条件验证该类的实例。有关API的一般信息,请参见Bean Validation。有关具体约束,请参阅Hibernate验证器文档。要了解如何将Bean验证提供程序设置为SpringBean ,请继续阅读。

3.7.2 配置Bean验证提供程序

Spring提供对Bean validation API的全面支持,包括将Bean Validation 提供程序引导为Spring Bean。这样,你就可以在应用程序中需要验证的地方注入javax.validation.ValidatorFactory或javax.validation.Validator. 你可以使用LocalValidatorFactoryBean将默认验证器配置为SpringBean,如下例所示:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @Configuration public class AppConfig { @Bean public LocalValidatorFactoryBean validator() { return new LocalValidatorFactoryBean(); } }

上例中的基本配置通过使用默认的引导机制触发Bean验证初始化。Bean验证提供程序(如Hibernate Validator)预计会出现在类路径中并被自动检测到。

Injecting a Validator

LocalValidatorFactoryBean实现了javax.validation.ValidatorFactory和javax.validation.Validator以及Spring的org.springframework.validation.Validator。你可以将这些接口的引用注入到需要调用验证逻辑的Bean中。 如果你希望直接使用Bean Validation 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声明,以及使用Spring进行依赖注入的相关ConstraintValidator实现:

@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实现可以像任何其他SpringBean一样拥有其依赖关系@Autowired

Spring-driven Method Validation

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

import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; @Configuration public class AppConfig { @Bean public MethodValidationPostProcessor validationPostProcessor() { return new MethodValidationPostProcessor(); } }

要符合Spring驱动的方法验证条件,所有的目标类都需要使用Spring的@Validated注解进行注解,该注解还可以选择性声明要使用的验证组。有关使用Hibernate Validator和Bean Validation 1.1 提供程序的设置详情,请参阅MethodValidationPostProcessor。

其他配置选项

默认的LocalValidatorFactoryBean配置足以应对大多数情况。从消息插值到遍历解析,各种bean验证构造有许多配置选项。有关这些选项的详细信息,请参阅LocalValidatorFactoryBean javadoc。

3.7.3 配置 DataBinder

自Spring3开始,你可以使用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将DataBinder与多个Validator实例配置在一起。这在将全局配置的Bean验证与本地配置在DataBinder实力上的Spring Validator相结合时非常有用。请参见Spring MVC验证配置。

3.7.4 Spring MVC Validation

请参见SpringMVC章节中的验证。

12 January 2026