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

9.1 简介


JSR-303/JSR-349 Bean 验证

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

_ application 可以选择全局启用 Bean Validation 一次,如第 9.8 节,“ Spring 验证”中所述,并专门用于所有验证需求。

_ application 还可以为DataBinder实例注册其他 Spring Validator实例,如部分 9.8.3,“配置数据绑定器”中所述。这可能对于在不使用 annotations 的情况下插入验证逻辑非常有用。


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

Data binding 对于允许用户输入动态绑定到 application 的域 model(或用于 process 用户输入的任何 objects)非常有用。 Spring 提供了 so-called DataBinder来做到这一点。 ValidatorDataBinder组成validation包,主要用于但不限于 MVC framework。

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

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

9.2 使用 Spring 的 Validator 接口进行验证

Spring features 一个Validator接口,可用于验证 objects。 Validator接口使用Errors object 工作,以便在验证时,验证程序可以向Errors object 报告验证失败。

让我们考虑一个小数据 object:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

我们将通过实现org.springframework.validation.Validator接口的以下两种方法来为Person class 提供验证行为:

  • supports(Class) - 这可以Validator验证提供的Class的实例吗?

  • validate(Object, org.springframework.validation.Errors) - 验证给定的 object,如果验证错误,则注册具有给定Errors object 的那些

实现Validator非常简单,尤其是当您知道 Spring Framework 也提供的ValidationUtils帮助 class 时。

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 class 上的static rejectIfEmpty(..)方法用于拒绝'name' property,如果它是null或空 string。看看ValidationUtils javadocs,看看它提供的功能除了前面显示的 example 之外。

虽然可以实现单个Validator class 来验证富 object 中的每个嵌套 objects,但最好将 object 的每个嵌套 class 的验证逻辑封装在它自己的Validator implementation 中。 'rich'object 的简单 example 将是Customer,它由两个String properties(第一个和第二个 name)和一个复杂的Address object 组成。 Address objects 可以独立于Customer objects 使用,因此实现了独特的AddressValidator。如果你希望你的CustomerValidator重用AddressValidator class 中包含的逻辑而不诉诸 copy-and-paste,你可以 dependency-inject 或在你的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 object。如果是 Spring Web MVC,您可以使用<spring:bind/>标签来检查错误消息,当然您也可以自己检查错误 object。有关它提供的方法的更多信息可以在 javadocs 中找到。

9.3 解析错误消息的代码

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

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

9.4 Bean 操作和 BeanWrapper

org.springframework.beans包遵循 Oracle 提供的 JavaBeans 标准。 JavaBean 只是一个具有默认 no-argument 构造函数的 class,它遵循命名约定,其中(通过 example)名为bingoMadness的 property 将具有 setter 方法setBingoMadness(..)和 getter 方法getBingoMadness()。有关 JavaBeans 和规范的更多信息,请参阅 Oracle 的网站(JavaBeans)。

beans 包中一个非常重要的 class 是BeanWrapper接口及其相应的 implementation(BeanWrapperImpl)。从 javadocs 引用,BeanWrapper提供了设置和获取 property 值(单独或批量),获取 property 描述符以及查询 properties 以确定它们是可读还是可写的功能。此外,BeanWrapper提供对嵌套 properties 的支持,使 sub-properties 上的 properties 设置为无限深度。然后,BeanWrapper支持添加标准 JavaBeans PropertyChangeListenersVetoableChangeListeners的功能,而无需在目标 class 中支持 code。最后但同样重要的是,BeanWrapper提供了对索引 properties 设置的支持。 BeanWrapper通常不直接由 application code 使用,而是由DataBinderBeanFactory使用。

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

9.4.1 设置并获取基本和嵌套的 properties

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

表格 1_.properties的示例

表达说明
name表示与方法getName()isName()setName(..)对应的 property name
account.name表示 property account的对应 e.g 的嵌套 property name。方法getAccount().setName()getAccount().getName()
account[2]表示索引 property account的第三个元素。索引的 properties 可以是arraylist或其他自然排序的集合
account[COMPANYNAME]表示由 Map property account的 key COMPANYNAME 索引的 map 条目的 value

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

(如果您不打算直接使用BeanWrapper,那么下一部分对您来说并不重要.如果您只是使用DataBinderBeanFactory以及它们的 out-of-the-box implementation,您应该跳到关于PropertyEditors .)的部分

考虑以下两个 classes:

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

以下 code 片段显示了一些如何检索和操作实例化的CompaniesEmployees的 properties 的示例:

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 Built-in PropertyEditor implementations

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

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

  • 在 beans 上设置 properties 是使用PropertyEditors完成的。当提到java.lang.String作为某些 bean 的 property 的 value 你在 XML 文件中声明时,Spring 会(如果相应的 property 的 setter 有一个Class -parameter)使用ClassEditor来尝试将参数解析为Class object。

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

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

表格 1_.Built-in PropertyEditors

说明
ByteArrayPropertyEditor字节数组的编辑器。 Strings 将简单地转换为其对应的字节表示。默认情况下由BeanWrapperImpl注册。
ClassEditor解析 Strings 将 classes 表示为实际的 classes,反之亦然。如果找不到 class,则抛出IllegalArgumentException。默认情况下由BeanWrapperImpl注册。
CustomBooleanEditorBoolean properties 的可自定义 property 编辑器。默认情况下由BeanWrapperImpl注册,但是,可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor集合的 Property 编辑器,将任何源Collection转换为给定的目标Collection类型。
CustomDateEditorjava.util.Date 的可自定义 property 编辑器,支持自定义 DateFormat。没有默认注册。必须根据需要以适当的格式注册用户。
CustomNumberEditor可自定义的 property 编辑器,适用于任何 Number 子类,如IntegerLongFloatDouble。默认情况下由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor能够将 Strings 解析为java.io.File objects。默认情况下由BeanWrapperImpl注册。
InputStreamEditorOne-way property 编辑器,能够将文本 string 和 producing(通过中间ResourceEditorResource)一个InputStream,所以InputStream properties 可以直接设置为 Strings。请注意,默认用法不会为您关闭InputStream!默认情况下由BeanWrapperImpl注册。
LocaleEditor能够将 Strings 解析为Locale objects,反之亦然(String 格式为[178] [179],这与 Locale 提供的 toString()方法相同)。默认情况下由BeanWrapperImpl注册。
PatternEditor能够将 Strings 解析为java.util.regex.Pattern objects,反之亦然。
PropertiesEditor能够将 Strings(使用java.util.Properties class 的 javadoc 中定义的格式进行格式化)转换为Properties objects。默认情况下由BeanWrapperImpl注册。
StringTrimmerEditor修剪 Strings 的 Property 编辑器。 (可选)允许将空 string 转换为null value。未默认注册;必须由用户注册。
URLEditor能够将 URL 的 String 表示解析为实际的URL object。默认情况下由BeanWrapperImpl注册。

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

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

请注意,您也可以在此处使用标准的BeanInfo JavaBeans 机制(描述为在 not-amazing-detail 这里)。在下面找到一个使用BeanInfo机制的示例,用于使用关联的 class 的 properties 显式注册一个或多个PropertyEditor实例。

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

这是引用的FooBeanInfo class 的 Java source code。这会将CustomNumberEditorFoo class 的age property 相关联。

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 properties 设置为 string value 时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditors将这些 Strings 转换为 property 的复杂类型。 Spring pre-registers 一些自定义PropertyEditors(对于 example,将表示为 string 的类名转换为真正的Class object)。此外,Java 的标准 JavaBeans PropertyEditor查找机制允许只为 class 被恰当地命名,并放置在与它提供支持的 class 相同的包中,以便自动找到。

如果需要注册其他自定义PropertyEditors,可以使用多种机制。假设您有一个BeanFactory reference,最简单的方法是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,这种方法通常不方便或不推荐。另一个稍微方便的机制是使用一个特殊的 bean 工厂 post-processor 调用CustomEditorConfigurer。虽然 bean factory post-processors 可以与BeanFactory __mplement 使用,但是CustomEditorConfigurer有一个嵌套的 property 设置,所以强烈建议它与ApplicationContext一起使用,它可以以类似于任何其他 bean 的方式部署,并自动检测和应用。

请注意,所有 bean 工厂和 application 上下文都会自动使用多个 built-in property 编辑器,通过使用名为BeanWrapper的东西来处理 property 转换。 BeanWrapper寄存器列在上一节中的标准 property 编辑器。此外,ApplicationContexts还会覆盖或添加额外数量的编辑器,以适合特定 application context 类型的方式处理资源查找。

标准 JavaBeans PropertyEditor实例用于将表示为 strings 的 property 值转换为 property 的实际复杂类型。 CustomEditorConfigurer,bean factory post-processor,可用于方便地将其他PropertyEditor实例的支持添加到ApplicationContext

考虑用户 class ExoticType和另一个需要ExoticType设置为 property 的 class 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;
    }
}

正确设置后,我们希望能够将 property 类型指定为 string,PropertyEditor将在后台转换为实际的ExoticType实例:

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

PropertyEditor implementation 看起来与此类似:

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

使用PropertyEditorRegistrar可能最好用 example 说明。首先,您需要创建自己的PropertyEditorRegistrar implementation:

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以了解 example PropertyEditorRegistrar implementation。注意它在registerCustomEditors(..)方法的 implementation 中如何创建每个 property 编辑器的新实例。

接下来我们配置一个CustomEditorConfigurer和 inject 一个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 framework的人来说,将PropertyEditorRegistrars与 data-binding Controllers(如SimpleFormController)结合起来可以非常方便。在initBinder(..)方法的 implementation 中查找使用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注册方式可以导致简洁的 code(initBinder(..)的实现只是一个 line long!),并允许 common PropertyEditor registration code 封装在 class 中,然后根据需要在Controllers之间共享。

9.5 弹簧类型转换

Spring 3 引入了一个core.convert包,它提供了一般的类型转换系统。系统定义了一个 SPI 来实现类型转换逻辑,以及一个在运行时执行类型转换的 API。在 Spring 容器中,此系统可用作 PropertyEditors 的替代方法,以将外部化的 bean property value strings 转换为所需的 property 类型。公共 API 也可以在您需要进行类型转换的 application 中的任何位置使用。

9.5.1 转换器 SPI

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

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

要创建自己的转换器,只需实现上面的接口即可。参数化S作为要转换的类型,T作为要转换的类型。如果S的集合或 array 需要转换为_ar或T的集合,也可以透明地应用这样的转换器,前提是委托 array/collection 转换器也已注册(默认情况下DefaultConversionService)。

对于每次调用convert(S),源参数都保证为非 null。如果转换失败,您的 Converter 可能会抛出任何未经检查的 exception;具体而言,应该抛出IllegalArgumentException来报告无效的源 value。注意确保Converter implementation 是 thread-safe。

为方便起见,core.convert.support包中提供了几个转换器 implementation。这些包括从 Strings 到 Numbers 和其他 common 类型的转换器。将StringToInteger视为典型Converter 实现的 example:

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

当您需要集中整个 class 层次结构的转换逻辑时,对于 example,当从 String 转换为 java.lang.Enum objects 时,实现ConverterFactory

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

参数化 S 为要转换的类型,R 为定义可转换为 classes 范围的基本类型。然后实现 getConverter(Class <T>),其中 T 是 R 的子类。

StringToEnum ConverterFactory 视为 example:

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 implementation 时,请考虑 GenericConverter 接口。通过更灵活但不太强类型的签名,GenericConverter 支持在多个源类型和目标类型之间进行转换。此外,GenericConverter 提供了在实现转换逻辑时可以使用的源和目标字段 context。这样的 context 允许类型转换由字段 annotation 或在字段签名上声明的通用信息驱动。

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

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

要实现 GenericConverter,请return 支持的源→目标类型对。然后实现 convert(Object,TypeDescriptor,TypeDescriptor)来实现转换逻辑。源 TypeDescriptor 提供对持有要转换的 value 的源字段的访问。目标 TypeDescriptor 提供对目标字段的访问,其中将设置转换的 value。

GenericConverter 的一个很好的例子是在 Java Array 和 Collection 之间进行转换的转换器。这样的 ArrayToCollectionConverter 会对声明目标 Collection 类型的字段进行内省,以解析 Collection 的元素类型。这允许在目标字段上设置 Collection 之前,将 source array 中的每个元素转换为 Collection 元素类型。

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

ConditionalGenericConverter

有时,如果特定条件包含 true,您只需要执行Converter。对于 example,如果目标字段上存在特定的 annotation,则可能只想执行Converter。或者,如果在目标 class 上定义了特定方法(例如static valueOf方法),则可能只想执行ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的 union,允许您定义此类自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter的一个好示例是 EntityConverter,它在持久实体标识符和实体 reference 之间进行转换。如果目标实体类型声明静态查找程序方法 e.g,则此类 EntityConverter 可能只会 match。 findAccount(Long)。您将在matches(TypeDescriptor, TypeDescriptor)的 implementation 中执行这样的 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 implementations 也实现了ConverterRegistry,它提供了一个用于注册转换器的 SPI。在内部,ConversionService implementation 委托其注册的转换器执行类型转换逻辑。

core.convert.support包中提供了强大的 ConversionService implementation。 GenericConversionService是 general-purpose implementation 适合在大多数环境中使用。 ConversionServiceFactory为 creating common ConversionService 配置提供了方便的工厂。

9.5.5 配置 ConversionService

ConversionService 是一个 stateless object,旨在在 application 启动时实例化,然后在多个线程之间共享。在 Spring application 中,通常为每个 Spring 容器(或 ApplicationContext)配置一个 ConversionService 实例。转换服务将由 Spring 选取,然后在 framework 需要执行类型转换时使用。您也可以将此 ConversionService 注入任何 beans 并直接调用它。

如果没有向 Spring 注册 ConversionService,则使用原始的 PropertyEditor-based 系统。

要使用 Spring 注册默认 ConversionService,请使用 id conversionService添加以下 bean 定义:

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

默认的 ConversionService 可以在 strings,numbers,enums,collections,maps 和其他 common 类型之间进行转换。要使用您自己的自定义 converter(s 补充或覆盖默认转换器,请设置converters property。 Property 值可以实现 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 application 中使用 ConversionService 也是常见的。请参阅 Spring MVC 章节中的第 22.16.3 节,“转换和格式化”

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

9.5.6 以编程方式使用 ConversionService

要以编程方式使用 ConversionService 实例,只需像对待任何其他 bean 那样 inject 一个 reference:

@Service
public class MyService {

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

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

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

幸运的是,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 class 上的静态addDefaultConverters方法向任何ConverterRegistry注册相同的转换器。

value 类型的转换器将重用于数组和集合,因此无需创建特定的转换器以将Collection S转换为Collection T,假设标准集合处理是合适的。

9.6 Spring 字段格式

如前一节所述,core.convert是 general-purpose 类型转换系统。它提供了统一的 ConversionService API 以及 strongly-typed Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring Container 使用此系统绑定 bean property 值。此外,Spring 表达式语言(SpEL)和 DataBinder 都使用此系统来绑定字段值。例如,当 SpEL 需要强制ShortLong以完成expression.setValue(Object bean, Object value)尝试时,core.convert 系统执行强制。

现在考虑典型 client 环境的类型转换要求,例如 web 或 desktop application。在这样的环境中,您通常从 String 转换为支持 client postback process,以及返回 String 以支持视图呈现 process。此外,您经常需要本地化 String 值。更通用的 core.convert Converter SPI 不直接解决此类格式化要求。为了直接解决它们,Spring 3 引入了一个方便的 Formatter SPI,为 client 环境提供了一个简单而强大的 PropertyEditors 替代方案。

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

9.6.1 Formatter SPI

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

package org.springframework.format;

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

Formatter 从 Printer 和 Parser building-block 接口扩展的位置:

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 为您要格式化的 object 的类型,对于 example,java.util.Date。实现print()操作以打印 T 实例以在 client locale 中显示。实现parse()操作以从 client locale 返回的格式化表示中解析 T 的实例。如果解析尝试失败,您的 Formatter 应抛出 ParseException 或 IllegalArgumentException。注意确保您的 Formatter implementation 为 thread-safe。

为方便起见,在format子包中提供了几个 Formatter implementations。 number包使用java.text.NumberFormat提供NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter来格式化java.lang.Number objects。 datetime包提供DateFormatter格式java.util.Date objects 与java.text.DateFormatdatetime.joda包基于Joda-Time library提供全面的 datetime 格式支持。

DateFormatter视为 example Formatter implementation:

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 团队欢迎 community-driven Formatter贡献;见jira.spring.io贡献。

9.6.2 Annotation-driven 格式化

如您所见,字段格式可以按字段类型或 annotation 配置。要将 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,用于 example org.springframework.format.annotation.DateTimeFormatgetFieldTypes() return 可以使用 annotation 的字段类型。 getPrinter() return 打印机打印带注释字段的 value。让getParser() return 一个 Parser 来解析一个带注释字段的 clientValue。

下面的 example AnnotationFormatterFactory implementation 将 @NumberFormat Annotation 绑定到格式化程序。此 annotation 允许指定数字样式或 pattern:

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

格式 Annotation API

org.springframework.format.annotation包中存在可移植格式 annotation API。使用 @NumberFormat 格式化 java.lang.Number 字段。使用 @DateTimeFormat 格式化 java.util.Date,java.util.Calendar,java.util.Long 或 Joda-Time 字段。

下面的 example 使用 @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 的 implementation。这个 implementation 可以使用FormattingConversionServiceFactoryBean以编程方式或声明方式配置为 Spring bean。因为这个 implementation 还实现了ConversionService,所以它可以直接配置为与 Spring 的 DataBinder 和 Spring 表达式语言(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 或 annotation 注册 Formatters。

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

9.6.4 FormatterRegistrar SPI

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

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

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

9.6.5 在 Spring MVC 中配置格式

请参阅 Spring MVC 章节中的第 22.16.3 节,“转换和格式化”

9.7 配置 global date&time 格式

默认情况下,未使用@DateTimeFormat注释的 date 和 time 字段使用DateFormat.SHORT样式从 strings 转换。如果您愿意,可以通过定义自己的 global 格式来更改此设置。

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

例如,以下 Java configuration 将注册 global“yyyyMMdd”格式。此 example 不依赖于 Joda-Time library:

@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 的 configuration,则可以使用FormattingConversionServiceFactoryBean。这是相同的 example,这个 time 使用 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 提供单独的不同类型来表示datetimedate-time值。应使用JodaTimeFormatterRegistrardateFormattertimeFormatterdateTimeFormatter properties 为每种类型配置不同的格式。 DateTimeFormatterFactoryBean提供了一种创建格式化程序的便捷方法。

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

9.8 Spring 验证

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

9.8.1 JSR-303 Bean Validation API 概述

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

为了说明,考虑一个带有两个 properties 的简单 PersonForm model:

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

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

public class PersonForm {

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

    @Min(0)
    private int age;
}

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

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

9.8.2 配置 Bean 验证提供程序

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

使用LocalValidatorFactoryBean将默认验证程序配置为 Spring bean:

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

上面的基本 configuration 将触发 Bean Validation 使用其默认引导机制进行初始化。 提供程序(例如 Hibernate Validator)应该出现在 classpath 中,并将自动检测到。

注入验证器

LocalValidatorFactoryBean同时实现javax.validation.ValidatorFactoryjavax.validation.Validator,以及 Spring 的org.springframework.validation.Validator。您可以 inject 这些接口中的任何一个 reference 到 beans 需要调用验证逻辑。

如果您希望直接使用 Bean Validation API,请将 reference 注入javax.validation.Validator

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

如果 bean 需要 Spring 验证 API,则将 reference 注入org.springframework.validation.Validator

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

配置自定义约束

每个 Bean 验证约束由两部分组成。首先,声明约束的@Constraint annotation 及其可配置的 properties。第二,实现约束行为的javax.validation.ConstraintValidator接口的 implementation。要将声明与 implementation 相关联,每个@Constraint annotation references 会引用相应的ConstraintValidator implementation class。在运行时,在域 model 中遇到约束 annotation 时实例化引用的 implementation。

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

下面显示的是自定义@Constraint声明的示例,后跟关联的ConstraintValidator implementation,它使用 Spring 进行依赖注入:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    ...
}

正如您所看到的,ConstraintValidator implementation 可能具有与其他 Spring bean 一样的依赖关系 @Autowired 。

Spring-driven 方法验证

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

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

在 order 有资格进行 Spring-driven 方法验证时,所有目标 classes 都需要使用 Spring 的@Validated annotation 注释,可选地声明要使用的验证组。使用 Hibernate Validator 和 Bean Validation 1.1 providers 查看MethodValidationPostProcessor javadocs 以获取设置详细信息。

其他 Configuration 选项

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

9.8.3 配置 DataBinder

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

以编程方式使用 DataBinder 时,可以在 binding 到目标 object 之后调用验证逻辑:

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 还可以通过dataBinder.addValidatorsdataBinder.replaceValidators配置多个Validator实例。将全局配置的 Bean 验证与在 DataBinder 实例上本地配置的 Spring Validator组合时,这非常有用。见???

9.8.4 Spring MVC 3 验证

请参阅 Spring MVC 章节中的第 22.16.4 节,“验证”

Updated at: 7 months ago
8.7.3. FileSystemResource 需要注意Table of content10. Spring 表达语言(SpEL)