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

原创
2017/08/17 16:15
阅读数 1.4K

1 简介

JSR-303/JSR-349 bean验证

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

应用程序可以选择一次开启全局Bean验证,在第8节Spring验证中介绍,专门用于所有验证需求。

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

考虑将验证作为业务逻辑有利有弊,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对象。

考虑一个小的数据对象:

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一章中的“验证”一节。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
2 收藏
分享
打赏
0 评论
2 收藏
0
分享
返回顶部
顶部