9. 验证,数据绑定和类型转换

9.1 简介


JSR-303/JSR-349 Bean Validation

Spring Framework 4.0在设置支持方面支持Bean Validation 1.0(JSR-303)和Bean Validation 1.1(JSR-349),并使其适应Spring的 Validator 接口。

应用程序可以选择全局启用Bean Validation一次,如 Section 9.8, “Spring Validation” 中所述,并专门用于所有验证需求。

应用程序还可以为 DataBinder 实例注册其他Spring Validator 实例,如 Section 9.8.3, “Configuring a DataBinder” 中所述。这可以用于在不使用注释的情况下插入验证逻辑。


将验证视为业务逻辑有利有弊,而Spring提供的验证(和数据绑定)设计不排除其中任何一个。具体的验证不应该与Web层绑定,应该易于本地化,并且应该可以插入任何可用的验证器。考虑到上述情况,Spring提出了一个 Validator 接口,它在应用程序的每一层都是基本的,非常实用的。

数据绑定对于允许用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。 Spring提供了所谓的 DataBinder 来做到这一点。 ValidatorDataBinder 组成了 validation 包,主要用于但不限于MVC框架。

BeanWrapper 是Spring Framework中的一个基本概念,并且在很多地方都使用过。但是,您可能不需要直接使用 BeanWrapper 。因为这是参考文献,我们觉得可能有些解释。我们将在本章中解释 BeanWrapper ,因为如果你要使用它,你很可能会在尝试将数据绑定到对象时这样做。

Spring的DataBinder和较低级别的BeanWrapper都使用PropertyEditors来解析和格式化属性值。 PropertyEditor 概念是JavaBeans规范的一部分,本章也对此进行了解释。 Spring 3引入了一个 "core.convert" 包,它提供了一个通用的类型转换工具,以及一个用于格式化UI字段值的更高级别的 "format" 包。这些新包可以用作PropertyEditors的更简单的替代方法,本章也将对此进行讨论。

9.2 使用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 Framework提供的 ValidationUtils 帮助器类时。

public class PersonValidator implements Validator {

    /**
     * This Validator validates *just* 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 javadocs,除了前面的例子之外,看看它提供了什么功能。

虽然可以实现单个 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/> 标记来检查错误消息,当然您也可以自己检查错误对象。有关它提供的方法的更多信息可以在javadocs中找到。

9.3 解析错误消息的代码

我们已经讨论了数据绑定和验证。输出与验证错误相对应的消息是我们需要讨论的最后一件事。在我们上面显示的示例中,我们拒绝了 nameage 字段。如果我们要使用 MessageSource 输出错误消息,我们将使用我们在给出的错误代码时这样做拒绝该字段(在这种情况下,'name'和'age')。当你从 Errors 接口调用(直接或间接地,使用例如 ValidationUtils 类) rejectValue 或其他 reject 方法之一时,底层实现不仅会注册你传入的代码,还会注册一些其他错误代码。它注册的错误代码由使用的 MessageCodesResolver 确定。默认情况下,使用 DefaultMessageCodesResolver ,例如,它不仅使用您提供的代码注册消息,还包含传递给reject方法的字段名称的消息。因此,如果您拒绝使用 rejectValue("age", "too.darn.old") 的字段,除了 too.darn.old 代码之外,Spring还会注册 too.darn.old.agetoo.darn.old.age.int (因此第一个将包含字段名称,第二个将包括字段的类型);这样做是为了方便开发人员定位错误消息等。

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

9.4 Bean操作和BeanWrapper

org.springframework.beans 包遵循Oracle提供的JavaBeans标准。 JavaBean只是一个具有默认无参数构造函数的类,它遵循命名约定,其中(作为示例)名为 bingoMadness 的属性将具有setter方法 setBingoMadness(..) 和getter方法 getBingoMadness() 。有关JavaBeans和规范的更多信息,请参阅Oracle的网站( javabeans )。

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

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

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

设置和获取属性是使用 setPropertyValue(s)getPropertyValue(s) 方法完成的,这些方法都带有几个重载变体。它们都在Spring附带的javadocs中有更详细的描述。重要的是要知道有一些用于指示对象属性的约定。几个例子:

Table 9.1. Examples of properties

表达式说明
name表示对应于方法 getName()isName()setName(..)的属性 name
account.name表示属性 account 的对应的属性 name ,例如方法 getAccount().setName()getAccount().getName()
account[2]表示索引属性 account 的第三个元素。索引属性可以是 arraylist 或其他自然排序的集合
account[COMPANYNAME]表示由Map属性的键COMPANYIGN索引的映射条目的值 account

下面你会找到一些使用 BeanWrapper 来获取和设置属性的例子。

(如果您不打算直接使用 BeanWrapper ,那么下一部分对您来说并不重要。如果您只是使用 DataBinderBeanFactory 及其开箱即用的实现,您应该跳到关于 PropertyEditors 的部分。)

考虑以下两个类:

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;
    }
}

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

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

9.4.2 内置PropertyEditor实现

Spring使用 PropertyEditors 的概念来实现 ObjectString 之间的转换。如果您考虑一下,有时可能会以与对象本身不同的方式表示属性。例如, Date 可以用人类可读的方式表示(如 String '2007-14-09' ),而我们仍然能够将人类可读的表格转换回原始日期(甚至更好:转换人类可读的任何日期)形式,回到 Date 对象)。可以通过注册 java.beans.PropertyEditor 类型的自定义编辑器来实现此行为。在 BeanWrapper 上注册自定义编辑器,或者在前一章中提到的特定IoC容器中注册,让它了解如何将属性转换为所需的类型。在Oracle提供的 java.beans 包的javadocs中阅读有关 PropertyEditors 的更多信息。

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

bean上的

  • 设置属性是使用 PropertyEditors 完成的。当提到 java.lang.String 作为您在XML文件中声明的某个bean的属性的值时,Spring将(如果相应属性的setter具有 Class -parameter)使用 ClassEditor 来尝试将参数解析为 Class 对象。

  • 解析Spring的MVC框架中的HTTP请求参数是使用 PropertyEditors 完成的,您可以在 CommandController 的所有子类中手动绑定。

Spring有许多内置的 PropertyEditors 让生活变得轻松。其中每个都列在下面,它们都位于 org.springframework.beans.propertyeditors 包中。大多数(但不是全部)(如下所示)默认情况下由 BeanWrapperImpl 注册。如果属性编辑器可以某种方式配置,您当然可以注册自己的变体来覆盖默认变量:

Table 9.2. Built-in PropertyEditors

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

Spring使用 java.beans.PropertyEditorManager 为可能需要的属性编辑器设置搜索路径。搜索路径还包括 sun.bean.editors ,其中包括 FontColor 等类型的 PropertyEditor 实现,以及大多数基本类型。另请注意,标准JavaBeans基础结构将自动发现 PropertyEditor 类(无需显式注册它们),如果它们与它们处理的类位于同一个包中,并且与该类具有相同的名称,并附加 'Editor' ;例如,可以使用以下类和包结构,这足以使 FooEditor 类被识别并用作 PropertyEditor 用于 Foo -typed属性。

com
  chank
    pop
      Foo
      FooEditor // the PropertyEditor for the Foo class

请注意,您也可以在此处使用标准的 BeanInfo JavaBeans机制(描述为 in not-amazing-detail here )。下面是使用 BeanInfo 机制显式注册一个或多个 PropertyEditor 实例以及相关类的属性的示例。

com
  chank
    pop
      Foo
      FooBeanInfo // the BeanInfo for the Foo class

以下是引用的 FooBeanInfo 类的Java源代码。这会将 CustomNumberEditorFoo 类的 age 属性相关联。

public class FooBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}

注册其他自定义PropertyEditors

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

如果需要注册其他自定义 PropertyEditors ,则有几种可用的机制。假设您有一个 BeanFactory 参考,那么通常不方便或不推荐的最常用的方法是简单地使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法。另一个稍微方便的机制是使用一个名为 CustomEditorConfigurer 的特殊bean工厂后处理器。尽管bean工厂后处理器可以与 BeanFactory 实现一起使用,但 CustomEditorConfigurer 具有嵌套属性设置,因此强烈建议它与 ApplicationContext 一起使用,它可以以类似于任何其他bean的方式部署,并自动检测到并应用。

请注意,所有bean工厂和应用程序上下文都会自动使用许多内置属性编辑器,通过使用名为 BeanWrapper 的东西来处理属性转换。 BeanWrapper 中列出 BeanWrapper 寄存器的标准属性编辑器。此外, ApplicationContexts 还会覆盖或添加其他数量的编辑器,以适合特定应用程序上下文类型的方式处理资源查找。

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

考虑一个用户类 ExoticType ,另一个需要 ExoticType 设置为属性的类 DependsOnExoticType

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 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()));
    }
}

最后,我们使用 CustomEditorConfigurerApplicationContext 注册新的 PropertyEditor ,然后可以根据需要使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>
使用PropertyEditorRegistrars

使用Spring容器注册属性编辑器的另一种机制是创建和使用 PropertyEditorRegistrar 。当您需要在几种不同的情况下使用同一组属性编辑器时,此接口特别有用:编写相应的注册器并在每种情况下重用它。 PropertyEditorRegistrars 与名为 PropertyEditorRegistry 的接口一起工作,该接口由Spring BeanWrapper (和 DataBinder )实现。 PropertyEditorRegistrarsCustomEditorConfigurer (介绍 here )一起使用时特别方便,它公开了一个名为_674941的属性: PropertyEditorRegistrars 以这种方式添加到 CustomEditorConfigurer 可以很容易地与 DataBinder 和Spring MVC Controllers 共享。此外,它避免了在自定义编辑器上进行同步的需要: PropertyEditorRegistrar 应该为每个bean创建尝试创建新的 PropertyEditor 实例。

使用 PropertyEditorRegistrar 可能最好用一个例子来说明。首先,您需要创建自己的 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...
    }
}

有关示例 PropertyEditorRegistrar 实现,另请参见 org.springframework.beans.support.ResourceEditorRegistrar 。请注意,在实现 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’s MVC web framework 的人来说,使用 PropertyEditorRegistrars 与数据绑定 Controllers (如 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 之间共享。

9.5 spring 类型转换

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

9.5.1 转换器SPI

用于实现类型转换逻辑的SPI简单且强类型化:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

要创建自己的转换器,只需实现该接口即可以上。参数化 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);
    }
}

9.5.2 ConverterFactory

当您需要集中整个类层次结构的转换逻辑时,例如,从String转换为java.lang.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的子类。

StringToEnum ConverterFactory为例:

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());
        }
    }
}

9.5.3 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 Array和Collection之间进行转换的转换器。这样的ArrayToCollectionConverter会对声明目标Collection类型的字段进行内省,以解析Collection的元素类型。这允许在目标字段上设置Collection之前,将源数组中的每个元素转换为Collection元素类型。

因为GenericConverter是一个更复杂的SPI接口,所以只在需要时使用它。喜欢Converter或ConverterFactory以满足基本的类型转换需求。

ConditionalGenericConverter

有时,如果特定条件成立,您只需要 Converter 才能执行。例如,如果目标字段上存在特定注释,则可能只想执行 Converter 。或者,如果在目标类上定义了特定方法(例如 static valueOf 方法),则可能只想执行 ConverterConditionalGenericConverterGenericConverterConditionalConverter 接口的并集,允许您定义此类自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

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

9.5.4 ConversionService API

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

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

大多数ConversionService实现还实现 ConverterRegistry ,它提供用于注册转换器的SPI。在内部,ConversionService实现委托其注册的转换器执行类型转换逻辑。

core.convert.support 包中提供了强大的ConversionService实现。 GenericConversionService 是适用于大多数环境的通用实现。 ConversionServiceFactory 提供了一个方便的工厂,用于创建常见的ConversionService配置。

9.5.5 配置ConversionService

ConversionService是一个无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在Spring应用程序中,通常为每个Spring容器(或ApplicationContext)配置一个ConversionService实例。那个ConversionService将被Spring选中,然后在框架需要执行类型转换时使用。您也可以将此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章节中的 Section 22.16.3, “Conversion and Formatting”

在某些情况下,您可能希望在转换期间应用格式。有关使用 FormattingConversionServiceFactoryBean 的详细信息,请参阅 Section 9.6.3, “FormatterRegistry SPI”

9.5.6 以编程方式使用ConversionService

要以编程方式使用ConversionService实例,只需像对任何其他bean一样注入对它的引用:

@Service
public class MyService {

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

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

对于大多数用例,可以使用指定targetType的 convert 方法,但它不适用于更复杂的类型,例如参数化元素的集合。例如,如果要以编程方式将 ListInteger 转换为 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 会自动注册适合大多数环境的转换器。这包括收集转换器,标量转换器以及基本的 ObjectString 转换器。可以使用 DefaultConversionService 类上的静态 addDefaultConverters 方法向任何 ConverterRegistry 注册相同的转换器。

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

9.6 Spring字段格式

如前一节所述, core.convert 是一种通用类型转换系统。它提供了统一的ConversionService API以及强类型转换器SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring Container使用此系统绑定bean属性值。此外,Spring Expression Language(SpEL)和DataBinder都使用此系统绑定字段值。例如,当SpEL需要强制 ShortLong 以完成 expression.setValue(Object bean, Object value) 尝试时,core.convert系统会执行强制。

现在考虑典型客户端环境(如Web或桌面应用程序)的类型转换要求。在这样的环境中,您通常从String转换为支持客户端回发过程,以及返回String以支持视图呈现过程。此外,您经常需要本地化String值。更通用的core.convert Converter SPI无法直接满足此类格式要求。为了直接解决这些问题,Spring 3引入了一个方便的Formatter SPI,为客户端环境提供了一个简单而强大的PropertyEditors替代方案。

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

9.6.1 格式化SPI

用于实现字段格式化逻辑的Formatter SPI简单且强类型化:

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 包使用 java.text.NumberFormat 提供 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter 来格式化 java.lang.Number 对象。 datetime 包提供 DateFormatter 格式 java.util.Date 对象 java.text.DateFormatdatetime.joda 包基于 Joda-Time library 提供全面的日期时间格式支持。

考虑 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 贡献;见 jira.spring.io 来有助于。

9.6.2 注释驱动的格式

如您所见,字段格式可以通过字段类型或注释进行配置。要将Annotation绑定到格式化程序,请实现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() 返回打印机以打印带注释字段的值。让 getParser() 返回一个Parser来解析带注释字段的clientValue。

下面的示例AnnotationFormatterFactory实现将@NumberFormat Annotation绑定到格式化程序。此注释允许指定数字样式或模式:

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格式化java.lang.Number字段。使用@DateTimeFormat格式化java.util.Date,java.util.Calendar,java.util.Long或Joda-Time字段。

下面的示例使用@DateTimeFormat将java.util.Date格式化为ISO Date(yyyy-MM-dd):

public class MyModel {

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

9.6.3 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 addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Formatter<?> formatter);

    void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}

如上所示,可以通过fieldType或注释注册Formatters。

FormatterRegistry SPI允许您集中配置格式规则,而不是在控制器之间复制此类配置。例如,您可能希望强制所有Date字段以某种方式格式化,或者具有特定注释的字段以某种方式格式化。使用共享的FormatterRegistry,您可以定义一次这些规则,并在需要格式化时应用它们。

9.6.4 FormatterRegistrar SPI

FormatterRegistrar是一个SPI,用于通过FormatterRegistry注册格式化程序和转换器:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

在为给定格式类别注册多个相关转换器和格式化程序时,FormatterRegistrar非常有用,例如日期格式。在声明性注册不充分的情况下,它也很有用。例如,格式化程序需要在与其自己的不同的特定字段类型下或在注册打印机/分析程序对时编制索引。下一节提供有关转换器和格式化程序注册的更多信息。

9.6.5 在Spring MVC中配置格式

请参阅Spring MVC章节中的 Section 22.16.3, “Conversion and Formatting”

9.7 配置全局日期和时间格式

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

您需要确保Spring不会注册默认格式化程序,而是应该手动注册所有格式化程序。根据您是否使用Joda-Time库,使用 org.springframework.format.datetime.joda.JodaTimeFormatterRegistrarorg.springframework.format.datetime.DateFormatterRegistrar 类。

例如,以下Java配置将注册全局“yyyyMMdd”格式。此示例不依赖于Joda-Time库:

@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 date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

如果您更喜欢基于XML的配置,则可以使用 FormattingConversionServiceFactoryBean 。这是相同的例子,这次使用Joda Time:

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd>

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

Joda-Time提供单独的不同类型来表示日期,时间和日期时间值。应使用JodaTimeFormatterRegistrar的dateFormatter,timeFormatter和dateTimeFormatter属性为每种类型配置不同的格式。 DateTimeFormatterFactoryBean提供了一种创建格式化程序的便捷方法。

如果您正在使用Spring MVC,请记住明确配置所使用的转换服务。对于基于Java的 @Configuration ,这意味着扩展 WebMvcConfigurationSupport 类并覆盖 mvcConversionService() 方法。对于XML,您应该使用 mvc:annotation-driven 元素的 'conversion-service' 属性。有关详细信息,请参阅 Section 22.16.3, “Conversion and Formatting”

9.8 spring 验证

Spring 3为其验证支持引入了一些增强功能。首先,现在完全支持JSR-303 Bean Validation API。其次,当以编程方式使用时,Spring的DataBinder现在可以验证对象以及绑定它们。第三,Spring MVC现在支持声明性地验证 @Controller 输入。

9.8.1 JSR-303 Bean Validation API概述

JSR-303标准化了Java平台的验证约束声明和元数据。使用此API,您可以使用声明性验证约束来注释域模型属性,并且运行时会强制执行它们。有很多您可以利用的内置约束。您还可以定义自己的自定义约束。

为了说明,请考虑一个具有两个属性的简单PersonForm模型:

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

JSR-303允许您为这些属性定义声明性验证约束:

public class PersonForm {

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

    @Min(0)
    private int age;
}

当JSR-303 Validator验证此类的实例时,将强制执行这些约束。

有关JSR-303 / JSR-349的一般信息,请参阅 Bean Validation website 。有关默认参考实现的特定功能的信息,请参阅 Hibernate Validator 文档。要了解如何将Bean Validation提供程序设置为Spring bean,请继续阅读。

9.8.2 配置Bean验证提供程序

Spring提供对Bean Validation API的完全支持。这包括方便地支持将JSR-303 / JSR-349 Bean Validation提供程序作为Spring bean引导。这允许在应用程序中需要验证的任何地方注入 javax.validation.ValidatorFactoryjavax.validation.Validator

使用 LocalValidatorFactoryBean 将默认Validator配置为Spring bean:

<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

上面的基本配置将触发Bean Validation使用其默认引导机制进行初始化。 JSR-303 / JSR-349提供程序(如Hibernate Validator)预计会出现在类路径中并自动检测。

注入验证器

LocalValidatorFactoryBean 同时实现 javax.validation.ValidatorFactoryjavax.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 Validation约束由两部分组成。首先,声明约束及其可配置属性的 @Constraint 注释。第二,实现约束行为的 javax.validation.ConstraintValidator 接口的实现。要将声明与实现相关联,每个 @Constraint 注释都引用相应的 ConstraintValidator 实现类。在运行时, ConstraintValidatorFactory 在域模型中遇到约束注释时实例化引用的实现。

默认情况下, LocalValidatorFactoryBean 配置使用Spring创建ConstraintValidator实例的 SpringConstraintValidatorFactory 。这允许您的自定义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实现可能与其他任何Spring bean一样具有@Autowired的依赖关系。

spring 驱动的方法验证

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

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

为了有资格进行Spring驱动的方法验证,所有目标类都需要使用Spring的 @Validated 注释进行注释,可以选择声明要使用的验证组。使用Hibernate Validator和Bean Validation 1.1提供程序查看 MethodValidationPostProcessor javadocs以获取设置详细信息。

其他配置选项

对于大多数情况,默认的 LocalValidatorFactoryBean 配置应该足够了。从消息插值到遍历解析,各种Bean Validation构造有许多配置选项。有关这些选项的更多信息,请参见 LocalValidatorFactoryBean javadocs。

9.8.3 配置DataBinder

从Spring 3开始,可以使用Validator配置DataBinder实例。配置完成后,可以通过调用 binder.validate() 来调用Validator。任何验证错误都会自动添加到 Binders 的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.addValidatorsdataBinder.replaceValidators 为多个 Validator 实例配置DataBinder。将全局配置的Bean验证与在DataBinder实例上本地配置的Spring Validator 组合时,这非常有用。见 ???

9.8.4 Spring MVC 3验证

请参阅Spring MVC章节中的 Section 22.16.4, “Validation”

Updated at: 5 months ago
8.7.3. FileSystemResource警告Table of content10. Spring表达式语言(SpEL)
Comment
You are not logged in.

There are no comments.