第9章 数据绑定 -- Spring4.3.8参考文档中文版

9 数据校验、数据绑定和类型转换

@sunRainAmazing

9.1 概述

JSR-303/JSR-349 Bean Validation
Spring 4.0 默认即支持Bean Validation 1.0 (JSR-303)和 Bean Validation 1.1(JSR-349)校验规范, 同时也能适配Spring的Validator校验接口。

application既可以像Section 7.8, “Spring Validation”所述一次性开启全局的Bean Validation,也可以在你 需要验证的地方一一开启。
application可以像Section 7.8.3, “Configuring a DataBinder”所述在每个DataBinder实例中注册多个自定义的Spring Validator 实例,这对那些不愿意使用注解来实现插件式的校验逻辑来说非常有用。

在业务逻辑中考虑数据校验利弊各半,Spring 提供的校验(和数据绑定)方案也未能解决这个问题。 能明确的是数据校验不应该被限定在web层使用,它应该能很方便的执行本地化,并且能在任何需要 数据校验的场合以插件的形式提供服务。基于以上考虑,Spring 设计了一个既基本又方便使用且能 在所有层使用的Validator接口。

Spring 提供了我们称作DataBinder的对象来处理数据绑定,所谓的数据绑定就是将用户的输入 自动的绑定到我们的领域模型(或者说任意用来处理用户输入的对象)。Spring 的Validator和 DataBinder构成了validation包,这个包主要被Spring MVC框架使用,但绝不限于只能在该 框架使用。

在Spring中BeanWrapper是一个很基本的概念,在很多地方都有使用到它。但是,你可能从来都没有 直接使用到它。鉴于这是一份参考文档,我们认为很有必要对BeanWrapper进行必要的解释。在这一 章中我们将解释BeanWrapper,在你尝试将数据绑定到对象时一定会使用到它。
Spring的数据绑定和较低级别的BeanWrapper都会使用PropertyEditors来进行转换和格式化。PropertyEditor 是JavaBeans规范的一部分,在这一章中我们将进行探讨。Spring3引入了”core.convert”这个包来提供通用 的类型转换工具和高级”format”包来格式化UI显示;这两个包提供的工具可以用作PropertyEditors 的替代品,我们也将在这一章对它们展开讨论。

9.2 使用Spring的Validator接口来进行数据校验

Spring 提供了Validator接口用来进行对象的数据校验。Validator接口在进行数据校验的时候 会要求传入一个Errors对象,当有错误产生时会将错误信息放入该Errors对象。
我们假设有这么一个数据对象:

public class Person {
    private String name;
    private int age;
    //省略getters和setters...
}

为了给Person类提供校验行为我们可以通过实现org.springframework.validation.Validator这个接口的两个方法来实现:
supports(Class) - 判断该Validator是否能校验提供的Class的实例?
validate(Object, org.springframework.validation.Errors) - 校验给定的对象,如果有校验失败信息,将其放入Errors对象
实现一个校验器是相当简单的,尤其是当你知道spring已经提供了一个ValidationUtils工具类时。

public class PersonValidator implements Validator {
    /**
     * 这个校验器*仅仅*只校验Person实例
     */
    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的javadocs,提前了解下除了例子中展示的功能外还有哪些好用的方法。
当校验一个复杂的对象时,自定义一个校验器类(封装嵌套对象的校验器类)比把校验逻辑分散到各个嵌套对象会更方便管理。 比如:现在有一个Customer复杂对象,它有两个String类型的属性(first and second name),以及一个Address对象; 这个Address对象和Customer对象是毫无关系的,它还实现了AddressValidator这样一个校验器。如果你想在Customer校验器 类中重用Address校验器的功能(这种重用不是通过简单的代码拷贝),你可以将Address校验器的实例通过依赖注入的方式注入到 Customer校验器中。 像下面所描述的这样:

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

    /**
     * 这个校验器校验Customer实例,同时也会校验Customer的子类实例
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

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

校验错误都会向作为参数传入的Errors对象进行报告。如果你使用的是Spring Web MVC,你可以使用<spring:bind/>标签 来提取校验错误信息,当然你也可以通过自己的方式来提取错误信息,这些方式可以通过阅读javadocs来获取更多的帮助。

9.3 通过错误编码得到错误信息

前面我们谈到数据绑定和数据校验。如何拿到校验错误信息是我们最后需要讨论的一个问题。在上面的例子中,
我们拒绝了nameage属性。如果我们想要输出校验错误的提示信息,就要用到校验失败时设置的错误编码(本例中就是’name’和’age’)。
当你调用Errors接口中的rejectValue方法或者它的任何一个方法,它的优先实现不仅仅会注册作为参数传入进来的错误编码,
还会注册一些遵循一定规则的错误编码。注册哪些规则的错误编码取决于你使用的MessageCodesResolver。当我们使用默认的DefaultMessageCodesResolver
时,除了会将错误信息注册到你指定的错误编码上之外,这些错误信息还会注册到包含属性名的错误编码上。假如你调用这样一个方法rejectValue("age", "too.darn.old")
Spring除了会注册too.darn.old这个错误编码外,还会注册too.darn.old.agetoo.darn.old.age.int这两个错误编码(即一个是包含属性名,另外一个既包含属性名还包含类型);这在Spring中作为一种约定,这样所有的开发者都能按照这种约定来定位错误信息了。
想要获取更多有关MessageCodesResolver和默认的策略,可以通过下面的在线文档获取: MessageCodesResolver DefaultMessageCodesResolver,

9.4 Bean的操作和BeanWrapper

org.springframework.beans包是符合Oracle公司的JavaBeans规范的。JavaBean是一个拥有默认无参构造函数的类,
它还有一种命名约定,假如这个类有一个属性bingoMadness,那它必须有一个setter方法setBingoMadness(..)
一个getter方法getBingoMadness()。为了获取更多关于JavaBeans及其规范的信息,请参考Oracle的网站(
http://docs.oracle.com/javase/6/docs/api/java/beans/package-summary.html[javabeans])

在beans包中相当重要的是BeanWrapper接口和它的实现类(BeanWrapperImpl)。引用其javadocs中的说明,BeanWrapper提供了设置和获取属性值, 获取属性描述符以及遍历属性来确定它们是可读的还是可写的功能。BeanWrapper也支持嵌套属性,允许不限嵌套级数的子属性设置。BeanWrapper还支持 在不需要目标类中加入额外的代码就能添加标准的JavaBeansPropertyChangeListeners和VetoableChangeListeners。值得一提的是BeanWrapper还支持 索引的属性。通常我们一般不会在应用代码中直接用到BeanWrapper,除了DataBinder和BeanFactory。
BeanWrapper基本上是通过它的名字来进行工作的:它包裹一个bean来代替它执行某些动作,如设置以及获取属性。

9.4.1 Setting和getting基本及嵌套属性

Setting和getting属性是通过一组变形的重载方法setPropertyValue(s)和getPropertyValue(s)来完成的。您可以通过Spring的javadoc来获得更多的信息。 你必须知道的是描述对象的属性有一些约定俗成的规则。下面有几个例子:

Table 9.1. 属性的例子
这里写图片描述

下面你将看到一些通过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;
    }
}

下面的代码向你展示了如何获取和操作已经实例化的Companies和Employees的属性。

BeanWrapper company = BeanWrapperImpl(new Company());//设置公司名称..
company.setPropertyValue("name", "Some Company Inc.");//...也可以像这样做:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
//现在我们来创建一个主管并把它绑定到公司上:
BeanWrapper jim = BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
//通过公司属性来获取主管的薪水
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

9.4.2内置PropertyEditor实现

Spring使用这个概念PropertyEditors来实现an Object和a 之间的转换 String。如果您考虑到这一点,有时可能会以与对象本身不同的方式表示属性。例如,aDate 可以以人类可读的方式(如此String ‘2007-14-09’)来表示,而我们仍然能够将人类可读形式转换回原始日期(或者甚至更好:将以人类可读形式输入的任何日期转换回到Date对象)。这种行为可以通过注册类型的 自定义编辑器来实现java.beans.PropertyEditor。BeanWrapper如上一章所述,在特定的IoC容器中或者交替地注册自定义编辑器,可以了解如何将属性转换为所需类型。阅读更多关于 Oracle提供PropertyEditors的java.beans包的javadocs 。

Spring中使用了一些属性编辑的例子:
使用 bean设置属性PropertyEditors。当提及 java.lang.String作为XML文件中声明的某个bean的属性的值时,Spring将(如果相应属性的setter具有 Class-parameter)使用该ClassEditor方法尝试将参数解析为Class 对象。

在Spring的MVC框架中解析HTTP请求参数是使用PropertyEditors可以在所有子类中手动绑定的各种类型完成的 CommandController。
Spring有一些内置的PropertyEditors使生活容易。其中的每一个都列在下面,它们都位于org.springframework.beans.propertyeditors 包装中。大多数但不是全部(如下所示),默认情况下注册 BeanWrapperImpl。在某些方面,属性编辑器可以配置,您当然可以注册自己的变体来覆盖默认的变体:

表9.2 内置PropertyEditor(部分)
这里写图片描述

Spring使用它java.beans.PropertyEditorManager来设置可能需要的属性编辑器的搜索路径。搜索路径还包括sun.bean.editors,其包括PropertyEditor实现为类型,例如Font,Color和最原始类型。还要注意的是,标准的JavaBeans基础架构将自动发现PropertyEditor类(不需要明确注册它们),如果它们处于与他们处理的类在同一个包中,并且具有与该类相同的名称,并’Editor’附加; 例如,一个可能具有以下类和封装结构,这将是足够的FooEditor类被识别并用作PropertyEditor用于Foo-typed性质。

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

请注意,您也可以在BeanInfo这里使用标准的JavaBeans机制( 在这里不详细的描述)。在下面的示例中,使用BeanInfo明确注册一个或多个PropertyEditor具有关联类的属性的实例的机制的示例。

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

以下是引用FooBeanInfo类的Java源代码。这将使a CustomNumberEditor与该类的age属性相关联Foo。

public class FooBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}

注册其他自定义PropertyEditor
将bean属性设置为字符串值时,Spring IoC容器最终将使用标准JavaBean PropertyEditors将这些字符串转换为属性的复杂类型。Spring预先注册了一些自定义PropertyEditors(例如,将一个表达为字符串的类名转换为真实Class对象)。此外,Java的标准JavaBeans PropertyEditor查找机制允许一个PropertyEditor 类被简单地命名为适当的名称,并放置在与它提供的类一样的包中,以便自动找到。
如果需要注册其他自定义PropertyEditors,则有几种机制可用。假设您有参考,最常用的手段方法通常不方便或不推荐,只需简单地使用接口的registerCustomEditor()方法 即可。另一个,稍微更方便的机制是使用一个特殊的bean工厂处理器调用。虽然bean工厂处理器可以与实现一起使用,但是它具有嵌套的属性设置,因此强烈建议将其与 其它bean类似地部署在其中,并自动检测并应用。

请注意,所有bean工厂和应用程序上下文都会自动使用一些内置的属性编辑器,通过使用一些名为a BeanWrapper来处理属性转换的东西。标准属性编辑BeanWrapper寄存器在上一节中列出。此外, ApplicationContexts还可以以适合于特定应用程序上下文类型的方式覆盖或添加附加数量的编辑器来处理资源查找。
标准JavaBeans PropertyEditor实例用于将表示为字符串的属性值转换为属性的实际复杂类型。 CustomEditorConfigurer一个bean工厂处理器,可以用来方便地添加对其他PropertyEditor实例的支持ApplicationContext。
考虑一个用户类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  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用来注册新PropertyEditor的 ApplicationContext,然后可以根据需要使用它:

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

使用PropertyEditorRegistrars
使用Spring容器注册属性编辑器的另一种机制是创建和使用a PropertyEditorRegistrar。当您需要在几种不同的情况下使用同一组属性编辑器时,此界面特别有用:在每种情况下写入相应的注册器并重新使用。PropertyEditorRegistrars与一个接口配合使用PropertyEditorRegistry,一个由SpringBeanWrapper(和DataBinder)实现的接口。PropertyEditorRegistrars 在与CustomEditorConfigurer (这里介绍)结合使用时特别方便,它暴露了一个名为setPropertyEditorRegistrars(..):PropertyEditorRegistrars以CustomEditorConfigurer这种方式添加 的属性可以轻松地与DataBinderSpring MVC共享Controllers。此外,它避免了对自定义编辑器的同步需求:a PropertyEditorRegistrar希望PropertyEditor 为每个创建bean 创建新的实例。
使用一个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)一起使用可以非常方便。在下面的一个例子中,使用一个PropertyEditorRegistrar在实现initBinder(..)方法中使用:

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 注册码封装在一个类中,然后Controllers根据需要共享 。

9.5 Spring式转换

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

9.5.1转换器SPI

SPI实现类型转换逻辑是简单和强类型:

package org.springframework.core.convert.converter;
    public interface Converter<S,T> {
    T  convert(S source);
}

要创建自己的转换器,只需实现上面的界面。参数化S 作为您要转换的类型,以及转换T为的类型。如果S需要将集合或数组转换为数组或集合,则这种转换器也可以透明地应用T,前提是已经注册了委托数组/集合转换器(DefaultConversionService默认情况下)。
对于每次调用convert(S),源参数保证不为空。如果转换失败,转换器可能会抛出任何未经检查的异常; 具体来说, IllegalArgumentException应该抛出一个报告一个无效的源值。注意确保您的Converter实现是线程安全的。
core.convert.support为方便起见,在包中提供了几个转换器实现。这些包括从字符串到数字和其他常见类型的转换器。考虑StringToInteger作为一个典型的例子Converter实现:

package org.springframework.core.convert.support;
    final class StringToInteger implements Converter<StringInteger> {
    public Integer convert(String source){
             return Integer.valueOf(source);
       }
}

9.5.2 ConverterFactory

当需要集中整个类层次结构的转换逻辑时,例如,当从String转换为java.lang.Enum对象时,请实现 ConverterFactory:

package org.springframework.core.convert.converter;
public interface  ConverterFactory <S,R> {
    <T extends R> Converter <S,T> getConverter(Class <T> targetType);
}

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

package org.springframework.core.convert.support;
final class StringToEnumConverterFactory implements  ConverterFactory <StringEnum> {
    public <T extends Enum> Converter <String,T> getConverter(Class <T> targetType){
         return  new StringToEnumConverter(targetType);
    }
    private  final class  StringToEnumConverter <T extends Enum> implements Converter <StringT> {
    private Class <T>  enumType;
    public StringToEnumConverterClass <T> enumType){
             this .enumType = enumType;
        }
    public T convert(String source){
             return(T)Enum.valueOf(this .enumType,source.trim());
        }
    }
}

9.5.3 GenericConverter

当需要复杂的Converter实现时,请考虑GenericConverter接口。GenericConverter具有更灵活但不太强的类型签名,支持在多种源和目标类型之间进行转换。此外,GenericConverter可以提供源和目标字段上下文,您可以在实现转换逻辑时使用。这种上下文允许类型转换由字段注释或在字段签名上声明的通用信息来驱动。

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

【注】因为GenericConverter是一个更复杂的SPI接口,只有当您需要它时才可以使用它。转换器或ConverterFactory进行基本类型转换的需要。
ConditionalGenericConverter
有时,只有Converter在特定条件成立时才需要执行。例如,Converter如果目标字段中存在特定注释,则可能只需要执行此操作。或者,Converter如果static valueOf在目标类上定义了一个特定的方法,例如一个方法,那么你可能只想执行一个。 ConditionalGenericConverter是的工会GenericConverter和ConditionalConverter接口,使您可以定义此类定制匹配的标准:

public interface  ConditionalConverter {
    boolean  matches(TypeDescriptor sourceType,TypeDescriptor targetType);
}
public interface  ConditionalGenericConverter extends GenericConverterConditionalConverter {
}

一个很好的例子ConditionalGenericConverter是EntityConverter,它在持久性实体标识符和实体引用之间进行转换。如果目标实体类型声明静态查找器方法,则这样的EntityConverter可能仅匹配 findAccount(Long)。你将执行这样一个finder方法检查的执行 matches(TypeDescriptor, TypeDescriptor)。

9.5.4 ConversionService API

ConversionService定义了一个用于在运行时执行类型转换逻辑的统一API。转换器通常在这个立面界面后面执行:

package org.springframework.core.convert;
public interface  ConversionService {
    boolean canConvert(Class <?> sourceType,Class <?> targetType);
    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配置提供了便利的工厂。

9.5.5配置ConversionService

ConversionService是一种无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在Spring应用程序中,您通常为每个Spring容器(或ApplicationContext)配置一个ConversionService实例。该转换服务将被Spring接收,然后在需要框架执行类型转换时使用。您也可以将此ConversionService注入任何bean,并直接调用。
【注】如果没有向Spring注册ConversionService,则使用原始的基于PropertyEditor的系统。
要使用Spring注册默认ConversionService,请添加以下bean定义

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章节的第22.16.3节”转换和格式化”。
在某些情况下,您可能希望在转换期间应用格式。有关使用的详细信息 ,请参见 第9.6.3节”FormatterRegistry SPI”FormattingConversionServiceFactoryBean。

9.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的方法,但它不能与更复杂的类型(例如参数化元素的集合)一起使用。如果你想转换List的Integer到List的String程序,例如,你需要提供的源和目标类型的正式定义。
幸运的是,TypeDescriptor提供了多种选择,使之简单化:

DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ....
cs.convert(input,
    TypeDescriptor.forObject(input),//列表<Integer>的类型描述符 
    TypeDescriptor.collection(List.class,TypeDescriptor.valueOf(Stringt.class)));

请注意,DefaultConversionService寄存器自动转换,适用于大多数环境。这包括收集器,标转换器,也基本Object到String转换器。可以ConverterRegistry使用类 上的静态 addDefaultConverters方法注册相同的转换器DefaultConversionService。
值类型转换器将被重新用于数组和集合,所以没有必要创建一个特定的转换器从转换Collection的S到 Collection的T,假设标准收集处理是适当的。

9.6 Spring格式化

如上一节所述,core.convert是一种通用型转换系统。它提供了一个统一的ConversionService API以及强类型的转换器SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring容器使用此系统绑定bean属性值。此外,Spring表达式语言(Spel)和DataBinder都使用此系统绑定字段值。例如,当使用SpEL需要强迫一个Short到一个Long完成的expression.setValue(Object bean, Object value)尝试,core.convert系统执行强制。
现在考虑一个典型的客户端环境(如Web或桌面应用程序)的类型转换要求。在这样的环境中,您通常会从String转换 为支持客户端回发过程,以及返回到String以支持视图呈现过程。此外,您经常需要本地化String值。更通用的core.convert转换器SPI不直接解决这种格式化要求。为了直接解决它们,Spring 3引入了一个方便的Formatter SPI,为客户端环境提供了一个简单而强大的PropertyEditor替代方案。
一般来说,当需要实现通用型转换逻辑时,请使用转换器SPI; 例如,用于在java.util.Date和java.lang.Long之间进行转换。当您在客户端环境(如Web应用程序)中工作时,请使用格式化程序SPI,并且需要解析和打印本地化的字段值。ConversionService为两个SPI提供统一的类型转换API。

9.6.1格式化SPI

格式化SPI实现字段格式化逻辑是简单和强类型:

package org.springframework.format;
public interface Formatter <T> extends  Printer<T>, Parser<T> {
}
Formatter从Printer and Parser解析器构建块接口扩展的地方:
public interface Printer <T> {
    String print(T fieldValue,Locale locale);
}
import java.text.ParseException;
public interface Parser<T> {
    T parse(String clientValue,Locale locale)throws ParseException;
}

要创建自己的Formatter,只需实现上面的Formatter界面。参数化T为要格式化的对象的类型,例如 java.util.Date。执行print()操作以输出用于在客户端区域设置中显示的T的实例。实现parse()从客户端语言环境返回的格式化表达式解析T实例的操作。如果解析尝试失败,则您的Formatter应抛出ParseException或IllegalArgumentException异常。注意确保您的Formatter实现是线程安全的。
format为方便起见,在子包中提供了几个Formatter实现。该number包提供了一个NumberFormatter,,CurrencyFormatter并 使用a PercentFormatter格式化java.lang.Number对象java.text.NumberFormat。该datetime包提供了一个DateFormatter用于格式化java.util.Date对象java.text.DateFormat。该datetime.joda软件包基于Joda Time库提供全面的日期时间格式支持。
考虑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){
         ifdate == null){
             return "" ;
        }
    return getDateFormat(locale).format(date);
    }
    public Date parse(String formatted,Locale locale)throws ParseException {
         if(formatted.length()== 0){
             return null;
        }
    return getDateFormat(locale).parse(formatted);
    }
    protected DateFormat getDateFormat(Locale locale){
        DateFormat dateFormat = new SimpleDateFormat(this .pattern,locale);
        dateFormat.setLenient(false);
    return dateFormat;
    }
}

Spring团队欢迎社区推动的Formatter贡献; 见 jira.spring.io贡献。

9.6.2注释驱动格式化

如您所见,字段格式可以通过字段类型或注释进行配置。要将注释绑定到格式化程序,请执行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()返回解析器解析一个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 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
org.springframework.format.annotation 包中存在便携式格式注释API 。使用@NumberFormat格式化java.lang.Number字段。使用@DateTimeFormat格式化java.util.Date,java.util.Calendar,java.util.Long或Joda时间字段。
下面的示例使用@DateTimeFormat将java.util.Date格式化为ISO日期(yyyy-MM-dd):

public class MyModel {
    @DateTimeFormat(iso = ISO.DATE)
    private Date date;
}

9.6.3 FormatterRegistry SPI

FormatterRegistry是用于注册格式化器和转换器的SPI。 FormattingConversionService是适用于大多数环境的FormatterRegistry的实现。该实现可以以编程方式或声明式配置为使用Spring Bean FormattingConversionServiceFactoryBean。因为这个实现也实现了ConversionService,它可以直接配置为与Spring的DataBinder和Spring表达式语言(Spel)一起使用。
请查看下面的FormatterRegistry SPI:

package org.springframework.format;
public interface  FormatterRegistry extends ConverterRegistry {
    void addFormatterForFieldType(Class <?> fieldType,Printer <?> printer,Parser <?> parser);
    void addFormatterForFieldType(Class <?> fieldType,Formatter <?> formatter);
    void addFormatterForFieldType(Formatter <?> formatter);
    void addFormatterForAnnotation(AnnotationFormatterFactory <??> factory);
}

如上所示,可以通过fieldType或注释注册Formatter。
FormatterRegistry SPI允许您集中配置格式化规则,而不是在控制器上复制此类配置。例如,您可能希望强制执行所有日期字段格式化为某种方式,否则具有特定注释的字段将以某种方式进行格式化。使用共享的FormatterRegistry,您可以定义这些规则一次,并且在需要格式化时应用它们。

9.6.4 FormatterRegistrar SPI

FormatterRegistrar是通过FormatterRegistry注册格式化器和转换器的SPI:

package org.springframework.format;
public interface  FormatterRegistrar {
    void registerFormatters(FormatterRegistry register);
}

在为给定的格式化类别注册多个相关转换器和格式化程序(如日期格式化)时,FormatterRegistrar很有用。在声明性注册不足的情况下也是有用的。例如,当格式化程序需要在与其自己的不同的特定字段类型下索引时,或者在注册打印机/解析器对时进行索引。下一节提供有关转换器和格式化器注册的更多信息。

9.6.5配置Spring MVC中的格式化

参见Spring MVC章节的第22.16.3节”转换和格式化”。

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

默认情况下,未注释的日期和时间字段@DateTimeFormat使用DateFormat.SHORT样式从字符串转换。如果你愿意,你可以通过定义你自己的全局格式来改变。
您将需要确保Spring不注册默认格式化程序,而应手动注册所有格式化程序。使用 org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar或 org.springframework.format.datetime.DateFormatterRegistrar类,取决于您是否使用Joda Time库。
例如,以下Java配置将注册全局”yyyyMMdd”格式。此示例不依赖于Joda Time库:

@Configuration 
public class AppConfig {
     @Bean
     public FormattingConversionService conversionService(){
        //使用DefaultFormattingConversionService,但不要注册默认值 
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
    //确保@NumberFormat仍然受支持 
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormoryFactory());
    //使用特定的全局格式注册日期转换 
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);
    return conversionService;
    }
}

如果您喜欢基于XML的配置,您可以使用 FormattingConversionServiceFactoryBean。这是同样的例子,这次使用Joda时间:

<?xml version ="1.0"encoding ="UTF-8"> <beans  xmlns = "http://www.springframework.org/schema/beans" 
    xmlnsxsi = "http://www.w3.org /2001 /XMLSchema-instance" 
    xsischemaLocation = "
        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>

【注】 约达时间提供单独的不同类型来表示date,time和date-time 的值。的dateFormatter,timeFormatter和dateTimeFormatter的性质JodaTimeFormatterRegistrar,应使用来配置不同的格式为每种类型。它DateTimeFormatterFactoryBean提供了一种方便的方式来创建格式化程序。
如果使用Spring MVC,请记住要明确配置所使用的转换服务。对于Java,@Configuration这意味着扩展 WebMvcConfigurationSupport类并覆盖该mvcConversionService()方法。对于XML,您应该使用元素的’conversion-service’属性 mvc:annotation-driven。详见第22.16.3节”转换和格式化”。

9.8 Spring验证

Spring 3对其验证支持进行了几个增强。首先,JSR-303 Bean Validation API现在完全支持。第二,当以编程方式使用时,Spring的DataBinder现在可以验证对象以及绑定它们。第三,Spring MVC现在支持声明式验证@Controller输入。

9.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 = 64private String name;
    @Min0private int age;
}

当此类的实例由JSR-303验证器验证时,这些约束将被强制执行。
有关JSR-303 /JSR-349的一般信息,请参阅Bean验证网站。有关默认参考实现的具体功能的信息,请参阅Hibernate Validator文档。要了解如何将Bean验证提供程序设置为Spring bean,请继续阅读。

9.8.2配置Bean验证提供程序

Spring提供对Bean验证API的全面支持。这包括方便的支持将JSR-303 /JSR-349 Bean验证提供程序作为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 Validator)将存在于类路径中,并将自动检测。
注入验证器
LocalValidatorFactoryBean同时实现了javax.validation.ValidatorFactory和 javax.validation.Validator,以及Spring的org.springframework.validation.Validator。您可以将这些接口中的任一个注入到需要调用验证逻辑的bean中。
javax.validation.Validator如果您喜欢直接使用Bean验证API,请注意引用:

import javax.validation.Validator;
@Service 
public class MyService {
    @Autowired
    private Validator validator;
引用一个引用,org.springframework.validation.Validator如果你的bean需要Spring Validation API:
import org.springframework.validation.Validator;
@Service 
public class MyService {
    @Autowired
    private Validator validator;
}

配置自定义约束
每个Bean验证约束由两部分组成。首先,@Constraint声明约束及其可配置属性的注释。其次,实现了javax.validation.ConstraintValidator实现约束行为的接口。为了将声明与实现相关联,每个@Constraint注释引用相应的ValidationConstraint实现类。在运行时,ConstraintValidatorFactory在域模型中遇到约束注解时,会 实例化引用的实现。
默认情况下,LocalValidatorFactoryBean配置SpringConstraintValidatorFactory 使用Spring创建ConstraintValidator实例的a。这允许您的自定义ConstraintValidator从任何其他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实现可能具有像任何其他Spring bean一样的依赖关系@Autowired。
Spring方法验证
Bean验证1.1支持的方法验证功能以及Hibernate Validator 4.3中的自定义扩展可以通过MethodValidationPostProcessorbean定义集成到Spring上下文中:

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

为了符合Spring驱动方法验证的资格,所有目标类都需要使用Spring的@Validated注释注释,可选地声明验证组使用。MethodValidationPostProcessor使用Hibernate Validator和Bean Validation 1.1提供程序查看javadocs的设置详细信息。
附加配置选项
在LocalValidatorFactoryBean大多数情况下,默认配置应该足够。有各种Bean验证结构的配置选项,从消息插入到遍历分辨率。有关LocalValidatorFactoryBean这些选项的更多信息,请参阅 javadocs。

9.8.3配置DataBinder

从Spring 3起,可以使用Validator配置DataBinder实例。一旦配置,验证器可以通过调用来调用binder.validate()。任何验证错误都会自动添加到绑定器的BindingResult中。
以编程方式使用DataBinder时,可以在绑定到目标对象之后调用验证逻辑:

Foo target = new Foo();
DataBinder binder = new DataBinder( target);
binder.setValidator(new FooValidator());//绑定到目标对象
binder.bind(propertyValues);//验证目标对象
binder.validate();//得到BindingResult,其中包含任何验证错误 
BindingResult results = binder.getBindingResult();

DataBinder还可以Validator通过dataBinder.addValidators和配置多个实例 dataBinder.replaceValidators。当将全局配置的Bean验证与Validator在DataBinder实例上本地配置的Spring进行组合时,此功能非常有用。

9.8.4 Spring MVC 3验证

参见Spring MVC章节的第22.16.4节”验证”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值