前言

数据填充

将验证视为业务逻辑有利有弊,Spring为验证(和数据绑定)提供了一种不排除其中任何一种的设计。具体来说,验证不应该绑定到web层,而且应该易于本地化,而且应该可以插入任何可用的验证器。考虑到这些问题,Spring提供了一个Validator契约,它既基本又在应用程序的每一层都非常有用。

数据绑定对于将用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring提供了恰当命名的DataBinder来完成这一任务。ValidatorDataBinder组成了validation包,它主要用于但不限于web层。

BeanWrapper是Spring框架中的一个基本概念,在很多地方都被使用。但是,您可能不需要直接使用BeanWrapper。然而,因为这是参考文档,我们觉得一些解释可能是合适的。我们将在本章中解释BeanWrapper,因为如果您打算使用它,那么您最有可能在尝试将数据绑定到对象时使用它。

Spring的DataBinder和低级的BeanWrapper都使用PropertyEditorSupport实现来解析和格式化属性值。PropertyEditorPropertyEditorSupport类型是javabean规范的一部分,在本章中也会进行解释。Spring 3引入了一个core.convert包,它提供了一个通用类型转换工具,以及一个用于格式化UI字段值的高级“格式”包。您可以使用这些包作为PropertyEditorSupport实现的更简单的替代方案。本章也将对它们进行讨论。

Spring通过设置基础设施和一个适配Spring自己的Validator契约的适配器来支持Java Bean验证。应用程序可以全局启用一次Bean验证,如Java Bean验证中所述,并专门用于所有验证需求。在web层中,应用程序可以进一步为每个DataBinder注册控制器本地Spring Validator实例,如配置一个DataBinder中所述,这对于插入自定义验证逻辑非常有用。

通过使用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类时。下面的示例实现了Person实例的Validator:

public class PersonValidator implements Validator {

    /**
     * This Validator validates only Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

ValidationUtils类上的static rejectIfEmpty(..)方法用于在name属性为null或空字符串时拒绝它。看看ValidationUtils javadoc,看看它除了前面显示的示例之外还提供了哪些功能。

虽然可以实现单个Validator类来验证富对象中的每个嵌套对象,但最好是将每个嵌套对象类的验证逻辑封装在它自己的验证器实现中。富对象的一个简单示例是Customer,它由两个String属性(第一个和第二个名称)和一个复杂的Address对象组成。Address对象可以独立于Customer对象使用,因此实现了一个不同的AddressValidator。如果您希望您的CustomerValidator重用包含在AddressValidator类中的逻辑而不诉诸于复制和粘贴,您可以依赖注入或在CustomerValidator中实例化AddressValidator,如下面的示例所示:

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                "support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }

    /**
     * This Validator validates Customer instances, and any subclasses of Customer too
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

验证错误被报告给传递给验证器的Errors对象。在Spring Web MVC的情况下,您可以使用<spring:bind/>标记来检查错误消息,但是您也可以自己检查Errors对象。关于它提供的方法的更多信息可以在javadoc中找到。

解析代码到错误消息

我们讨论了数据绑定和验证。本节介绍输出与验证错误相对应的消息。在上一节所示的示例中,我们拒绝了nameage字段。如果我们希望通过使用MessageSource输出错误消息,我们可以使用拒绝字段时提供的错误代码(在本例中是“name”和“age”)来实现这一点。当您(直接或间接地,通过使用,例如,ValidationUtils类)从Errors接口调用rejectValue或其他reject方法之一时,底层实现不仅注册您传入的代码,而且还注册许多额外的错误代码。MessageCodesResolver确定Errors接口注册的错误代码。默认情况下,使用DefaultMessageCodesResolver,它(例如)不仅用您提供的代码注册消息,而且还注册包含您传递给reject方法的字段名的消息。所以,如果你通过使用rejectValue("age", "too.darn.old")拒绝一个字段,Spring还注册了too.darn.old.agetoo.darn.old.age.int(第一个包含字段名,第二个包含字段类型)。这样做是为了方便帮助开发人员定位错误消息。

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

Bean操作和BeanWrapper

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

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

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

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

设置和获取属性是通过BeanWrapper重载的setPropertyValuegetPropertyValue方法变量来完成的。详情请参阅他们的Javadoc。下表展示了这些约定的一些例子:

表达式

解释

name

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

account.name

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

account[2]

指示已索引的属性acount的第三个元素。索引属性的类型可以是arraylist或其他自然排序的集合。

account[COMPANYNAME]

指示acount Map属性的COMPANYNAME键索引的映射条目的值。

(如果您不打算直接使用BeanWrapper,那么下一节对您来说并不是非常重要。如果您只使用DataBinderBeanFactory以及它们的默认实现,则应该直接跳到PropertyEditors一节。)

以下两个示例类使用BeanWrapper来获取和设置属性:

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

内置PropertyEditor实现

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

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

  • 通过使用PropertyEditor实现来设置bean上的属性。当您使用String作为您在XML文件中声明的某个bean的属性的值时,Spring(如果相应属性的setter有一个Class参数)使用ClassEditor尝试将该参数解析为一个Class对象。
  • 解析Spring MVC框架中的HTTP请求参数是通过使用各种PropertyEditor实现来完成的,这些实现可以手动绑定到CommandController的所有子类中。

Spring有许多内置的PropertyEditor实现,可以简化工作。它们都位于org.springframework.beans.propertyeditors包中。默认情况下,大多数(但不是全部,如下表所示)是由BeanWrapperImpl注册的。在属性编辑器以某种方式可配置的情况下,您仍然可以注册自己的变体以覆盖默认的变体。下表描述了Spring提供的各种PropertyEditor实现:


解释

ByteArrayPropertyEditor

字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认情况下由BeanWrapperImpl注册。

ClassEditor

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

CustomBooleanEditor

可自定义的Boolean属性的属性编辑器。默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。

CustomCollectionEditor

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

CustomDateEditor

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

CustomNumberEditor

可定制的属性编辑器,用于任何Number子类,如IntegerLongFloatDouble。默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。

FileEditor

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

InputStreamEditor

单向属性编辑器,可以接受一个字符串并产生(通过中间的ResourceEditorResource)一个InputStream,这样InputStream属性就可以直接设置为字符串。注意,默认用法不会为您关闭InputStream。默认情况下,由BeanWrapperImpl注册。

LocaleEditor

可以将字符串解析为Locale对象,反之亦然(字符串格式为[country][variant],与LocaletoString()方法相同)。默认情况下,由BeanWrapperImpl注册。

PatternEditor

可以将字符串解析为java.util.regex.Pattern,反之亦然。

PropertiesEditor

可以将字符串(使用java.util.Properties类的javadoc中定义的格式进行格式化)转换为Properties对象。默认情况下,由BeanWrapperImpl注册。

StringTrimmerEditor

修剪字符串的属性编辑器。可选允许将空字符串转换为null值。默认没有注册-必须是用户注册的。

URLEditor

可以将URL的字符串表示解析为实际的URL对象。默认情况下,由BeanWrapperImpl注册。

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

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

请注意,您也可以在这里使用标准的BeanInfo javabean机制(在这里进行了一定程度的描述)。下面的示例使用BeanInfo机制显式地将一个或多个PropertyEditor实例注册到关联类的属性中:

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

下面引用的SomethingBeanInfo类的Java源代码将CustomNumberEditorSomething类的age属性关联起来:

public class SomethingBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}
注册额外的自定义PropertyEditor实现

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

如果需要注册其他自定义propertyeditor,可以使用几种机制。最手动的方法(通常不方便也不推荐)是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,假设您有一个BeanFactory引用。另一种(稍微方便一些的)机制是使用名为CustomEditorConfigurer的特殊bean工厂后处理器。虽然可以使用bean工厂后处理器BeanFactory实现,CustomEditorConfigurer有一个嵌套的属性设置,所以我们强烈建议您使用它与ApplicationContext,以类似的方式,您可以将其部署到任何其他bean,它可以自动检测和应用。

请注意,所有bean工厂和应用程序上下文自动使用许多内置的属性编辑器,通过它们使用BeanWrapper来处理属性转换。前一节列出了BeanWrapper注册的标准属性编辑器。此外,ApplicationContexts还覆盖或添加其他编辑器,以适合特定应用程序上下文类型的方式处理资源查找。

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

考虑以下示例,它定义了一个名为ExoticType的用户类和另一个名为DependsOnExoticType的类,需要将ExoticType设置为属性:

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定义显示了如何建立这种关系:

<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()));
    }
}
// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}

最后,下面的示例展示了如何使用CustomEditorConfigurer将新的PropertyEditor注册到ApplicationContext中,这样ApplicationContext就可以根据需要使用它了:

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

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

下面的例子展示了如何创建你自己的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与数据绑定控制器(如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之间共享。

Spring类型转换

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

转换器SPI

实现类型转换逻辑的SPI是简单且强类型的,如下面的接口定义所示:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

要创建自己的转换器,请实现Converter接口,并将S参数化为要转换的类型,将T参数化为要转换的类型。如果S的集合或数组需要转换为T的集合或数组,您也可以透明地应用这样的转换器,前提是委托的数组或集合转换器也已经注册(默认情况下,DefaultConversionService会这样做)。

对于每个convert(S)的调用,source参数保证不为null。如果转换失败,您的Converter可能会抛出任何未检查的异常。具体来说,它应该抛出一个IllegalArgumentException来报告一个无效的源值。请注意确保您的Converter实现是线程安全的。

为了方便起见,core.convert.support包中提供了几个转换器的实现。其中包括从字符串到数字和其他常见类型的转换器。下面的清单显示了StringToInteger类,它是一个典型的Converter实现:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

使用ConverterFactory

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

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

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

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

StringToEnumConverterFactory为例:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

使用GenericConverter

当您需要一个复杂的Converter实现时,请考虑使用GenericConverter接口。与Converter相比,GenericConverter具有更灵活但类型较弱的签名,它支持在多个源类型和目标类型之间进行转换。此外,在实现转换逻辑时,可以使用GenericConverter提供的源和目标字段上下文。这种上下文允许由字段注释或字段签名上声明的泛型信息驱动类型转换。下面的清单显示了GenericConverter的接口定义:

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

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

要实现一个GenericConverter,请让getConvertibleTypes()返回支持的源→目标类型对。然后实现convert(Object, TypeDescriptor, TypeDescriptor)来包含转换逻辑。源TypeDescriptor提供了对保存被转换值的源字段的访问。目标TypeDescriptor提供了对要设置转换后的值的目标字段的访问。

GenericConverter的一个很好的例子是在Java数组和集合之间进行转换的转换器。这样的ArrayToCollectionConverter会内省声明目标集合类型的字段,以解析该集合的元素类型。这使得源数组中的每个元素都可以在目标字段上设置集合之前转换为集合元素类型。

因为GenericConverter是一个更复杂的SPI接口,所以您应该只在需要时使用它。为基本类型转换需求,请选择ConverterConverterFactory

使用ConditionalGenericConverter

有时,您希望仅在特定条件为真时才运行Converter。例如,您可能希望仅在目标字段上存在特定注释时运行Converter,或者您可能希望仅在目标类上定义了特定方法(例如static valueOf方法)时运行ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的结合,让你可以定义这样的自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

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

ConversionService API

ConversionService定义了在运行时执行类型转换逻辑的统一API。转换器通常在以下门面接口后面运行:

package org.springframework.core.convert;

public interface ConversionService {

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

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

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

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

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

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

配置一个ConversionService

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

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

要向Spring注册一个默认的ConversionService,请添加以下idconversionService的bean定义:

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

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

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

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

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

以编程方式使用ConversionService

要以编程方式使用ConversionService实例,您可以像对任何其他bean一样注入对它的引用。下面的例子展示了如何做到这一点:

@Service
public class MyService {

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

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

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

幸运的是,TypeDescriptor提供了各种选项,让这一过程变得简单,如下面的例子所示:

DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ...
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

请注意,DefaultConversionService会自动注册适合大多数环境的转换器。这包括集合转换器、标量转换器和基本的ObjectString转换器。通过使用DefaultConversionService类上的静态addDefaultConverters方法,可以向任何ConverterRegistry注册相同的转换器。

值类型的转换器可用于数组和集合,因此不需要创建特定的转换器来将SCollection转换为TCollection,假设标准集合处理是合适的。

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系统会执行强制转换。

现在考虑典型客户端环境的类型转换需求,例如web或桌面应用程序。在这样的环境中,您通常会将String转换为支持客户端回发过程,并将String转换为支持视图呈现过程。此外,您经常需要本地化String值。更通用的core.convert ConverterSPI没有直接解决这种格式要求。为了直接解决这些问题,Spring 3引入了一个方便的FormatterSPI,它为客户端环境提供了一个简单而健壮的PropertyEditor实现替代方案。

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

Formatter SPI

用于实现字段格式化逻辑的Formatter SPI很简单,而且是强类型的。下面的清单显示了Formatter接口定义:

package org.springframework.format;

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

FormatterPrinterParser构建块接口扩展而来。下面的清单显示了这两个接口的定义:

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接口。参数化T为您希望格式化的对象类型—例如,java.util.Date。实现print()操作打印T的一个实例,以便在客户端区域设置中显示。实现parse()操作,从客户机区域设置返回的格式化表示解析T的实例。如果解析失败,格式化程序应该抛出ParseExceptionIllegalArgumentException。请确保Formatter程序实现是线程安全的。

为了方便起见,格式化子包提供了几种格式化器实现。number包提供NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter来格式化使用java.text.NumberFormatNumber对象。datetime包提供了一个DateFormatter来格式化java.util.Date对象,使用java.text.DateFormat

下面的DateFormatter是一个示例Formatter实现:

package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

    private String pattern;

    public DateFormatter(String pattern) {
        this.pattern = pattern;
    }

    public String print(Date date, Locale locale) {
        if (date == null) {
            return "";
        }
        return getDateFormat(locale).format(date);
    }

    public Date parse(String formatted, Locale locale) throws ParseException {
        if (formatted.length() == 0) {
            return null;
        }
        return getDateFormat(locale).parse(formatted);
    }

    protected DateFormat getDateFormat(Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
        dateFormat.setLenient(false);
        return dateFormat;
    }
}

Spring团队欢迎社区驱动的Formatter贡献。请参阅GitHub问题进行贡献。

注解驱动的格式

可以通过字段类型或注释配置字段格式。要将注释绑定到Formatter,请实现AnnotationFormatterFactory。下面的清单显示了AnnotationFormatterFactory接口的定义:

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}

创建一个实现:参数化A为您希望关联格式化逻辑的annotationType字段—例如org.springframework.format.annotation.DateTimeFormat。让getFieldTypes()返回可以使用注释的字段类型。让getPrinter()返回一个打印带注释字段值的Printer。让getParser()返回一个Parser来解析带注释的字段的clientValue

下面的示例AnnotationFormatterFactory实现将@NumberFormat注释绑定到格式化器,以指定数字样式或模式:

public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {

    public Set<Class<?>> getFieldTypes() {
        return new HashSet<Class<?>>(asList(new Class<?>[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }

    public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
        if (!annotation.pattern().isEmpty()) {
            return new NumberStyleFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentStyleFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyStyleFormatter();
            } else {
                return new NumberStyleFormatter();
            }
        }
    }
}

要触发格式化,您可以用@NumberFormat注释字段,如下面的示例所示:

public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}
格式注释API

org.springframework.format.annotation包中有一个可移植的格式注释API。您可以使用@NumberFormat来格式化诸如DoubleLong之类的Number字段,并且使用@DateTimeFormat来格式化java.util.Datejava.util.CalendarLong(用于毫秒时间戳)以及JSR-310 java.time

@DateTimeFormatjava.util.Date格式化为ISO日期(yyyy-MM-dd)为例:

public class MyModel {

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

FormatterRegistry SPI

FormatterRegistry是一个用于注册格式化器和转换器的SPI。FormattingConversionServiceFormatterRegistry的一个实现,适用于大多数环境。您可以通过编程方式或声明方式将此变体配置为Spring bean,例如通过使用FormattingConversionServiceFactoryBean。因为这个实现也实现了ConversionService,所以您可以直接配置它,以便与Spring的DataBinder和Spring表达式语言(SpEL)一起使用。

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

如上面的清单所示,您可以根据字段类型或注释注册格式化程序。

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

FormatterRegistrar SPI

FormatterRegistry是一个SPI,用于通过FormatterRegistry注册格式化器和转换器。下面的清单显示了它的接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

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

在Spring MVC中配置格式化

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

配置全局日期和时间格式

默认情况下,没有使用@DateTimeFormat注释的日期和时间字段将使用DateFormat.SHORT样式从字符串转换。如果您愿意,您可以通过定义自己的全局格式来改变这一点。

要做到这一点,请确保Spring没有注册默认格式化程序。相反,在以下方法的帮助下手动注册格式化器:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
  • org.springframework.format.datetime.DateFormatterRegistrar

例如,以下Java配置注册了全局的yyyyMMdd格式:

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // Use the DefaultFormattingConversionService but do not register defaults
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // Ensure @NumberFormat is still supported
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // Register JSR-310 date conversion with a specific global format
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

如果喜欢基于xml的配置,可以使用FormattingConversionServiceFactoryBean。下面的例子展示了如何做到这一点:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd>

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

注意:在web应用程序中配置日期和时间格式时,还有一些额外的考虑事项。

Java Bean验证


Bean验证概述

Bean验证为Java应用程序提供了一种通过约束声明和元数据进行验证的通用方法。要使用它,您可以使用声明性验证约束来注释域模型属性,然后运行时强制执行这些约束。有内置的约束,您也可以定义自己的自定义约束。

考虑下面的示例,该示例显示了一个具有两个属性的简单PersonForm模型:

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

Bean验证允许您声明约束,示例如下:

public class PersonForm {

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

    @Min(0)
    private int age;
}

Bean验证验证器然后根据声明的约束验证该类的实例。有关API的一般信息,请参阅Bean验证。有关特定的约束,请参阅Hibernate验证器文档。要学习如何将bean验证提供程序设置为Spring bean,请继续阅读。

配置Bean验证提供程序

Spring提供了对Bean验证API的全面支持,包括将Bean验证提供者引导为Spring Bean。这让你可以在应用程序中需要验证的地方注入javax.validation.ValidatorFactoryjavax.validation.Validator

您可以使用LocalValidatorFactoryBean将默认验证器配置为Spring bean,如下面的示例所示:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

上面示例中的基本配置通过使用其默认的引导机制来触发bean验证进行初始化。Bean验证提供者(如Hibernate验证器)应该出现在类路径中,并被自动检测到。

注入一个验证器

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验证约束由两部分组成:

  • 声明约束及其可配置属性的@Constraint注释。
  • 实现约束行为的javax.validation.ConstraintValidator接口的实现。

为了将声明与实现关联起来,每个@Constraint注释引用一个对应的ConstraintValidator实现类。在运行时,当在域模型中遇到约束注释时,ConstraintValidatorFactory实例化所引用的实现。

默认情况下,LocalValidatorFactoryBean配置了一个SpringConstraintValidatorFactory,它使用Spring来创建ConstraintValidator实例。这使得您的自定义ConstraintValidators可以像其他Spring bean一样从依赖注入中受益。

下面的例子展示了一个自定义的@Constraint声明,后面跟着一个关联的ConstraintValidator实现,它使用Spring来进行依赖注入:

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

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}

正如前面的示例所示,ConstraintValidator实现可以将其依赖项@Autowired作为任何其他Spring bean。

Spring驱动的方法验证

您可以通过MethodValidationPostProcessor Bean定义将Bean validation 1.1(以及Hibernate Validator 4.3的自定义扩展)支持的方法验证特性集成到Spring上下文中:

import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
public class AppConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

要符合Spring驱动方法验证的条件,所有目标类都需要用Spring的@Validated注释进行注释,该注释还可以选择性地声明要使用的验证组。关于Hibernate验证器和Bean验证1.1提供程序的设置细节

方法验证依赖于目标类周围的AOP代理,要么是接口上方法的JDK动态代理,要么是CGLIB代理。使用代理有一定的限制,在理解AOP代理中描述了一些限制。此外,记住总是在代理类上使用方法和访问器;直接访问字段将不起作用。

额外的配置选项

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

配置一个DataBinder

从Spring 3开始,您可以使用Validator配置DataBinder实例。一旦配置好,您就可以通过调用binder.validate()来调用Validator。任何验证Errors都会自动添加到绑定器的BindingResult中。

下面的例子展示了如何通过编程方式使用DataBinder在绑定到目标对象后调用验证逻辑:

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

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

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

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

您还可以通过dataBinder.addValidatorsdataBinder.replaceValidators使用多个Validator实例来配置一个DataBinder。这在将全局配置的bean验证与在DataBinder实例上本地配置的Spring Validator相结合时非有用。参见Spring MVC验证配置。