将验证视为业务逻辑有利有弊,Spring为验证和数据绑定提供了一种设计,并不排斥其中任何一种。具体来说,验证不应该与Web层捆绑在一起,应该易于本地化,而且应该可以插入任何可用的验证器(validator)。考虑到这些问题,Spring提供了一个 Validator 约定,它既是基本的,又可以在应用程序的每个层中使用。 数据绑定对于让用户输入动态地绑定到应用程序的domain模型(或你用来处理用户输入的任何对象)非常有用。

Spring提供了一个被恰当地命名为 DataBinder 的工具来实现这一目的。Validator 和 DataBinder 组成了 validation 包,它主要用于但不限于Web层。 BeanWrapper 是Spring框架的一个基本概念,在很多地方都会用到。然而,你可能不需要直接使用 BeanWrapper。然而,由于这是参考文档,我们觉得可能需要做一些解释。我们在本章中解释了 BeanWrapper,因为如果你要使用它,你很可能是在试图将数据绑定到对象上的时候。 Spring的 DataBinder 和低级别的 BeanWrapper 都使用 PropertyEditorSupport 实现来解析和格式化属性值。PropertyEditor 和 PropertyEditorSupport 类型是JavaBeans规范的一部分,在本章也有解释。Spring的 core.convert 包提供了一个通用的类型转换工具,以及一个用于格式化UI字段值的更高层次的 format 包。你可以使用这些包作为 PropertyEditorSupport 实现的更简单的替代品。本章也会讨论它们。 Spring通过设置基础设施和Spring自己的 Validator 适配器来支持Java Bean验证。应用程序可以在全局范围内启用一次Bean Validation,如 Java Bean 校验 中所述,并专门使用它来满足所有验证需求。在Web层,应用程序可以进一步为每个 DataBinder 注册 controller 本地的Spring Validator 实例,如配置 配置 DataBinder 中所述,这对于插入自定义验证逻辑非常有用。

一、 使用 Spring 的 Validator 接口进行验证

Spring有一个 Validator 接口,你可以用它来验证对象。Validator 接口通过使用一个 Errors 对象来工作,这样,在验证时,验证器可以向 Errors 对象报告验证失败。

考虑下面这个小数据对象的例子:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

下一个例子通过实现 org.springframework.validation.Validator 接口的以下两个方法为 Person 类提供验证行为。

  • supports(Class): 这个 Validator 可以验证提供的 Class 的实例吗?
  • validate(Object, org.springframework.validation.Errors): 验证给定的对象,如果出现验证错误,则用给定的 Errors 对象登记这些错误。

实现一个 Validator 是相当简单的,特别是当你知道Spring框架也提供的 ValidationUtils 帮助类时。下面的例子为 Person 实例实现了 Validator:

public class PersonValidator implements Validator {

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

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

ValidationUtils 类的 static rejectIfEmpty(..) 方法用于拒绝 name 属性,如果它是 null 或空字符串。请看 ValidationUtils javadoc,看看除了前面显示的例子外,它还提供了哪些功能。 当然,实现一个单一的 Validator 类来验证富对象中的每个嵌套对象是可能的,但最好是将每个嵌套类对象的验证逻辑封装在自己的 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/> 标签来检查错误信息,但你也可以自己检查 Errors 对象

二、 将code解析为错误信息

我们涵盖了数据绑定和验证。本节涵盖了输出与验证错误相对应的信息。在上一节 所示的例子中,我们拒绝了 name 和 age 字段。如果我们想通过使用 MessageSource 来输出错误信息,我们可以使用拒绝字段时提供的错误代码(本例中为’name’和’age')来实现。当你调用(无论是直接调用,还是间接调用,例如使用 ValidationUtils 类)rejectValue 或 Errors 接口的其他 reject 方法之一时,底层实现不仅会注册你传入的代码,还会注册一些额外的错误代码。 MessageCodesResolver 决定了 Errors 接口注册哪些错误代码。默认情况下,使用 DefaultMessageCodesResolver,它(例如)不仅用你给的代码注册了一条消息,而且还注册了包括你传递给拒绝方法的字段名的消息。因此,如果你使用 rejectValue("age", "too.darn.old") 拒绝一个字段,除了 too.darn.old 代码外,Spring 还注册了 too.darn.old.age 和 too.darn.old.age.int(第一个包括字段名,第二个包括字段的类型)。这样做是为了方便开发者针对错误信息进行处理。

 三、Bean 操作和 BeanWrapper

org.springframework.beans 包遵守 JavaBeans 标准。JavaBean是一个具有默认无参数构造函数的类,它遵循一个命名惯例,(例如)一个名为 bingoMadness 的属性将有一个setter方法 setBingoMadness(..) 和一个getter方法 getBingoMadness()

eans包中一个相当重要的类是 BeanWrapper 接口及其相应的实现(BeanWrapperImpl)。正如在javadoc中引用的那样,BeanWrapper 提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是否可读或可写的功能。此外,BeanWrapper 还提供了对嵌套属性的支持,使得对子属性的属性设置可以达到无限的深度。BeanWrapper 还支持添加标准的JavaBeans PropertyChangeListeners 和 VetoableChangeListeners 的能力,而不需要在目标类中添加支持代码。最后但同样重要的是,BeanWrapper 提供了对设置索引属性的支持。BeanWrapper 通常不被应用程序代码直接使用,而是被 DataBinder 和 BeanFactory 使用。

BeanWrapper 的工作方式在一定程度上可以从它的名字中看出:它包装一个Bean来对该Bean进行操作,例如设置和检索属性。

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

设置和获取属性是通过 BeanWrapper 的 setPropertyValue 和 getPropertyValue 重载方法变体完成的。详情请参见它们的Javadoc。下表显示了这些约定的一些例子。

Table 11. 属性示例

表达式

说明

name

表示与 getName() 或 isName() 和 setName(..) 方法相对应的属性 name

account.name

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

account[2]

表示索引属性 account 的第三个元素。索引属性可以是 arraylist 或其他自然有序的集合类型。

account[COMPANYNAME]

表示由 account Map 属性的 COMPANYNAME key 索引的 map 条目的值。

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

public class Company {

    private String name;
    private Employee managingDirector;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Employee getManagingDirector() {
        return this.managingDirector;
    }

    public void setManagingDirector(Employee managingDirector) {
        this.managingDirector = managingDirector;
    }
}
public class Employee {

    private String name;

    private float salary;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getSalary() {
        return salary;
    }

    public void setSalary(float salary) {
        this.salary = salary;
    }
}

下面的代码片断显示了一些如何检索和操作实例化的 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");

2. 内置的 PropertyEditor 实现

Spring使用 PropertyEditor 的概念来实现 Object 和 String 之间的转换。用不同于对象本身的方式来表示属性是很方便的。例如,一个 Date 可以用人类可读的方式表示(如 String'2007-14-09'),而我们仍然可以将人类可读的形式转换回原始日期(或者,甚至更好,将任何以人类可读形式输入的日期转换回 Date 对象)。这种行为可以通过注册 java.beans.PropertyEditor 类型的自定义编辑器来实现。在 BeanWrapper 上注册自定义编辑器,或者在特定的IoC容器中注册(如前一章中提到的),使其了解如何将属性转换为所需类型。

有几个在Spring中使用属性(property)编辑的例子。

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

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

Table 12. 内置的  PropertyEditor 实现



说明

ByteArrayPropertyEditor

字节数组的编辑器。将字符串转换为其相应的字节表示。默认由 BeanWrapperImpl 注册。

ClassEditor

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

CustomBooleanEditor

用于 Boolean 属性的可定制的属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但可以通过注册它的一个自定义实例作为自定义编辑器来重写。

CustomCollectionEditor

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

CustomDateEditor

java.util.Date 的可定制属性编辑器,支持自定义 DateFormat。默认情况下没有注册。必须根据需要由用户注册适当的格式(format)。

CustomNumberEditor

可定制的属性编辑器,用于任何 Number 子类,如 IntegerLongFloat 或 Double。默认情况下,由 BeanWrapperImpl 注册,但可以通过注册它的一个自定义实例作为自定义编辑器来重写。

FileEditor

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

InputStreamEditor

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

LocaleEditor

可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant],与 Locale 的 toString() 方法相同)。也接受空格作为分隔符,作为下划线的替代。默认情况下,由 BeanWrapperImpl 注册。

PatternEditor

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

PropertiesEditor

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

StringTrimmerEditor

trim 字符串的属性编辑器。可选择允许将空字符串转换为 null 值。默认情况下没有注册 - 必须由用户注册。

URLEditor

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

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

下面是引用的 SomethingBeanInfo 类的Java源代码,它将一个 CustomNumberEditor 与 Something 类的 age 属性联系起来:

public class SomethingBeanInfo extends SimpleBeanInfo {

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

注册额外的自定义 PropertyEditor 实现

当把Bean属性设置为字符串值时,Spring IoC容器最终会使用标准的JavaBeans PropertyEditor 实现来把这些字符串转换成属性的复杂类型。Spring预先注册了一些自定义的 PropertyEditor 实现(例如,将表示为字符串的类名转换为 Class 对象)。此外,Java的标准JavaBeans PropertyEditor 查询机制让一个类的 PropertyEditor 被适当地命名,并与它提供支持的类放在同一个包里,这样它就能被自动找到。

如果需要注册其他的自定义 PropertyEditors,有几种机制可用。最手动的方法是使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法,假设你有一个 BeanFactory 引用,这种方法通常并不方便,也不推荐。另一种(略微更方便)的机制是使用一个特殊的Bean工厂后处理器,叫做 CustomEditorConfigurer。尽管你可以使用 BeanFactory 实现的Bean工厂后处理器,但 CustomEditorConfigurer 有一个嵌套的属性设置,所以我们强烈建议你在 ApplicationContext 中使用它,在那里你可以以类似于任何其他bean的方式部署它,并且它可以被自动检测和应用。

请注意,所有的Bean工厂和应 application context 都会通过使 用BeanWrapper 来处理属性转换,自动使用一些内置的属性编辑器。BeanWrapper 注册的标准属性编辑器在 上一节 中列出。此外, ApplicationContext 还覆盖或添加额外的编辑器,以适合特定 application context 类型的方式处理资源查找。

标准JavaBeans的 PropertyEditor 实例被用来将以字符串表示的属性值转换为属性的实际复杂类型。你可以使用 CustomEditorConfigurer,一个bean工厂的后处理程序,方便地在 ApplicationContext 中添加对额外的 PropertyEditor 实例的支持。

考虑下面的例子,它定义了一个叫 ExoticType 的 user 类和另一个叫 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定义显示了如何设置这种关系:

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

PropertyEditor 的实现可以类似于以下内容:

package example;

import java.beans.PropertyEditorSupport;

// converts string representation to ExoticType object
public class ExoticTypeEditor extends PropertyEditorSupport {

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

最后,下面的例子显示了如何使用 CustomEditorConfigurer 向 ApplicationContext 注册新的 PropertyEditor,然后 ApplicationContext 将能够根据需要使用它:

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

使用 PropertyEditorRegistrar

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

下面的例子展示了如何创建你自己的 PropertyEditorRegistrar 实现:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // it is expected that new PropertyEditor instances are created
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // you could register as many custom property editors as are required here...
    }
}

四、Spring 类型转换

core.convert 包提供了一个通用的类型转换系统。该系统定义了一个用于实现类型转换逻辑的SPI和一个用于在运行时执行类型转换的API。在Spring容器中,你可以使用这个系统作为 PropertyEditor 实现的替代品,将外化的Bean属性值字符串转换为所需的属性类型。你也可以在你的应用程序中任何需要类型转换的地方使用public API。

1. 转换器(Converter) SPI

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

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

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

对于每个对 convert(S) 的调用,源参数被保证不为空。如果转换失败,你的 Converter 可以抛出任何未经检查的异常。特别是,它应该抛出一个 IllegalArgumentException 来报告一个无效的源值。请注意确保你的 Converter 实现是线程安全的。

core.convert.support 包中提供了几个converter的实现,作为一种便利。这些包括从字符串到数字和其他常见类型的转换器。下面的列表显示了 StringToInteger 类,它是一个典型的 Converter 实现:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

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

2. 使用 ConverterFactory

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

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

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

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

考虑以 StringToEnumConverterFactory 为例:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

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

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

        private Class<T> enumType;

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

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

3. 使用 GenericConverter

当你需要一个复杂的 Converter 实现时,考虑使用 GenericConverter 接口。 GenericConverter 具有比 Converter 更灵活但不那么强类型的签名,支持在多种源和目标类型之间进行转换。此外,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 会对声明目标集合类型的字段进行内省,以解决集合的元素类型。这让源数组中的每个元素在集合被设置在目标字段上之前被转换为集合元素类型。

使用 ConditionalGenericConverter

有时,你想让一个 Converter 只在一个特定的条件成立的情况下运行。例如,你可能想只在目标字段上存在特定注解的情况下运行一个 Converter,或者你可能想只在目标类上定义了特定的方法(比如 static valueOf 方法)的情况下运行一个 Converter。 ConditionalGenericConverter 是 GenericConverter 和 ConditionalConverter 接口的联合体,可以让你定义这样的自定义匹配标准:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

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

4. ConversionService API

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

package org.springframework.core.convert;

public interface ConversionService {

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

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

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

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

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

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

5. 配置 ConversionService

ConversionService 是一个无状态的对象,旨在应用启动时被实例化,然后在多个线程之间共享。在Spring应用程序中,你通常为每个Spring容器(或 ApplicationContext)配置一个 ConversionService 实例。当框架需要进行类型转换时,Spring会拾取该 ConversionService 并使用它。你也可以将这个 ConversionService 注入到你的任何Bean中并直接调用它。

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

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

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

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

在Spring MVC应用程序中使用 ConversionService 也很常见。

6. 以编程方式使用 ConversionService

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

@Service
public class MyService {

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

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

对于大多数用例,你可以使用指定 targetType 的 convert 方法,但对于更复杂的类型,如一个泛型的集合,它是不起作用的。例如,如果你想以编程方式将 List<Integer> 转换成 List<String>,你需要提供源和目标类型的正式定义。

幸运的是,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 会自动注册适合大多数环境的转换器。这包括集合转换器、标量转换器和基本的 Object 到 String 的转换器。你可以通过使用 DefaultConversionService 类的静态 addDefaultConverters 方法在任何 ConverterRegistry 中注册相同的转换器。

值类型的转换器被重复用于数组和集合,所以不需要创建一个特定的转换器来将 S 的 Collection 转换为 T 的 Collection,假设标准的集合处理是合适的

五. Spring 字段格式化

正如上一节所讨论的, core.convert 是一个通用的类型转换系统。它提供了一个统一的 ConversionService API以及一个强类型的 Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring容器使用这个系统来绑定Bean的属性值。此外,Spring表达式语言(SpEL)和 DataBinder 都使用这个系统来绑定字段值。例如,当SpEL需要将 Short 强制转换为 Long 以完成 expression.setValue(Object bean, Object value) 的尝试时,core.convert 系统会执行强制转换的操作。 现在考虑一下典型的客户端环境的类型转换要求,比如说web或桌面应用程序。在这样的环境中,你通常要从 String 转换,以支持客户端的postback过程,以及回到 String 以支持视图的渲染过程。此外,你经常需要对 String 值进行本地化。更加通用的 core.convert Converter SPI并没有直接解决这样的格式化要求。为了直接解决这些问题,Spring提供了一个方便的 Formatter SPI,为客户端环境的 PropertyEditor 实现提供了一个简单而强大的选择。 一般来说,当你需要实现通用的类型转换逻辑时,你可以使用 Converter SPI - 例如,在 java.util.Date 和 Long 之间进行转换。当你在客户端环境中工作(如Web应用程序)并需要解析和打印本地化的字段值时,你可以使用 Formatter SPI。ConversionService 为这两个SPI提供了一个统一的类型转换API。

1. Formatter SPI

用于实现字段格式化逻辑的 Formatter SPI是简单的、强类型的。下面的列表显示了 Formatter 接口的定义:

package org.springframework.format;

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

Formatter 扩展自 Printer 和 Parser 构件接口。下面的列表显示了这两个接口的定义:

public interface Printer<T> {

    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {

    T parse(String clientValue, Locale locale) throws ParseException;
}

要创建你自己的 Formatter,实现前面所示的 Formatter 接口。将 T 参数化为你希望格式化的对象的类型—例如,java.util.Date。实现 print() 操作,打印一个 T 的实例,以便在客户机上显示。实现 parse() 操作,从客户端 locale 返回的格式化表示中解析 T 的一个实例。如果解析尝试失败,你的 Formatter 应该抛出一个 ParseException 或 IllegalArgumentException。请注意确保你的 Formatter 的实现是线程安全的。

format 子包提供了几个 Formatter 的实现,作为一种方便。Number 包提供 NumberStyleFormatterCurrencyStyleFormatter 和 PercentStyleFormatter,用于格式化使用 java.text.NumberFormat 的 Number 对象。datetime 包提供了一个 DateFormatter 来格式化使用 java.text.DateFormat 的 java.util.Date 对象。

下面的 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;
    }
}

2. 注解驱动的格式化

字段格式化可以通过字段类型或注解来配置。为了将注解绑定到 Formatter,需要实现 AnnotationFormatterFactory。下面的列表显示了 AnnotationFormatterFactory 接口的定义

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

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

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

要创建一个实现。

  1. 将 A 参数化为你希望与之关联格式化逻辑的字段 annotationType--例如 org.springframework.format.annotation.DateTimeFormat
  2. 让 getFieldTypes() 返回注解可以使用的字段类型。
  3. 让 getPrinter() 返回一个 Printer 来打印一个注解字段的值。
  4. 让 getParser() 返回一个 Parser 来解析一个注解字段的 clientValue

下面的例子 AnnotationFormatterFactory 的实现将 @NumberFormat 注解绑定到一个 formatter 上,让数字样式或pattern被指定:

public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {

    private static final Set<Class<?>> FIELD_TYPES = Set.of(Short.class,
            Integer.class, Long.class, Float.class, Double.class,
            BigDecimal.class, BigInteger.class);

    public Set<Class<?>> getFieldTypes() {
        return FIELD_TYPES;
    }

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

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

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

为了触发格式化,你可以用 @NumberFormat 来注解字段,如下例所示:

public class MyModel {

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

格式化注解API

org.springframework.format.annotation 包中存在一个可移植的format 注解 API。你可以使用 @NumberFormat 来格式化 Double 和 Long 等 Number 字段,使用 @DateTimeFormat 来格式化 java.util.Datejava.util.CalendarLong(用于毫秒级时间戳)以及 JSR-310 java.time

下面的例子使用 @DateTimeFormat 将一个 java.util.Date 格式化为ISO日期(yyyy-MM-dd):

public class MyModel {

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

六. 配置全局的 Date 和 Time 的格式

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

要做到这一点,请确保Spring不注册默认的formatter。相反,在以下工具的帮助下手动注册formatter。

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

例如,下面的 Java 配置注册了一个全局 YYYMMDD 格式:

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

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

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

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

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

        return conversionService;
    }
}

七、Java Bean 校验

1. Bean 校验(Validation)的概述

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

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

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

Bean Validation可以让你声明约束,如下例所示:

public class PersonForm {

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

    @Min(0)
    private int age;
}