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

1 简介

JSR-303/JSR-349 bean验证java

Spring Framework 4.0 支持 Bean验证 1.0(JSR-303)和 Bean验证 1.1(JSR-349),也可使用Spring的Validator接口进行验证。web

应用程序能够选择一次开启全局Bean验证,在第8节Spring验证中介绍,专门用于全部验证需求。spring

也能够为每个DataBinder接口注册额外的Spring Validator实例,在8.3节“配置DataBinder中介绍”。这个功能在不使用注解插入验证逻辑时颇有用。express

考虑将验证做为业务逻辑有利有弊,Spring提供了一个不排除任何利弊的验证(和数据绑定)设计。具体的验证不该该与Web层相关联,它应该易于本地化而且应当可以插入任何可用的验证器。考虑以上需求,Spring提供了基础的、在应用程序每一个层均可以使用的Validator接口。编程

数据绑定在把用户输入动态绑定到应用程序的域模型(或者任何用来处理用户出入的对象)时颇有用。Spring提供提供了DataBinder完成这个任务。Validator和DataBinder组成了validation包,主要用于但不局限于MVC框架。数组

BeanWrapper是Spring Framework中的一个基础概念,而且被用于不少地方。然而也许并不须要直接使用BeanWrapper。安全

Spring的DataBinder和底层的BeanWrapper都使用PropertyEditors解析和格式化属性值。PropertyEditor概念是JavaBean规范的一部分,而且也会在本章中讲解。Spring 3 引入了“core.convert”包用于提供通常类型转换设施,同时高级别的“format”包用于格式化UI 域数据。这些新包能够用做PropertyEditors的简单替代,也会在本章讨论。多线程

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

Spring提供Validator接口用于验证对象。Validator接口使用Errors对象,以便在验证时验证器能够将验证失败报告给Errors对象。mvc

考虑一个小的数据对象:app

public class Person {
    private String name;
    private ing age;

    // the usual getters and setters...
}

咱们将要经过实现org.springframework.validation.Validator接口的两个方法为Person类提供验证行为:

  • support(Class) 这个Validator验证接口是否支持Class参数;
  • validate(Object, org.springframework.validation.Errors) 验证给定的对象而且若是有验证错误,使用给定的Errors对象注册这些错误。

实现一个Validator接口至关直接,特别是当知道Spring Framework还提供ValidationUtils帮助器类时。

public class PersonValidator implements Validator {

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

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

正如所见,ValidationUtils类的静态方法rejectIfEmpty(...)用于拒绝name属性若是当它为null或空字符串。能够查看ValidationUtils的javadoc了解除上面的例子外还提供了什么方法。

尽管能够实现单个Validator类用于验证在复杂对象中的每一个嵌入对象,在其本身的Validator实现中封装每一个嵌入对象的验证逻辑可能会更好。一个关于复杂对象的简单例子是一个Customer类由两个String属性(first name 和 second name)和一个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/标签来检查错误消息,可是固然能够本身检查错误消息。这个方法更多的信息能够查看javadoc。

3 将错误码解析为错误消息

已经讨论过数据绑定和验证。关于验证错误的输出消息是须要讨论的最后一个事情。在上面的例子中,验证了name和age字段。若是想要使用MessageSource输出错误消息,将在拒绝该字段(此处为name和age)时给出错误代码。当调用(直接或间接的,例如使用ValidationUtils类)rejectValue或者Errors接口的其它reject方法之一,底层实现不只注册出错的代码,同时注册一系列附加的错误代码。会注册什么错误代码取决于使用的MessageCodesResolver。默认的,使用DefaultMessageCodesResolver,它不只使用给定的代码注册消息,还包含传递给reject方法的域名字。因此若是使用rejectValue("age", "too.darn.old")拒绝一个域,除了too.darn.ord代码,Spring还会注册too.darn.ord.age和too.darn.old.age.int(前一个包含域名而且后一个包含域类型);这有助于帮助开发者定位错误信息。

更多的关于MessageCodesResolver和默认策略能够分别查找MessageCodesResolver和DefaultMessageCodesResolber的javadoc。

4 Bean操做和BeanWrapper

org.springframework.beans包遵照Oracle提供了JavaBean标准。一个JavaBean仅仅是一个类,它拥有默认的无參构造函数,而且遵照命名约定,例如一个属性名为bingoMadness须要有一个setter方法setBingoMadness(...)和getter方法getBingoMadness()。关于更多的JavaBean信息,请参考Oracle的网站。

在beans包中至关重要的类是BeanWrapper接口和它的相关实现(BeanWrapperImpl)。正如javadoc中提到的,BeanWrapper提供设置和读取值的功能(单独或批量),获取属性描述和查询属性是否可读或可写。同时,BeanWrapper提供对嵌入属性的支持,能够在无限深度上设置属性的子属性。同时,BeanWrapper支持添加标准JavaBean PropertyChangeListeners和VetoableChangeListeners,不须要在目标类添加支持代码。最后但并不是最不重要的是,BeanWrapper提供了对设置索引属性的支持。BeanWrapper一般不被应用代码直接使用,它被DataBinder和BeanFactory使用。

BeanWrapper的工做方式部分的被它的名字展示:它包装一个bean以对该bean执行操做,例如设置和检索属性。

4.1 设置和获取基本和嵌入属性

使用setPropertyValue和getPropertyValue方法设置和获取属性,这两个方法都有几个重载版本。它们都在Spring的javadoc中有详细的说明。重要的是指出一些对象表示属性的约定。下面是一些例子:

表达式 解释
name 表示name属性的相关方法为getName()或isName()和setName(...)
account 表示account属性的嵌入属性name的相关方法为getAccount().setName或getAccount().getName()
account[2] 表示索引属性account的第三个元素。索引属性能够是array,list或其它天然排序的集合
account[COMPANYNAME] 表示Map属性account的键COMPANYNAME对应的值。

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

(若是不打算直接使用BeanWrapper,下个章节不是十分重要。若是仅仅使用DataBinder、BeanFactory和它们开箱即用的实现,能够跳过关于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;
    }
}

下面的代码段展现了一些例子关于若是获取和操做Company和Employee实例的一些属性:

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

4.2 内建的PropertyEditor实现

Spring使用PropertyEditor的概念用于影响对象和字符串之间的转换。若是考虑到这一点,有时可能会方便的用不一样的方式方便的表示属性,而不是对象自己。例如,一个Date对象能够被表示为人类可读的方式(做为字符串‘2007-09-14’),同时咱们仍可以转换人类可读的形式回到原始的日期对象(或者更好的:转换任何日期对象为人类可读的方式,并能够转换回Date对象)。这种行为能够经过注册自定义的编辑器得到,编辑器的类型是java.beans.PropertyEditor。在BeanWrapper上注册自定义编辑器或者前面章节提到的在一个指定的IoC容器中注册,而后告诉它如何转换属性到指望的类型。从Oracle提供的java.beans包的javadoc中能够阅读到有关PropertyEditors的更多信息。

在Spring中有一些关于属性编辑的例子:

  • 在bean上设置属性是经过PropertyEditors完成的。当java.lang.String做为在XML文件中声明的某个bean的属性值时,Spring将会(若是相关属性的设置方法有一个Class类型的参数)使用ClassEditor尝试解析参数为Class对象。

  • 在Spring MVC 框架中解析HTTP请求参数是使用能够在CommandController的全部子类中手动绑定的各类PropertyEditor完成的。

Spring 有一系列内建的PropertyEditors。它们都在下面的表格中列出而且都位于org.springframework.beans.propertyeditors包中。其中大多数,但不是所有(以下所述),被BeanWrapperImpl自动注册。属性编辑器能够以某种方式配置,固然能够注册本身的编辑器实现。

解析
ByteArrayPropertyEditor 字节数组的编辑器。字符串将会简单的转换为它们相应的字节表示。被BeanWrapperImpl默认注册。
ClassEditor 解析字符串表示的类为实际的类,和相反的方向。当没法找到类,会抛出IllegalArgumentException。被BeanWrapperImpl默认注册
CustomBooleanEditor 用于Boolean属性的可定制编辑器。被BeanWrapperImpl默认注册,可是能够经过注册自定义的Boolean编辑器实例被覆盖
CustomCollectionEditor 用于集合的属性编辑器,转换任何资源的集合为给定目标Collection类型。
CustomDateEditor 用于java.util.Date的可定制属性编辑器,支持自定义DaeFormat。不会被默认注册。必须被用户以正确格式注册。
CustomNumberEditor 用于任何Number子类例如Integer、Long、Float、Double的可定制属性编辑器。被BeanWrapperImpl默认注册,可是能够经过注册自定义的实例被覆盖。
FileEditor 解析字符串为java.io.File对象。被BeanWrapperImpl默认注册。
InputStreamEditor 单向的属性编辑器,可以获取文本字符串并生成(经过内部的ResourceEditor和Resource)InputStream,因此InputStream属性能够直接设置成字符串。请注意,默认的使用不会关闭InputStream。被BeanWrapperImpl默认注册。
LocaleEditor 可以解析字符串为Locale对象而且反之亦然(字符串的格式是[国家][变种],与Locale提供的toString()方法返回的结果相同)。被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,它包含了用于Font,Color和大多数基本类型的PropertyEditor实现。也请注意标准的JavaBeans基础设置会自动发现PropertyEditor类(不须要你显示注册它们)若是它们位于它们要处理类的相同的包中,而且有与被处理的类有相同的名字并加上Editor后缀;例如能够有以下的类和包结构,FooEditor类能够被自动注册而且做为Foo类型属性的PropertyEditor。

com
    chank
        pop
            Foo
            FooEditor // Foo类的属性编辑器

请注意,在这里你也可使用标准的BeanInfo JavaBeans机制。在下面的例子中,使用BeanInfo机制来显示的注册用于相关类属性的一个或多个PropertyEditor实例。

com
    chank
        pop
            Foo
            FooBeanInfo // Foo类的BeanInfo

如下是引用FooBeanInfo类的Java源代码。这将Foo类的age属性与CustomNumberEditor关联起来。

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

注册其它自定义属性编辑器

当设置bean的属性为字符串的值,Spring控制反转容器最终使用标准的JavaBeans PropertyEditor来转换这些字符串为复杂类型的属性。Spring预先注册了一系列的自定义PropertyEditors(例如,转换用字符串表示的类名为实际的类对象)。例如,Java的标准JavanBeans PropertyEditor查找机制容许PropertyEditor被自动查找,只要它被正确的命名并于它提供支持的类位于同一包中。

若是须要注册其它自定义的PropertyEditors,有几种可行的机制。最手工的办法,并非很便利且不被推荐,是使用ConfigurableBeanFactory接口的registerCustomerEditor()方法,须要你有一个BeanFactory引用。另一个方式,更方便一些,是使用名为CustomEditorConfigurer的特殊的bean工厂后处理器。尽管bean工厂后处理器能够于BeanFactory的实现一块儿使用,但CustomEditorConfigurer具备嵌套属性设置,因此强烈推荐它与ApplicationContext一块儿使用,它将会与其它bean同样的方式被部署,而且被自动发现和应用。

须要注意的是,全部bean工厂和应用上下文自动使用一些内建的属性编辑器,经过使用BeanWrapper类处理属性转换。BeanWrapper注册的标准的属性编辑器在上一节中列出。额外的,ApplicationContext也能够以适用于特定应用上下文类型的方式覆盖或添加其它编辑器来处理资源。

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

当被正确设置,咱们须要可以用字符串正确的设置类型,PropertyEditor会在后台将其转换为ExoticType实例:

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

PropertyEditor的实现应对相似于:

// 转换字符串为ExoticType对象
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

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

最终,咱们使用CustomEditorConfigurer在ApplicationContext中注册新的PropertyEditor,而后它将会被按需使用:

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

使用PropertyEdotirRegistrars

用于在Spring容器中注册属性编辑器的另外一个机制是建立和使用PropertyEditorRegistrar。这个接口尤为有用当你须要使用相同属性编辑器的集合在不一样的场景中:编写一个相应的注册器而且在每一个场景中重用。PropertyEditorRegistrars与名为PropertyEditorRegistry的接口配合工做,PropertyEditorRegistry接口被Spring的BeanWrapper(和DataBinder)实现。PropertyEditorRegistrars尤为方便当与CustomEditorConfigurer一块儿使用时,它暴露了名为setPropertyEditorRegistrars(...)的属性:以这种方式添加到CustomEditorConfigurer中的PropertyEditorRegistrars能够轻松地与DataBinder和Spring MVC控制器共享。此外,它避免了在自定义编辑器上同步的须要:一个PropertyEditorRegistrar能够为每一个bean建立尝试过程建立新的PropertyEditor实例。

使用PropertyEditorRegistrar最好经过一个例子来讲明。首先,你须要建立你本身的PropertyEditorRegistrar实现:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // 指望新的PropertyEditor实例被建立
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // 在这里能够注册尽量多的自定义属性编辑器...
    }
}

也能够查看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);
    }

    // 注册用户的其它方法
}

这种PropertyEditor注册方法能够带来简洁的代码(initBinder(...)的实现仅仅一行代码的长度!),而且容许常见的PropertyEditor注册代码封装在一个类中而后在须要的Contollers中共享。

5 Spring类型转换

Spring 3引入了core.convert包提供通常类型转换系统。系统定义了一个SPI实现类型转换逻辑和一个API执行运行时类型转换。在一个Spring容器中,这个系统能够做为PropertyEditor的替代方法用于转换外部bean属性值字符串为须要的属性类型。公共的API能够在你的应用程序的任何须要类型转换的地方使用。

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)的每次调用,源参数保证不会空。你的转换器能够抛出任何未检查异常若是转换失败;特别的,报告一个无效的源值须要抛出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);
    }
}

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

5.3 通用转换器

当你须要一个复杂的转换器实现,能够考虑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接口,仅当你须要的时候使用。优选选择Converter或ConverterFactory用于基础类型转换的需求。

ConditionalGenericConverter

有时你仅须要一个转换器在指定条件为true时执行。例如,你可能想要在目标字段上提供指定的注解是执行Converter。或者想要在目标类中定义了指定的方法,例如一个静态的valueOf方法是执行Converter。ConditionalGenericConverter是GenericConverter和ConditionalConverter接口的组合,容许你定义此类自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

public interface ConditionalGenericConverter
    extends GenericConverter, ConditionalConverter {

}

ConditionalGenericConverter的一个很好的例子是EntityConverter用于持久化标识符与一个实体引用间的转换。这种EntityConverter仅完成匹配当目标实体类型声明了一个静态查找器方法例如findAccount(Long)。你须要执行这种查找方法检查在matches(TypeDescriptor, TypeDescriptor)的实现中。

5.4 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实现代理它注册的转换器用于携带转换逻辑。

一个健壮的ConversionService实如今core.convert.support包中被提供。GenericConversionService是适用于大多数环境的通用实现。ConversionServiceFactory提供了一个方便的工厂用于建立常见的ConverionService配置。

5.5 配置一个ConversionService

ConversionService是一个无状态的对象,被设计为应用启动阶段实例化,而后在多线程间共享。在一个Spring应用中,典型的每一个Spring容器(或ApplicationContext)配置一个ConversionService实例。这个ConversionService将会被Spring打包而且在须要类型转换的时候被框架使用。你能够将ConversionService注入到你的任何bean而且直接使用它。

若是Spring没有注册ConversionService,原始的基于PropertyEditor系统会被使用。

为了使用Spring注册默认的ConversionService,添加下面的bean定义使用id conversionService:

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

一个默认的ConversionService能够在字符串、数字、枚举、集合、映射和其余常见类型之间转换。为了使用自定义的转换器补充和覆盖默认的转换器,设置converters属性。属性值能够实现了Converter、ConverterFactory或者GenericConverter接口的类。

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

在Spring MVC应用中使用ConversionService也是很常见的。参看Spring MVC 章节中的“转换和格式化"。

在某些状况下想应用在转换中应用格式化,能够查看6.3节“格式化器注册 SPI"关于使用FormattingConversionServiceFactoryBean的更多信息。

5.6 编程的方式使用ConversionService

编程的方式使用ConversionService,像其余任何bean同样将它注入便可:

@Service
public class MyService {

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

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

在大多数用例中,convert方法指定的targetType能够被使用,可是面对更复杂的类型它就不会工做,例如范型参数的集合。例如若是想要编程的方法转换一个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自动注册适用于大多数场景的转换器。包含了集合转换器、纯量转换器和基本的对象到字符串转换器。可使用DefaultConversionService类上的静态addDefaultConverters方法向任何ConverterRegistry注册相同的转换器。

值类型的转换器会被数组和集合重用,因此没有必要建立一个特定的转换器用于转换S的Collection到T的Collection,假设标准的集合处理是正确的。

6 Spring 字段格式化

如前面章节所述,core.convert是通常用途的类型转换系统。它提供统一的ConversionService API和强类型的转换SPI用于实现从一个类型到另外一个类型的转换逻辑。Spring容器使用这个系统绑定bean属性值。此外,Spring表达式语言(SpEL)和DataBinder使用这个系统绑定字段值。例如,当SpEL须要强转一个Short到Long用于完成expression.setValue(Object bean, Object value)尝试,core.convert系统执行这个强转。

如今考虑一个典型的客户端环境(如Web或桌面应用程序)的类型转换要求。在这种环境中,一般会从字符串转换来支持客户回传过程,同时转换回字符串来支持视图描绘过程。此外,常常须要本地化字符串的值。更通用的core.convert转换器SPI不直接处理这中格式化需求。为了直接处理它们,Spring 3进入了一个方便的格式化器SPI提供简单而且强壮的用于客户端环境的PropertyEditors的替代方法。

通常状况下,使用转换器SPI当须要实现通用目的的类型转换逻辑;例如,用于在java.util.Date和java.lang.Long之间的转换。使用格式化器SPI当在客户端环境工做时,例如一个web应用,须要解析和打印出本地化字段值。ConversionService为两种SPI都提供统一的类型转换API。

6.1 格式化器SPI

格式化器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接口便可。范型参数T是但愿格式化的对象的类型,例如,java.util.Date。实现print()方法以打印用于在客户端区域中显示的T的实例。实现parse()方法用于从在客户端区域的格式化表示解析T的实例。格式化器若是解析失败须要抛出ParseException或IllegalArgumentException。必定确保格式化器的实现是线程安全的。

集中格式化器的实如今format子包中被提供。number包提供了NumberFormatter、CurrencyFormatter和PercentFormatter用于使用java.text.NumberFormat格式化java.lang.Number对象。datetime包提供DateFormatter用于使用java.text.DateFormat格式化java.util.Date对象.datetime.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团队欢迎社区驱动的格式化器贡献;查询jira.spring.io。

6.2 注解驱动的格式化

如你所见,字段格式化能够被字段类型或注解配置。为了绑定一个注解到格式化器,须要实现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()返回注解能够注释的字段类型。使getPrinter()返回一个打印器用于打印注解字段的值。使getParser()返回一个解析器用于解析被注解字段的客户端值。

下面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 NumberFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyFormatter();
            } else {
                return new NumberFormatter();
            }
        }
    }
}

为了触发格式化,使用@NumberFormat注解注视字段便可:

public class MyModel {

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

}

格式化注解API

一个便携的格式化注解API在org.springframework.format.annotaiont包中。使用@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;

}

6.3 FormatterRegistry SPI

FormatterRegistry是一个用于注册格式化器和转换器的SPI。FormattingConversionService是一个可用于大多数环境的FormatterRegistry套件的实现。这个实现可使用编程的方式配置或者使用FormattingConversionServiceFactoryBean声明它为一个Spring bean。由于这个实现也实现了ConversionService接口,它能够被直接配置为使用Spring的数据绑定器和Spring表达式语言。

下面是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);

}

如上所述,格式化器能够被字段类型或注解注册。

FormatterRegistry SPI容许你集中配置格式化规则,而不是在各个Controller中重复的配置。例如,你也许想要让全部Date字段经过一个肯定的方法被格式化,或者使用指定注解的字段经过一个肯定的方法被格式化。使用共享的FormatterRegistry,你能够定义这些规则一次而且它们被应用于任何须要格式化的时间。

6.4 FormatterRegistrar SPI

FormatterRegistrar是一个经过FormatterRegistry注册格式化器和转化器的SPI:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);

}

FormatterRegistrar在为一类给定的格式化注册多个相关的转换器和格式化器时是颇有用的,例如时间转换器。它在声明注册没法知足需求时也是有用的。例如当一个格式化器须要被一个不一样于它的范型参数<T>的特定类型检索时或者当注册一个打印器/格式化器对时。下一节提供关于转换器和格式化器注册的更多信息。

6.5 在Spring MVC中配置格式化器

能够查看Spring MVC一章中的“转换和格式化”一节。

7 配置一个全局的日期和时间格式化

默认的,没有被@DateTimeFormat注解注释的日期和时间字段使用DateFormat.SHORT风格从字符串转换。若是须要,能够经过定义本身的全局格式化修改这种行为。

须要保证Spring没有注册默认的格式化器,而且做为替代你须要手动注册全部的格式化器。根据是否使用Joda Time库使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar或者org.springframework.format.datetime.DateFormatterRegistrar类。

例如,下面的Java配置将会注册一个全局的“yyyyMMdd”格式。这个例子没有使用Joda Time库:

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // 使用DefaultFormattingConversionService可是不是用默认的注册器
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // 确保仍然支持@NumberFormat
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // 使用特定的全局格式注册数据转换
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

若是想要使用基于XML的配置,可使用FormattingConversionServiceFactoryBean。下面是相同的例子,这回使用Joda Time:

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

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

Joda Time提供不一样的类型表示日期、时间和日期时间值。JodaTimeFormatterRegistrar的dateFormatter、timeFormatter和dateTimeFormatter属性被用于配置每种类型的不一样格式。DateTimeFormatterFactoryBean提供一个便利的方式建立格式化器。

若是正在使用Spring MVC基于显示的配置所使用的转换服务。对于基于Java的@Configuration这意味着扩展WebMvcConfigurationSupport类并覆盖mvcConversionService()方法。对于XML配置须要使用mvc:annotation-driven元素的"conversion-service"属性。更多信息能够查看Spring MVC的“转换和格式化”一章。

8 Spring 验证

Spring 3对器验证支持引入了几种加强。首先,JSR-303 Bean验证API被彻底支持。其次,当使用编程方法,Spring的DataBinder能够验证对象同时绑定它们。最后,Spring MVC支持声明式的验证@Controller输入。

8.1 JSR-303 Bean验证API概览

JSR-303标准化Java平台的验证约束声明和元数据。使用这个API,可使用声明性验证约束注释域模型属性,而且在运行时强制执行它们。可使用一系列的内建约束。也能够定义本身的约束。

为了展现,考虑一个简单的PersonForm模型它有两个属性:

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

JSR-303容许为这些属性定义声明式的验证约束:

public class PersonForm {

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

    @Min(0)
    private int age;

}

当这个类的一个实例被JSR-303验证器验证,这些约束将会起做用。

查看Bean验证的网站了解JSR-303/JSR-349更多信息。有关默认参考实现的具体功能信息,能够查看Hibernate 验证器文档。了解如何创建一个Bean验证提供者做为Spring Bean,继续向下阅读。

8.2 配置一个Bean验证提供者

Spring 提供了Bean验证API的完整支持。这包括了启动一个JSR-303/JSR-349Bean验证提供这个做为一个Spring bean的便利支持。在应用程序中任何须要验证的地方容许javax.validation.ValidatorFactory或javax.validation.Validator被注入。

使用LocalValidatorFactoryBean配置一个默认的验证器做为Spring bean:

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

上述基础配置将触发使用默认的启动机制初始化Bean验证。一个JSR-303/JSR-349提供者,例如Hibernate验证器,须要在类路径中被提供而且会被自动检测。

注入一个验证器

LocalValidatorFactoryBean实现了javax.validation.ValidatorFactory和javax.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验证API,注入org.springframework.validation.Validator引用:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

}

配置自定义约束

每一个Bean验证约束有两部分组成。首先,一个@Constraint注解声明约束和它的配置属性。第二,一个javax.validation.ConstraintValidator接口的实现,它实现了约束的行为。为了将声明和实现关联,每一个@Contraint注解引用一个相关的验证约束实现类。在运行时,当域模型中遇到约束注释时ConstraintValidatorFactory会实例化引用的实现。

默认的,LocalValidatorFactoryBean配置一个SpringConstraintValidatorFactory,它使用Spring建立约束验证器实例。这容许自定义的约束验证器像其余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驱动的方法验证

Bean验证1.1支持方法验证特性,而且做为Hibernate验证器4.3的一个自定义扩展,能够经过MethodValidationPostProcessor bean定义被集成进入Spring上下文:

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

为了符合Spring驱动方法验证的规范,全部目标类须要被Spring的@Validated注解注释,可选的声明使用的验证组。参看MethodValidationPostProcessor的javadoc来获取关于Hibernate验证器和Bean验证1.1提供者的配置信息。

额外的配置选项

默认的LocalValidatorFactoryBean配置对于大多数状况都足够了。但依然提供了一系列用于不一样bean验证构造的配置选项,从消息插值(message interpolation)到遍历解析(traversal resolution)。查看LocalValidatorFactoryBean的javadoc获取更多关于这些选项的信息。

8.3 配置数据绑定器

从Spring 3开始,一个DataBinder接口使用被配置为验证器。一旦被配置,验证器能够经过调用binder.validate()调用。任何验证错误被自动的添加到绑定器的BindingResult。

当经过编程的方法使用DataBinder,它能够用于在绑定到目标对象后调用验证逻辑:

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

// 绑定到目标对象
binder.bind(propertyValues);

// 验证目标对象
binder.validate();

// 获取包含任何验证错误的BindResult
BindingResult results = binder.getBindingResult();

一个DataBinder也能够经过dataBinder.addValidators和dataBinder.replaceValidators配置多个Validator实例。当将全局配置的Bean验证与在DataBinder实例上本地配置的Spring验证器组合时,此功能很是有用。

8.3 Spring MVC 3验证

查看Spring MVC一章中的“验证”一节。

相关文章
相关标签/搜索