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

9.1 Introduction

JSR-303/JSR-349 Bean Validation

就设置支持而言,Spring Framework 4.0 支持 Bean 验证 1.0(JSR-303)和 Bean 验证 1.1(JSR-349),还使其适应 Spring 的Validator接口。

应用程序可以选择一次全局启用 Bean 验证(如第 9.8 节“Spring 验证”中所述),并将其专用于所有验证需求。

应用程序还可以为每个DataBinder实例注册其他 Spring Validator实例,如第 9.8.3 节“配置 DataBinder”中所述。这对于在不使用 Comments 的情况下插入验证逻辑可能很有用。

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

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

BeanWrapper是 Spring 框架中的一个基本概念,在很多地方都使用过。但是,您可能不需要直接使用BeanWrapper。但是,由于这是参考文档,因此我们认为可能需要进行一些解释。我们将在本章中对BeanWrapper进行解释,因为如果您要使用它,则在尝试将数据绑定到对象时最有可能使用它。

Spring 的 DataBinder 和较低级别的 BeanWrapper 都使用 PropertyEditor 来解析和格式化属性值。 PropertyEditor概念是 JavaBeans 规范的一部分,本章还将对此进行说明。 Spring 3 引入了一个“ core.convert”包,该包提供了常规的类型转换工具,以及一个用于格式化 UI 字段值的高级“ format”包。这些新软件包可以用作 PropertyEditor 的更简单替代方案,并且还将在本章中进行讨论。

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 框架还提供的ValidationUtils helper 类时。

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实现中。 *'rich'*对象的一个简单示例是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的 javadocs 中在线找到。

9.4 Bean 操作和 BeanWrapper

org.springframework.beans软件包遵循 Oracle 提供的 JavaBeans 标准。 JavaBean 只是具有默认无参数构造函数的类,该类遵循命名约定,在命名约定中,(举例来说)名为bingoMadness的属性将具有 setter 方法setBingoMadness(..)和 getter 方法getBingoMadness()。有关 JavaBean 和规范的更多信息,请访问 Oracle 网站(javabeans)。

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

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

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

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

表 9.1. 属性示例

ExpressionExplanation
name表示与方法getName()isName()setName(..)对应的属性name
account.name表示对应于例如的属性account的嵌套属性name。到方法getAccount().setName()getAccount().getName()
account[2]指示索引属性account的* third 元素。索引属性可以是arraylist类型或其他自然排序的*集合
account[COMPANYNAME]表示由 Map 属性account的键* COMPANYNAME *索引的 Map 条目的值

以下是使用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'),而我们仍然能够将人类可读的形式转换回原始日期(甚至更好:将人类可读的形式 Importing 的任何日期转换为原始日期)形式,返回到Date个对象)。此行为可以通过注册java.beans.PropertyEditor类型的自定义编辑器来实现。如上一章所述,在BeanWrapper上或在特定的 IoC 容器中注册自定义编辑器,使他们了解如何将属性转换为所需的类型。在 Oracle 提供的java.beans软件包的 javadocs 中阅读有关PropertyEditors的更多信息。

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

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

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

Spring 具有许多内置的PropertyEditors来简化生活。下面列出了每个选项,它们都位于org.springframework.beans.propertyeditors包中。默认情况下,大多数(但不是全部)(如下所示)由BeanWrapperImpl注册。如果可以通过某种方式配置属性编辑器,那么您当然仍然可以注册自己的变量以覆盖默认变量:

表 9.2. 内置 PropertyEditor

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

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

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

请注意,您也可以在此处使用标准的BeanInfo JavaBeans 机制(描述为在这里没有惊人的细节)。在下面查找使用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());
        }
    }
}

注册其他自定义 PropertyEditor

当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditors将这些 String 转换为属性的复杂类型。 Spring 预注册了许多自定义PropertyEditors(例如,将表示为字符串的类名转换为实际的Class对象)。另外,Java 的标准 JavaBeans PropertyEditor查找机制允许为一个类的PropertyEditor进行适当的命名,并自动将其与它提供支持的类放在同一包中。

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

注意,所有 bean 工厂和应用程序上下文通过使用称为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>
Using PropertyEditorRegistrars

向 Spring 容器注册属性编辑器的另一种机制是创建并使用PropertyEditorRegistrar。当需要在几种不同情况下使用同一组属性编辑器时,此接口特别有用:编写相应的注册器,并在每种情况下重复使用该注册器。 PropertyEditorRegistrars与称为PropertyEditorRegistry的接口配合使用,该接口由 Spring BeanWrapper(和DataBinder)实现。 PropertyEditorRegistrarsCustomEditorConfigurer(引入的here)结合使用时特别方便,它公开了称为setPropertyEditorRegistrars(..)的属性:以这种方式添加到CustomEditorConfigurerPropertyEditorRegistrars可以轻松地与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...
    }
}

另请参见org.springframework.beans.support.ResourceEditorRegistrar以获取示例PropertyEditorRegistrar的实现。请注意,在实现registerCustomEditors(..)方法时,它如何创建每个属性编辑器的新实例。

接下来,我们配置CustomEditorConfigurer并将CustomPropertyEditorRegistrar的实例注入其中:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

<bean id="customPropertyEditorRegistrar"
    class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

最后,与本章重点有所不同的是,对于使用Spring 的 MVC Web 框架的用户,将PropertyEditorRegistrars与数据绑定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.5Spring 类型转换

Spring 3 引入了core.convert软件包,该软件包提供了常规的类型转换系统。系统定义了一个用于实现类型转换逻辑的 SPI,以及一个用于在运行时执行类型转换的 API。在 Spring 容器中,此系统可以用作 PropertyEditor 的替代方案,以将外部化的 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 使您可以在实现转换逻辑时使用可用的源字段和目标字段上下文。这种上下文允许类型转换由字段 Comments 或在字段签名上声明的通用信息驱动。

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

Note

由于 GenericConverter 是更复杂的 SPI 接口,因此仅在需要时才使用它。支持 Converter 或 ConverterFactory 以满足基本的类型转换需求。

ConditionalGenericConverter

有时,您只希望在满足特定条件的情况下执行Converter。例如,如果目标字段上存在特定 Comments,则可能只想执行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,该ConverterRegistry提供了用于注册转换器的 SPI。在内部,ConversionService 实现委派其注册的转换器执行类型转换逻辑。

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

9.5.5 配置 ConversionService

ConversionService 是 Stateless 对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常每个 Spring 容器(或 ApplicationContext)配置一个 ConversionService 实例。 Spring 会选择该 ConversionService,然后在框架需要执行类型转换时使用它。您也可以将此 ConversionService 注入到任何 bean 中,然后直接调用它。

Note

如果未向 Spring 注册任何 ConversionService,则使用原始的基于 PropertyEditor 的系统。

要向 Spring 注册默认的 ConversionService,请添加以下 ID 为conversionService的 bean 定义:

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

默认的 ConversionService 可以在字符串,数字,枚举,集合,Map 和其他常见类型之间进行转换。要使用您自己的自定义转换器来补充或覆盖默认转换器,请设置converters属性。属性值可以实现 Converter,ConverterFactory 或 GenericConverter 接口。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

在 Spring MVC 应用程序中使用 ConversionService 也很常见。请参阅 Spring MVC 章节中的第 22.16.3 节“转换和格式化”

在某些情况下,您可能希望在转换过程中应用格式设置。有关使用FormattingConversionServiceFactoryBean的详细信息,请参见第 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方法,但不适用于更复杂的类型,例如参数化元素的集合。例如,如果要以编程方式将IntegerList转换为StringList,则需要提供源和目标类型的正式定义。

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

值类型的转换器将重新用于数组和集合,因此,无需使用特定的转换器就可以将SCollection转换为TCollection,前提是需要标准的集合处理。

9.6 Spring 字段格式

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

现在考虑典型 Client 端环境(例如 Web 或桌面应用程序)的类型转换要求。在这种环境中,通常会将* from String 转换为支持 Client 端回发过程,以及将 from String 转换为支持视图渲染过程。另外,您通常需要本地化 String 值。更通用的 core.convert 转换器 SPI 不能直接满足此类格式*要求。为了直接解决这些问题,Spring 3 引入了便利的 Formatter SPI,它为 Client 端环境提供了 PropertyEditor 的简单而强大的替代方案。

通常,当需要实现通用类型转换逻辑时,请使用 Converter SPI。例如,用于在 java.util.Date 和和 java.lang.Long 之间进行转换。在 Client 端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,请使用 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 构建块接口扩展的地方:

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 的实例以在 Client 端语言环境中显示。实现parse()操作,以从 Client 端语言环境返回的格式化表示形式解析 T 的实例。如果解析尝试失败,则 Formatter 应该抛出 ParseException 或 IllegalArgumentException。注意确保您的 Formatter 实现是线程安全的。

为了方便起见,在format子包中提供了几种 Formatter 实现。 number包提供NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter来使用java.text.NumberFormat格式化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 注解驱动的格式

正如您将看到的,可以通过字段类型或 Comments 来配置字段格式。要将 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 参数化为您希望与格式逻辑关联的字段注解类型,例如org.springframework.format.annotation.DateTimeFormat。让getFieldTypes()返回可以在其上使用 Comments 的字段类型。让getPrinter()返回打印机以打印带 Comments 的字段的值。让getParser()返回解析器以解析带 Comments 字段的 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();
            }
        }
    }
}

要触发格式,只需使用@NumberFormatComments 字段:

public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}

格式 CommentsAPI

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

下面的示例使用@DateTimeFormat 将 java.util.Date 格式化为 ISO 日期(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 表达式语言(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 或 Comments 注册。

FormatterRegistry SPI 允许您集中配置格式设置规则,而不必在控制器之间复制此类配置。例如,您可能需要强制以某种方式设置所有“日期”字段的格式,或者以某种方式设置带有特定注解的字段的格式。使用共享的 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 章节中的第 22.16.3 节“转换和格式化”

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

默认情况下,未使用@DateTimeFormatComments 的日期和时间字段是使用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>

Note

Joda-Time 提供了单独的不同类型来表示datetimedate-time值。 JodaTimeFormatterRegistrardateFormattertimeFormatterdateTimeFormatter属性应用于为每种类型配置不同的格式。 DateTimeFormatterFactoryBean提供了一种创建格式化程序的便捷方法。

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

9.8Spring 验证

Spring 3 对其验证支持进行了一些增强。首先,现在完全支持 JSR-303 Bean 验证 API。其次,当以编程方式使用时,Spring 的 DataBinder 现在可以验证对象并绑定到它们。第三,Spring MVC 现在支持声明式验证@ControllerImporting。

9.8.1 JSR-303 Bean 验证 API 概述

JSR-303 标准化了 Java 平台的验证约束声明和元数据。使用此 API,您可以使用声明性验证约束来 Comments 域模型属性,然后运行时将其强制执行。您可以利用许多内置约束。您也可以定义自己的自定义约束。

为了说明,请考虑一个具有两个属性的简单 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 验证器验证此类的实例时,将强制执行这些约束。

有关 JSR-303/JSR-349 的一般信息,请参见Bean 验证网站。有关默认参考实现的特定功能的信息,请参见Hibernate Validator文档。要学习如何将 Bean 验证提供程序设置为 Spring Bean,请 continue 阅读。

9.8.2 配置 Bean 验证提供程序

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

使用LocalValidatorFactoryBean将默认的验证器配置为 Spring Bean:

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

上面的基本配置将触发 Bean 验证使用其默认的引导程序机制进行初始化。诸如 Hibernate Validator 之类的 JSR-303/JSR-349 提供程序应该存在于 Classpath 中,并且将被自动检测到。

注入验证器

LocalValidatorFactoryBean实现javax.validation.ValidatorFactoryjavax.validation.Validator以及 Spring 的org.springframework.validation.Validator。您可以将对这些接口之一的引用注入需要调用验证逻辑的 Bean 中。

如果您希望直接使用 Bean 验证 API,请插入对javax.validation.Validator的引用:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

如果您的 bean 需要使用 Spring Validation API,请插入对org.springframework.validation.Validator的引用:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

配置自定义约束

每个 Bean 验证约束均由两部分组成。首先,一个@ConstraintComments 声明了约束及其可配置属性。其次,实现约束的行为的javax.validation.ConstraintValidator接口的实现。为了将声明与实现相关联,每个@Constraint注解都引用一个对应的ConstraintValidator实现类。在运行时,当域模型中遇到约束 Comments 时,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 的@ValidatedComments 进行 Comments,可以选择声明要使用的验证组。查阅MethodValidationPostProcessor javadocs 以获得有关 Hibernate Validator 和 Bean Validation 1.1 提供程序的设置详细信息。

其他配置选项

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

9.8.3 配置 DataBinder

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

以编程方式使用 DataBinder 时,可以在绑定到目标对象后使用它来调用验证逻辑:

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

还可以通过dataBinder.addValidatorsdataBinder.replaceValidators为 DataBinder 配置多个Validator实例。当将全局配置的 Bean 验证与在 DataBinder 实例上本地配置的 Spring Validator结合使用时,这很有用。参见???

9.8.4 Spring MVC 3 验证

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