天天看点

Spring 5 中文解析核心篇-IoC容器之数据校验、数据绑定和类型转换

将验证视为业务逻辑有其优缺点,Spring提供的验证(和数据绑定)设计不排除其中任何一种。具体来说,验证不应与Web层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring提供了一个

Validator

契约,该契约既基本又可以在应用程序的每个层中使用。

数据绑定对于使用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring提供了恰当地命名为

DataBinder

的功能。

Validator

DataBinder

validation

包组成,被主要的使用但不仅限于web层。

BeanWrapper

在Spring框架中是一个基本的概念并且在许多地方被使用到。然而,你大概不需要直接地使用

BeanWrapper

。但是,由于这是参考文档,所以我们认为可能需要一些解释。我们将在本章中解释

BeanWrapper

,因为如果你要使用它,那么在尝试将数据绑定到对象时最有可能使用它。

Spring的

DataBinder

和低级别

BeanWrapper

两者使用

PropertyEditorSupport

实现去解析和格式化属性值。

PropertyEditor

PropertyEditorSupport

类型是JavaBeans规范的一部分并且在这个章节进行解释。Spring 3开始引入了

core.convert

包,该包提供了常规的类型转换工具,以及用于格式化UI字段值的高级“

format

”包。你可以将这些包用作

PropertyEditorSupport

实现的更简单替代方案。这些也会在这个章节讨论。

Spring通过安装基础设计和适配Spring的

Validator

契约提供JavaBean校验。应用程序可以全局一次启用Bean验证,像在 JavaBean校验 中描述一样,并且仅将其用于所有验证需求。在Web层中,应用程序可以每个

DataBinder

进一步注册控制器本地的Spring

Validator

实例,如配置

DataBinder

中所述,这对于插入自定义验证逻辑很有用。

3.1 通过使用Spring的校验接口校验

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

类上的静态

rejectIfEmpty(...)

方法用于拒绝

name

属性(如果该属性为

null

或空字符串)。查看

ValidationUtils

javadoc,看看它除了提供前面显示的示例外还提供什么功能。

虽然可以实现单个验证器类来验证对象中的每个嵌套对象,但更好的做法是将每个嵌套对象类的验证逻辑封装到自己的验证器实现中。一个“

丰富

”对象的简单示例是一个由两个String属性(第一个和第二个名字)和一个复杂的

Address

对象组成的

Customer

Address

对象可以独立于

Customer

对象使用,因此已经实现了独特的

AddressValidator

。如果希望

CustomerValidator

重用

AddressValidator

类中包含的逻辑而需要复制和粘贴,则可以在

CustomerValidator

中依赖注入或实例化一个

AddressValidator

,如以下示例所示:

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

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

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

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

验证错误将报告给传递给验证器的

Errors

对象。在Spring Web MVC场景中,你可以使用

<spring:bind/>

标签去检查错误信息,但是你也可以自己检查

Errors

对象。更多关于提供的信息在

Javadoc

中。

参考代码:

com.liyong.ioccontainer.service.validator.ValidatorTest

3.2 解析码到错误信息

我们介绍了数据绑定和校验。本节介绍与验证错误对应的输出消息。在上一节显示的例子中,我们拒绝

name

age

字段。如果我们想使用

MessageSource

去输出错误信息,我们可以使用提供的错误码,当拒绝字段时(在这个场景中

name

age

)。当你

Errors

接口调用(直接地或间接地,通过使用

ValidationUtils

类)

rejectValue

或其他

reject

方法之一时,底层的实现不仅注册你传递的码,而且还注册一些附加的错误码。

MessageCodesResolver

确定哪一个错误码注册到

Errors

接口。默认情况下,使用

DefaultMessageCodesResolver

,它(例如)不仅使用你提供的代码注册消息,而且还注册包含传递给拒绝方法的字段名称的消息。因此,如果你通过使用

rejectValue(“age”,“too.darn.old”)

拒绝字段,则除了

too.darn.old

代码外,Spring还将注册

too.darn.old.age

too.darn.old.age.int

(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。

更多

MessageCodesResolver

上和默认策略信息可以分别地在

MessageCodesResolver

DefaultMessageCodesResolver

javadoc中找到。

3.3 bean操作和BeanWrapper

这个

org.springframework.beans

包遵循JavaBeans标准。JavaBean是具有默认无参数构造函数的类,并且遵循命名约定,在该命名约定下,例如:名为

bingoMadness

的属性将具有

setter

方法

setBingoMadness(..)

getter

getBingoMadness()

。更多关于JavaBean信息和规范,查看

javaBeans

beans

包中一个非常重要的类是

BeanWrapper

接口和它的对应实现(

BeanWrapperImpl

)。就像从Javadoc引言的那样,

BeanWrapper

提供了以下功能:设置和获取属性值(单独或批量),获取属性描述符以及查询属性以确定它们是否可读或可写。此外,

BeanWrapper

还支持嵌套属性,从而可以将子属性上的属性设置为无限深度。

BeanWrapper

还支持添加标准JavaBeans 的

PropertyChangeListeners

VetoableChangeListeners

的功能,而无需在目标类中支持代码。最后但并非不重要的一点是,

BeanWrapper

支持设置索引属性。

BeanWrapper

通常不直接由应用程序代码使用,而是由

DataBinder

BeanFactory

使用。

BeanWrapper

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

3.3.1 设置和获取基本的和潜入的属性

设置和获取属性是通过

BeanWrapper

的重载方法

setPropertyValue

getPropertyValue

的变体。查看它们的详细文档。下面的表格显示这些约定:

Expression Explanation

name

表示属性

name

对应的

getName()

isName()

setName(..)

方法。

account.name

表示嵌入

account

属性的

name

属性对应的

getAccount().setName()

getAccount().getName()

account[2]

表示2个索引元素属性

account

。索引属性可以是类型

array

list

或其他自然顺序集合。

account[COMPANYNAME]

表示

map

实体的值通过

account

Map属性的key

COMPANYNAME

索引。

(如果你没打算直接使用

BeanWrapper

,下面部分不是至关重要地。如果你仅仅使用

DataBinder

BeanFactory

和他的默认实现,你可以跳过

PropertyEditors

的部分)。

下面两个例子类使用

BeanWrapper

去获取和设置属性:

public class Company {

    private String name;
    private Employee managingDirector;

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

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

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

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

    private String name;

    private float salary;

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

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

    public float getSalary() {
        return salary;
    }

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

以下代码段显示了一些有关如何检索和操纵实例化的

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");           
代码示例:

com.liyong.ioccontainer.service.beanwrapper.BeanWrapperTest

3.3.2 内建

PropertyEditor

Spring使用

PropertyEditor

概念去影响一个对象和字符串之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,日期可以用人类可读的方式表示(如字符串:'

2007-14-09

'),而我们仍然可以将人类可读的形式转换回原始日期(或者更好的是,转换任何日期以人类可读的形式输入到

Date

对象)。通过注册类型为

java.beans.PropertyEditor

的自定义编辑器,可以实现此行为。在

BeanWrapper

上或在特定的IoC容器中注册自定义编辑器(如上一章所述),使它具有如何将属性转换为所需类型的能力。更多关于

PropertyEditor

请参阅Oracle的java.beans包的javadoc

在Spring中使用属性编辑的两个示例:

  • 通过使用

    PropertyEditor

    实现在bean上设置属性。当使用String作为在XML文件中声明的某个bean的属性的值时,Spring(如果相应属性的

    setter

    具有

    Class

    参数)将使用

    ClassEditor

    尝试将参数解析为

    Class

  • 在Spring的MVC框架中,通过使用各种

    PropertyEditor

    实现来解析HTTP请求参数,你可以在

    CommandController

    的所有子类中手动绑定这些实现。

Spring有一个内建的

PropertyEditor

实现。它们都位于

org.springframework.beans.propertyeditors

包中。默认情况下,大多数(但不是全部,如下表所示)由

BeanWrapperImpl

注册。如果可以通过某种方式配置属性编辑器,则仍可以注册自己的变体以覆盖默认变体。

下表描述了Spring提供的各种

PropertyEditor

实现:

Class

ByteArrayPropertyEditor

字节数组的编辑器。将字符串转换为其相应的字节表示形式。默认

BeanWrapperImpl

注册。

ClassEditor

将代表类的字符串解析为实际类,反之亦然。当类没有找到抛出

IllegalArgumentException

。默认

BeanWrapperImpl

CustomBooleanEditor

Boolean

属性的可定制属性编辑器。默认,通过

BeanWrapperImpl

注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖它。

CustomCollectionEditor

集合属性编辑器,转换任何源

Collection

到给定

Collection

类型。

CustomDateEditor

java.util.Date

的可自定义属性编辑器,支持一个自定义

DateFormat

。默认不会被注册。必须根据需要以适当的格式进行用户注册。

CustomNumberEditor

任何

Number

子类可自定义属性编辑器,例如

Integer

Long

Float

Double

。默认,通过

BeanWrapperImpl

FileEditor

解析字符串为

java.io.File

对象。默认,通过

BeanWrapperImpl

InputStreamEditor

单向属性编辑器,它可以采用字符串并生成(通过中间的

ResourceEditor

Resource

)一个

InputStream

,以便可以将

InputStream

属性直接设置为字符串。请注意,默认用法不会为你关闭

InputStream

。默认情况下,由

BeanWrapperImpl

注册

LocaleEditor

可以将字符串解析为

Locale

对象,反之亦然(字符串格式为

[country] [variant]

,类似

Locale

的toString()方法相同)。默认,通过

BeanWrapperImpl

PatternEditor

能够解析字符串为

java.util.regex.Pattern

对象,反之亦然。

PropertiesEditor

可以将字符串(格式设置为

java.util.Properties

类的javadoc中定义的格式)转换为

Properties

对象

StringTrimmerEditor

修剪字符串的属性编辑器。 (可选)允许将空字符串转换为空值。默认不被注册-必须被用户注册。

URLEditor

能够转换一个字符串代表的URL为真实的URL对象。默认,通过

BeanWrapperImpl

java.beans.PropertyEditorManager

去设置属性编辑器可能需要的搜索路径。搜索路径也可以包含

sun.bean.editors

,它包括例如

Font

Color

和大多数原始类型的

PropertyEditor

实现。还要注意,如果标准JavaBeans基础结构与它们处理的类在同一包中并且与该类具有相同的名称,并且附加了

Editor

,则标准JavaBeans基础结构会自动发现

PropertyEditor

类(无需显式注册它们)。例如,可以使用以下类和包结构,这就足以识别

SomethingEditor

类并将其用作某种类型属性的

PropertyEditor

com
  chank
    pop
      Something
      SomethingEditor // SomethingEditor用作Something类           

注意,你也可以在此处使用标准的

BeanInfo

JavaBeans机制(

这里

有所描述)。下面例子使用

BeanInfo

机制去明确地注册一个或多个

PropertyEditor

实例到关联类的属性:

com
  chank
    pop
      Something
      SomethingBeanInfo // BeanInfo用作Something类           

下面是引用的

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) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}           

com.liyong.ioccontainer.service.propertyeditor.PropertyEditorTest

注册附加的自定义

PropertyEditor

当设置bean属性为字符串值时,Spring IoC容器最终地使用标准JavaBean的

PropertyEditor

实现去转换这些字符串为属性的复杂类型。Spring预注册了非常多的自定义

PropertyEditor

实现(例如,将表示为字符串的类名称转换为Class对象)。此外,Java的标准JavaBeans

PropertyEditor

查找机制允许适当地命名类的

PropertyEditor

,并将其与提供支持的类放在同一包中,以便可以自动找到它。

如果需要注册其他自定义

PropertyEditors

,则可以使用几种机制。最手动的方法(通常不方便或不建议使用)是使用

ConfigurableBeanFactory

接口的

registerCustomEditor()

方法,假设你有

BeanFactory

引用。另一种(稍微方便些)的机制是使用称为

CustomEditorConfigurer

的特殊bean工厂后处理器。尽管你可以将Bean工厂后处理器与

BeanFactory

实现一起使用,但

CustomEditorConfigurer

具有嵌套的属性设置,因此我们强烈建议你将其与

ApplicationContext

一起使用,在这里可以将其以与其他任何Bean相似的方式进行部署,并且可以在任何位置进行部署。自动检测并应用。

请注意,所有的bean工厂和应用程序上下文通过使用

BeanWrapper

来处理属性转换,都会自动使用许多内置的属性编辑器。上一节列出了

BeanWrapper

注册的标准属性编辑器。此外,

ApplicationContext

还以适合特定应用程序上下文类型的方式重写或添加其他编辑器,以处理资源查找。

标准JavaBeans

PropertyEditor

实例用于将表示为字符串的属性值转换为属性的实际复杂类型。你可以使用bean工厂的后处理器

CustomEditorConfigurer

来方便地将对其他

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

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

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

// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

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

最后,下面的示例演示如何使用

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>           

com.liyong.ioccontainer.starter.PropertyEditorIocContainer

使用PropertyEditorRegistrar

在Spring容器中注册属性编辑器的其他机制是创建和使用

PropertyEditorRegistrar

。当需要在几种不同情况下使用同一组属性编辑器时,此接口特别有用。你可以在每一种场景中写对应的注册和重新使用。

PropertyEditorRegistrar

实例与一个名为

PropertyEditorRegistry

的接口一起工作,该接口由Spring

BeanWrapper

(和

DataBinder

)实现。与

CustomEditorConfigurer

(在此描述)结合使用时,

PropertyEditorRegistrar

实例特别方便,该实例暴露了名为

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) {

        // 期望创建一个新的PropertyEditor示例
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

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

另请参阅

org.springframework.beans.support.ResourceEditorRegistrar

以获取示例

PropertyEditorRegistrar

实现。注意,在实现

registerCustomEditors(...)

方法时,它如何创建每个属性编辑器的新实例。

下一个示例显示了如何配置

CustomEditorConfigurer

并将其注入我们的

CustomPropertyEditorRegistrar

的实例:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

<bean id="customPropertyEditorRegistrar"
    class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>           

最后(对于使用Spring的MVC Web框架的读者来说,与本章的重点有所偏离),使用

PropertyEditorRegistrars

与数据绑定

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

    // other methods to do with registering a User
}           

这种

PropertyEditor

注册样式可以使代码简洁(

initBinder(..)

的实现只有一行长),并且可以将通用的

PropertyEditor

注册代码封装在一个类中,然后根据需要在许多

Controller

之间共享。

3.4 Spring类型转换

Spring 3 已经引入一个

core.convert

包,它提供了一般类型系统转换。系统定义了一个用于实现类型转换逻辑的SPI和一个用于在运行时执行类型转换的API。在Spring容器中,可以使用此特性作为

PropertyEditor

实现的替代方法,以将外部化的bean属性值字符串转换为所需的属性类型。你还可以在应用程序中需要类型转换的任何地方使用公共API。

3.4.1 转换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)

的调用,方法参数必须保证不能为

null

。如果转换失败,你的

Converter

可能抛出未检查异常。特别地,它可能抛出

IllegalArgumentException

去报告无效参数值异常。小心的去确保

Converter

实现是线程安全的。

为了方便起见,在

core.convert.support

包中提供了几种转换器实现。这些包括从字符串到数字和其他常见类型的转换器。下面的清单显示了

StringToInteger

类,它是一个典型的

Converter

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

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

3.4.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.4.3 使用

GenericConverter

当你需要复杂的

Converter

实现时,请考虑使用

GenericConverter

接口。与

Converter

相比,

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

会检查声明目标集合类型的字段以解析集合的元素类型。这样就可以在将集合设置到目标字段上之前,将源数组中的每个元素转换为集合元素类型。

由于

GenericConverter

是一个更复杂的SPI接口,因此仅应在需要时使用它。支持

Converter

ConverterFactory

以满足基本的类型转换需求。

com.liyong.ioccontainer.service.converter.GenericConverterTest

使用

ConditionalGenericConverter

有时,你希望

Converter

仅在满足特定条件时才运行。例如,你可能只想在目标字段上存在特定注解时才运行

Converter

,或者可能在目标类上定义了特定方法(例如静态

valueOf

方法)时才运行

Converter

ConditionalGenericConverter

GenericConverter

ConditionalConverter

接口的联合,可让你定义以下自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}           

ConditionalGenericConverter

的一个很好的例子是

EntityConverter

,它在持久实体标识和实体引用之间转换。仅当目标实体类型声明静态查找器方法(例如

findAccount(Long)

)时,此类

EntityConverter

才可能匹配。你可以在

matchs(TypeDescriptor,TypeDescriptor)

的实现中执行这种

finder

方法检查。

com.liyong.ioccontainer.service.converter.ConditionalConverterTest

3.4.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

实现委派其注册的转换器执行类型转换逻辑。

core.convert.support

包中提供了一个强大的

ConversionService

实现。

GenericConversionService

是适用于大多数环境的通用实现。

ConversionServiceFactory

提供了一个方便的工厂来创建通用的ConversionService配置。

3.4.5 配置

ConversionService

ConversionService

是无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在Spring应用程序中,通常为每个Spring容器(或

ApplicationContext

)配置一个

ConversionService

实例。当框架需要执行类型转换时,Spring会使用该

ConversionService

并使用它。你还可以将此

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一章中的

转换和格式化

在某些情况下,你可能希望在转换过程中应用格式设置。有关使用

FormattingConversionServiceFactoryBean

的详细信息,请参见

FormatterRegistry SPI

3.4.6 编程式地使用

ConversionService

要以编程方式使用

ConversionService

实例,可以像对其他任何bean一样注入对该bean例的引用。以下示例显示了如何执行此操作:

@Service
public class MyService {

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

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

对于大多数用例,可以使用指定

targetType

convert

方法,但不适用于更复杂的类型,例如参数化元素的集合。例如,如果要以编程方式将整数列表转换为字符串列表,则需要提供源类型和目标类型的格式定义。

幸运的是,如下面的示例所示,

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

的集合转换为

T

的集合。

3.5 Spring字段格式

如上一节所述,

core.convert

是一种通用类型转换系统。它提供了统一的

ConversionService

API和强类型的

Converter

SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring容器使用此系统绑定bean属性值。此外,Spring Expression Language(SpEL)和

DataBinder

都使用此系统绑定字段值。例如,当SpEL需要强制将

Short

Long

来完成

expression.setValue(Object bean,Object value)

尝试时,

core.convert

系统将执行强制转换。

考虑一个典型的客户端环境转换需求,例如web或桌面应用。在这种环境中,你通常将字符串转换为支持客户端提交处理,以及将字符串转换为支持视图呈现过程。以及,你通常需要本地化

String

值。更通用的

core.convert

Converter

SPI不能直接满足此类格式化要求。为了直接解决这些问题,Spring 3 引入了方便的

Formatter

SPI,它为客户端环境提供了

PropertyEditor

实现的简单而强大的替代方案。

通常,当你需要实现通用类型转换逻辑时,可以使用

Converter

SPI,例如,在

java.util.Date

Long

之间转换。当你在客户端环境中(例如,web应用)并且需要去解析和打印本地化字段值时,你可以使用

Formatter

SPI。

ConversionService

为这两个SPI提供统一的类型转换。

3.5.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()

操作,以从客户端本地返回的格式化表示形式解析

T

的实例。如果尝试解析失败,你的

Formatter

应该抛一个

ParseException

IllegalArgumentException

异常。注意确保你的

Formatter

为了方便

format

子包提供一些

Formatter

number

包提供

NumberStyleFormatter

CurrencyStyleFormatter

PercentStyleFormatter

去格式化

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) {
        if (date == null) {
            return "";
        }
        return getDateFormat(locale).format(date);
    }

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

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

Spring欢迎社区驱动

Formatter

贡献。查看

GitHub Issues

去贡献。

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

去创建一个实现:将

A

参数化为要与格式逻辑关联的字段

annotationType

,例如,

org.springframework.format.annotation.DateTimeFormat

getFieldTypes()

返回可在其上使用注解的字段类型。让

getPrinter()

返回

Printer

以打印带注解的字段的值。让

getParser()

Parser

去为注解字段解析

clientValue

下面的示例

AnnotationFormatterFactory

实现将

@NumberFormat

注解绑定到格式化程序,以指定数字样式或模式:

public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {

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

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

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

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

触发格式,可以使用

@NumberFormat

注解字段,如以下示例所示:

public class MyModel {
    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}           

格式注解API

org.springframework.format.annotation

包中存在一个可移植的格式注解API。你可以使用

@NumberFormat

格式化

Number

字段(例如

Double

Long

),并使用

@DateTimeFormat

java.util.Date

java.util.Calendar

Long

(用于毫秒时间戳)以及JSR-310

java.time

Joda-Time

值类型。

下面例子使用

@DateTimeFormat

去格式

java.util.Date

为ISO日期(

yyyy-MM-dd

);

public class MyModel {

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

3.5.3

FormatterRegistry

FormatterRegistry

是一个SPI用于注册格式化器和转换器。

FormattingConversionService

FormatterRegistry

实现适用于绝大环境。通过使用

FormattingConversionServiceFactoryBean

,你可以编程式地或声明式配置这些变体作为Spring bean。由于此实现还实现了

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

像在前面清单显示,你通过字段类型或通过注解注册格式化器。

FormatterRegistry

SPI使你可以集中配置格式设置规则,而不必在控制器之间重复此类配置。例如,你可能要强制所有日期字段以某种方式设置格式或带有特定注解的字段以某种方式设置格式。使用共享的

FormatterRegistry

,你可以一次定义这些规则,并在需要格式化时应用它们。

3.5.4

FormatterRegistrar

FormatterRegistrar

是一个SPI,用于通过

FormatterRegistry

注册格式器和转换器。以下清单显示了其接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}           

为给定的格式类别(例如日期格式)注册多个相关的转换器和格式器时,

FormatterRegistrar

很有用。在声明式注册不充分的情况下它也很有用。例如,当格式化程序需要在不同于其自身的特定字段类型下进行索引时,或者在注册

Printer

/

Parser

对时。下一节将提供有关转换器和格式化注册的更多信息。

3.5.5 在Spring MVC中配置格式化

在Spring MVC章节中,查看

Conversion 和 Formatting
3.6 配置全局

Date

Time

格式

默认情况下,未使用

@DateTimeFormat

注解日期和时间字段是使用

DateFormat.SHORT

格式从字符串转换的。如果愿意,可以通过定义自己的全局格式来更改此设置。

为此,请确保Spring不注册默认格式器。相反,可以借助以下方法手动注册格式化器:

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

  • org.springframework.format.datetime.DateFormatterRegistrar

    或为

    Joda-Time

    org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar

例如,下面Java配置注册一个全局的

yyyyMMdd

格式:

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

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

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

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

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

        return conversionService;
    }
}           

如果你偏好与基于XML配置,你可以使用

FormattingConversionServiceFactoryBean

。下面例子显示怎样去做(这里使用

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
        https://www.springframework.org/schema/beans/spring-beans.xsd>

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.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>           

注意:当在web应用中配置日期和时间格式时需要额外考虑。请查看

WebMVC Conversion 和 Formatting

or

WebFlux Conversion 和 Formatting

.

3.7 Java Bean校验

Spring框架提供对J

ava Bean

校验API。

3.7.1 Bean校验概要

Bean验证为Java应用程序提供了通过约束声明和元数据进行验证的通用方法。要使用它,你需要使用声明性验证约束对域模型属性进行注解,然后由通过运行时强制实施约束。有内置的约束,你也可以定义自己的自定义约束。

考虑以下示例,该示例显示了具有两个属性的简单

PersonForm

模型:

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

Bean验证使你可以声明约束,如以下示例所示:

public class PersonForm {

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

    @Min(0)
    private int age;
}           

然后,Bean验证器根据声明的约束来验证此类的实例。有关该API的一般信息,请参见

Bean Validation

。有关特定限制,请参见

Hibernate Validator

文档。要学习如何将bean验证提供程序设置为Spring bean,请继续阅读。

3.7.2 配置Bean Validation提供者

Spring提供了对Bean验证API的全面支持,包括将Bean验证提供程序作为Spring Bean执行引导。这使你可以在应用程序中需要验证的任何地方注入

javax.validation.ValidatorFactory

javax.validation.Validator

你可以使用

LocalValidatorFactoryBean

将默认的

Validator

配置为Spring Bean,如以下示例所示:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

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

前面示例中的基本配置触发Bean验证以使用其默认引导机制进行初始化。Bean验证提供程序,例如

Hibernate

Validator

,应该存在于类路径中并被自动检测到。

注入校验器

LocalValidatorFactoryBean

同时实现

javax.validation.ValidatorFactory

javax.validation.Validator

以及Spring的

org.springframework.validation.Validator

。你可以将对这些接口之一的引用注入需要调用验证逻辑的bean中。

如果你希望直接使用Bean

Validation

API,则可以注入对

javax.validation.Validator

的引用,如以下示例所示:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}           

配置自定义约束

每个bean校验约束由两部分组成:

  • @Constraint

    注解,用于声明约束及其可配置属性。
  • javax.validation.ConstraintValidator

    接口的实现,用于实现约束的行为。

要将声明与实现相关联,每个

@Constraint

注解都引用一个对应的

ConstraintValidator

实现类。在运行时,当在域模型中遇到约束注解时,

ConstraintValidatorFactory

实例化引用的实现。

默认情况下,

LocalValidatorFactoryBean

配置一个

SpringConstraintValidatorFactory

,该工厂使用Spring创建

ConstraintValidator

实例。这使你的自定义

ConstraintValidators

像其他任何Spring bean一样受益于依赖项注入。

以下示例显示了一个自定义

@Constraint

声明,后跟一个关联的

ConstraintValidator

实现,该实现使用Spring进行依赖项注入:

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

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}           

如前面的示例所示,

ConstraintValidator

实现可以像其他任何Spring bean一样具有

@Autowired

依赖项。

com.liyong.ioccontainer.service.validator.ConstraintTest

Spring驱动方法验证

你可以通过

MethodValidationPostProcessor

bean定义将

Bean Validation 1.1

(以及作为自定义扩展,还包括

Hibernate

Validator 4.3

)支持的方法验证功能集成到Spring上下文中:

import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration

public class AppConfig {

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

为了有资格进行Spring驱动的方法验证,所有目标类都必须使用Spring的

@Validated

注解进行注释,该注解也可以选择声明要使用的验证组。有关使用

Hibernate Validator

Bean Validation 1.1

提供程序的设置详细信息,请参见

MethodValidationPostProcessor
方法验证依赖于目标类周围的AOP代理,即接口上方法的JDK动态代理或CGLIB代理。代理的使用存在某些限制,在 理解 AOP 代理 中介绍了其中的一些限制。另外,请记住在代理类上使用方法和访问器;直接访问将不起作用。

com.liyong.ioccontainer.starter.MethodvalidationIocContainer

其他配置选项

在大多数情况下,默认

LocalValidatorFactoryBean

配置就足够了。从消息插值到遍历解析,有多种用于各种Bean验证构造的配置选项。有关这些选项的更多信息,请参见

LocalValidatorFactoryBean

Javadoc。

3.7.3 配置

DataBinder

从Spring 3 开始,你可以使用

Validator

配置

DataBinder

实例。配置完成后,你可以通过调用

binder.validate()

来调用

Validator

。任何验证错误都会自动添加到绑定的

BindingResult

下面的示例演示如何在绑定到目标对象后,以编程方式使用

DataBinder

来调用验证逻辑:

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

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

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

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

你还可以通过

dataBinder.addValidators

dataBinder.replaceValidators

配置具有多个

Validator

实例的

DataBinder

。当将全局配置的bean验证与在

DataBinder

实例上本地配置的Spring

Validator

结合使用时,这很有用。查看

Spring MVC 校验配置

com.liyong.ioccontainer.service.validator.ValidatorTest

3.7.4 Spring MVC 3 校验

在Sprint MVC 章节中,查看

Validation

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!

博客地址:

http://youngitman.tech

CSDN:

https://blog.csdn.net/liyong1028826685

微信公众号:

Spring 5 中文解析核心篇-IoC容器之数据校验、数据绑定和类型转换

技术交流群:

Spring 5 中文解析核心篇-IoC容器之数据校验、数据绑定和类型转换