20220507 Core - 3. Validation, Data Binding, and Type Conversion

概览

文档地址

将验证视为业务逻辑有其利弊,Spring 提供了一种不排除任何一种验证(和数据绑定)设计。具体来说,验证不应该绑定到 web 层,应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个 Validator ,它在应用程序的每一层都是基本的、有用的。

数据绑定对于让用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring 提供了 DataBinder 来实现这一点。

ValidatorDataBinder 组成了验证包,主要用于但不限于 web 层。

BeanWrapper 是 Spring 框架中的基本概念,并在很多地方被使用。但是,您可能不需要直接使用 BeanWrapper 。如果您打算使用它,那么在尝试将数据绑定到对象时很可能会使用它。

DataBinder 和较低级别 BeanWrapper 都使用 PropertyEditorSupport 实现来解析和格式化属性值。PropertyEditorPropertyEditorSupport 是 JavaBeans 规范的一部分。

Spring 3 引入了一个提供通用类型转换工具的 core.convert 包,以及一个用于格式化 UI 字段值的更高级别的 format 包。您可以将这些包用作 PropertyEditorSupport 实现的更简单替代方案。

Spring 通过设置基础设施和 Spring 自己的 Validator 契约的适配器支持 Java Bean 验证。应用程序可以全局启用一次 Bean 验证,并将其专门用于所有验证需求。在 web 层,应用程序可以进一步为每个 DataBinder 注册控制器本地的 Spring Validator 实例,这对于插入自定义验证逻辑非常有用。

使用 Spring 的 Validator 接口进行验证

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

以 Person 对象为例:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

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

  • supports(Class)Validator 支持验证所提供的 Class 实例吗?
  • validate(Object, org.springframework.validation.Errors) :验证给定的对象,并在验证错误的情况下向给定的 Errors 对象注册错误对象。

实现 Validator 相当简单,Spring Framework 还提供了帮助程序类 ValidationUtils 。以下示例 ValidatorPerson 实例实现:

public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Person.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        Person p = (Person) target;
        if (p.getAge() < 0) {
            errors.rejectValue("age", "negative value");
        } else if (p.getAge() > 110) {
            errors.rejectValue("age", "too.darn.old");
        }
    }
}

虽然实现 Validator 类来验证富对象中的每个嵌套对象是可以的,但最好将每个嵌套对象类的验证逻辑封装在其自己的 Validator 实现中。

“富”对象的一个简单示例 Customer 是由两个 String 属性(第一个和第二个名称)和一个复杂 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 对象。

参考源码

  • org.springframework.validation.Validator
  • org.springframework.validation.ValidationUtils

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

当您调用(直接或间接,例如通过使用 ValidationUtils 类)rejectValueErrors 接口中的其他 reject 方法时,底层实现不仅会注册您传入的代码,还会注册一些额外的错误代码MessageCodesResolver 确定 Errors 接口注册的错误代码。默认情况下, 使用 DefaultMessageCodesResolver ,它不仅使用您提供的代码注册消息,还注册包含您传递给拒绝方法的字段名称的消息。因此,如果您使用 rejectValue("age", "too.darn.old") 拒绝字段,除了代码 too.darn.old 之外,Spring 还会注册 too.darn.old.agetoo.darn.old.age.int(第一个包括字段名称,第二个包括字段的类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。

  • org.springframework.validation.MessageCodesResolver
  • org.springframework.validation.DefaultMessageCodesResolver

Bean 操作和 BeanWrapper

org.springframework.beans 包符合 JavaBeans 标准。JavaBean 是具有默认无参数构造函数的类,并且遵循命名约定,其中命名 bingoMadness 的属性将具有 setter 方法 setBingoMadness(..) 和 getter 方法 getBingoMadness()

bean 包中一个非常重要的类是 BeanWrapper 接口及其实现 BeanWrapperImplBeanWrapper 提供了

  • 设置和获取属性值、获取属性描述符和查询属性以确定它们是否可读或可写的功能
  • 提供对嵌套属性的支持,允许将子属性上的属性设置为无限深度
  • 支持添加标准 JavaBeans PropertyChangeListenersVetoableChangeListeners 的功能,而无需在目标类中支持代码
  • 提供了对设置索引属性的支持

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

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

  • org.springframework.beans.BeanWrapper

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

设置和获取属性是通过 BeanWrappersetPropertyValuegetPropertyValue 的重载方法变体完成的。

属性示例:

表达式 解释
name 指示属性 name 对应的方法,getName()isName()setName(..)
account.name 指示属性 account 的嵌套属性 namegetAccount().setName()getAccount().getName()
account[2] 指示索引属性 account 的第三个元素。索引属性可能是的 arraylist 或其它有序集合
account[COMPANYNAME] 指示由 Map 属性 accountCOMPANYNAME 键索引的映射条目的值

以下两个示例类使用 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;
    }
}

以下代码片段展示了如何检索和操作实例化 CompaniesEmployees 的某些属性的一些示例:

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

内置 PropertyEditor 实现

java.beans.PropertyEditor

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

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

  • 在 bean 上设置属性是通过使用 PropertyEditor 实现来完成的。当使用 String 作为在XML文件中声明的某个 bean 的属性值时,Spring(如果相应属性的 setter 方法有 Class 参数)ClassEditor 会尝试将该参数解析为 Class 对象。
  • Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种 PropertyEditor 实现来完成的,可以在 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 的自定义子类属性编辑器,如 IntegerLongFloatDouble 。默认情况下由 BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖
FileEditor 将字符串解析为 java.io.File 对象。默认情况下由 BeanWrapperImpl 注册
InputStreamEditor 单向属性编辑器,可以接受一个字符串并生成(通过中间 ResourceEditorResourceInputStream 以便可以将 InputStream 属性直接设置为字符串。注意,默认不会为您关闭 InputStream。默认情况下由 BeanWrapperImpl 注册
LocaleEditor 可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant] ,与 LocaletoString() 方法相同)。也接受空格作为分隔符,作为下划线的替代品。默认情况下由 BeanWrapperImpl 注册
PatternEditor 可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然
PropertiesEditor 可以将字符串(使用 java.util.Properties 类的 javadoc 中定义的格式进行格式化 )转换为 Properties 对象。默认情况下由 BeanWrapperImpl 注册
StringTrimmerEditor 修剪字符串的属性编辑器。允许将空字符串转换为 null 值。默认情况下未注册,必须是用户注册的
URLEditor 可以将 URL 的字符串表示解析为实际 URL 对象。默认情况下由 BeanWrapperImpl 注册

Spring 使用 java.beans.PropertyEditorManager 为可能需要的属性编辑器设置搜索路径。搜索路径还包括 sun.bean.editors ,其中包括 FontColor 和大多数基本类型的 PropertyEditor 实现。

另请注意,如果标准 JavaBeans 基础结构 PropertyEditor 与它们处理的类在同一个包中并且与该类具有相同的名称,那么标准 JavaBeans 基础结构会自动发现 Editor 类(无需显式注册它们)。例如,可以具有以下类和包结构,这足以使 SomethingEditor 类被识别并用作 Something 类型属性的 PropertyEditor

com
  chank
    pop
      Something
      SomethingEditor // the PropertyEditor for the Something class

请注意,您也可以在此处使用标准 BeanInfo JavaBeans 机制。以下示例使用 BeanInfo 机制显式注册一个或多个具有关联类属性的 PropertyEditor 实例:

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

SomethingBeanInfo 类将 CustomNumberEditorSomething 类的 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 实现。此外,Java 的标准 JavaBeans PropertyEditor 查找机制允许对 PropertyEditor 类进行适当命名并将其放置在与它提供支持的类相同的包中,以便可以自动找到它。

如果需要注册其他自定义PropertyEditors ,可以使用几种机制。通常不推荐的方法是使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法。另一种(稍微方便一点)机制是使用一个特殊的 bean 工厂后处理器,称为 CustomEditorConfigurer 。尽管您可以将 bean 工厂后处理器与 BeanFactory 实现一起使用,但 CustomEditorConfigurer 具有嵌套的属性设置,因此我们强烈建议您将其与 ApplicationContext 一起使用 ,您可以以与其他 bean 类似的方式部署它,并且可以自动检测和应用。

注意,所有 bean 工厂和应用程序上下文都会通过使用 BeanWrapper 来处理属性转换,自动使用许多内置属性编辑器。此外,ApplicationContext 还可以覆盖或添加其他编辑器,以适合特定应用上下文类型的方式处理资源查找。

使用示例
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;
    }
}
<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>
public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}
<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当您需要在多种不同情况下使用同一组属性编辑器时,此接口特别有用。您可以编写相应的注册器并重复使用它。 PropertyEditorRegistrarPropertyEditorRegistry 接口一起工作,Spring BeanWrapperDataBinder 实现 PropertyEditorRegistry 接口 。PropertyEditorRegistrarCustomEditorConfigurer 结合使用特别方便,它公开了一个属性 setPropertyEditorRegistrars(..) ,以这种方式添加到 PropertyEditorRegistrar 的实例。

CustomEditorConfigurer 可以很容易地与DataBinder 和 Spring MVC 控制器一起使用。此外,它避免了在自定义编辑器上进行同步的需要:PropertyEditorRegistrar 将为每个 bean 创建过程尝试创建新的 PropertyEditor 实例。

创建您自己的 PropertyEditorRegistrar 实现:

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

org.springframework.beans.support.ResourceEditorRegistrar 可以作为实现自定义的参考

配置 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 框架来说,PropertyEditorRegistrars 与数据绑定 Controllers 结合使用会非常方便。以下示例在 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 注册代码封装在一个类中,然后根据需要在多个 Controllers 类之间共享 。

Spring 类型转换

Spring 3 引入了 core.convert 包,它提供了一个通用类型转换系统。系统定义一个 SPI 来实现类型转换逻辑,定义一个 API 在运行时执行类型转换。在 Spring 容器中,您可以使用此系统作为 PropertyEditor 实现的替代方案,将外部化的 bean 属性值字符串转换为所需的属性类型。您还可以在应用程序中需要进行类型转换的任何位置使用公共 API 。

转换器 SPI - Converter

用于实现类型转换逻辑的 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 包中提供了几个转换器实现。包括从字符串到数字和其他常见类型的转换器。以下显示了 StringToInteger 类,这是一个典型的 Converter 实现:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}
  • org.springframework.core.convert.converter.Converter

使用 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) ,其中 TR 的子类。

举例: 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());
        }
    }
}
  • org.springframework.core.convert.converter.ConverterFactory

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

要实现 GenericConvertergetConvertibleTypes() 必须返回支持的源、目标类型对。然后实现 convert(Object, TypeDescriptor, TypeDescriptor) 以包含您的转换逻辑。源 TypeDescriptor 提供对包含要转换的值的源字段的访问。目标 TypeDescriptor 提供对要设置转换值的目标字段的访问。

一个很好的 GenericConverter 例子是在 Java 数组和集合之间转换的转换器,ArrayToCollectionConverter 内省声明目标集合类型的字段以解析集合的元素类型。这使得在目标字段上设置集合之前,源数组中的每个元素都可以转换为集合元素类型。

因为 GenericConverter 是一个更复杂的 SPI 接口,应该只在需要时使用它。ConverterConverterFactory 可以满足基本的类型转换需求。

  • org.springframework.core.convert.converter.GenericConverter
使用 ConditionalGenericConverter

有时,您希望仅 在特定条件成立时运行 Converter 。例如,您可能希望仅在目标字段上存在特定注解时才运行 Converter ,或者您可能只想在目标类上定义了特定方法(例如 static valueOf 方法)时才运行 ConverterConditionalGenericConverterGenericConverterConditionalConverter 接口的并集 ,可让您定义此类自定义匹配条件:

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

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
    
}

一个很好的 ConditionalGenericConverter 例子是 IdToEntityConverter ,它在持久实体标识符和实体引用之间进行转换。IdToEntityConverter 仅当目标实体类型声明静态查找器方法(例如,findAccount(Long) )时,此类才可能匹配 。您可以在在 matches(TypeDescriptor, TypeDescriptor) 方法里实现查找逻辑。

  • org.springframework.core.convert.converter.ConditionalGenericConverter

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 实现委托其注册的转换器执行类型转换逻辑。

ConfigurableConversionService 接口同时继承了 ConversionServiceConverterRegistry

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

  • org.springframework.core.convert.ConversionService
  • org.springframework.core.convert.converter.ConverterRegistry
  • org.springframework.core.convert.support.ConfigurableConversionService
  • org.springframework.core.convert.support.GenericConversionService
  • org.springframework.core.convert.support.ConversionServiceFactory

配置 ConversionService

ConversionService 是一个无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,您通常为每个 Spring 容器(或 ApplicationContext )配置一个 ConversionService 实例。Spring 在框架需要执行类型转换时使用 ConversionService 。您还可以将 ConversionService 注入任何 bean 并直接调用它。

如果没有在 Spring 中注册 ConversionService ,则使用原始的基于 PropertyEditor 系统。

要向 Spring 注册默认的 ConversionService ,请添加 idconversionService 的 bean 定义:

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

默认 ConversionService 可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。

要使用您自己的自定义转换器补充或覆盖默认转换器,请设置 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 也很常见。

在某些情况下,您可能希望在转换期间应用格式化。使用 FormattingConversionServiceFactoryBean 的详细信息见 FormatterRegistry SPI

  • org.springframework.context.support.ConversionServiceFactoryBean
  • org.springframework.core.convert.support.DefaultConversionService
  • org.springframework.format.support.FormattingConversionServiceFactoryBean

以编程方式使用 ConversionService

要以编程方式使用 ConversionService 实例,可以像注入任何其他 bean 一样注入对它的引用。

@Service
public class MyService {

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

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

对于大多数用例,可以使用 convert 方法指定的 targetType ,但它不适用于更复杂的类型,例如参数化元素的集合。例如,如果要以编程方式将 IntegerList 转换为 StringList ,则需要提供源类型和目标类型的正式定义。

幸运的是,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 自动注册了适用于大多数环境的转换器。这包括集合转换器、标量转换器以及基本的 ObjectString 转换器。可以使用 DefaultConversionService 类上的静态方法 addDefaultConverters 向任何 ConverterRegistry 注册相同的转换器。

值类型的转换器可重用于数组和集合,因此无需创建特定的转换器来将 SCollection 转换为 TCollection ,前提是标准集合处理是合适的。

  • org.springframework.core.convert.support.DefaultConversionService

Spring 属性格式化( Format )

core.convert 是一个通用类型转换系统。它提供了一个统一的 ConversionService API 以及一个强类型的 Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用此系统绑定 bean 属性值。此外,Spring Expression Language( SpEL)和 DataBinder 都使用此系统绑定字段值。例如,当 SpEL 需要将 Short 强制转换为 Long 以完成 expression.setValue(Object bean,Object value) 尝试时。

考虑典型客户端环境(例如 Web 或桌面应用程序)的类型转换要求。在此类环境中,您通常会转换 String 为支持客户端回发过程,以及转换 String 为支持视图渲染过程。此外,您经常需要本地化 String 值。 Converter SPI 不直接解决此类格式要求。为了直接解决这些问题,Spring 3 引入了一个方便的 Formatter SPI,它为客户端环境的 PropertyEditor 实现提供了一个简单而健壮的替代方案。

通常,当您需要实现通用类型转换逻辑时可以使用 Converter SPI ,例如,在 java.util.DateLong 之间进行转换。当您在客户端环境(例如 Web 应用程序)中工作并且 需要解析和打印本地化的字段值时 ,您可以使用 Formatter SPI。

ConversionServiceConverterFormatter 两个 SPI 提供统一的类型转换 API 。

Formatter SPI

实现字段格式化逻辑的 Formatter SPI 简单且 强类型

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
    
}
package org.springframework.format;

public interface Printer<T> {

    String print(T fieldValue, Locale locale);
}
package org.springframework.format;

import java.text.ParseException;
import java.util.Locale;

public interface Parser<T> {

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

注意确保 Formatter 实现是线程安全的。

为了方便起见,format 子包提供了几种 Formatter 实现。number 包提供 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter 使用 java.text.NumberFormat 来格式化 Number 对象。 datetime 包提供了 DateFormatter 使用 java.text.DateFormat 格式化 java.util.Date 对象。

参考实现:org.springframework.format.datetime.DateFormatter

  • org.springframework.format.Formatter

注解驱动的格式化( AnnotationFormatterFactory

字段格式可以通过字段类型或注解进行配置。要将注解绑定到 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);
}

要创建实现,请执行以下操作:参数化要与格式逻辑关联的字段 annotationType ,例如 org.springframework.format.annotation.DateTimeFormatgetFieldTypes() 返回可以使用注解的字段类型,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 属性,如 DoubleLong ,使用 @DateTimeFormat 以格式化 java.util.Datejava.util.CalendarLong (毫秒时间戳)以及 JSR-310 java.time

以下示例使用 @DateTimeFormatjava.util.Date 格式化为 ISO 日期 ( yyyy-MM-dd ):

public class MyModel {

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

FormatterRegistry SPI

FormatterRegistry 是用于注册格式化程序和转换器的 SPIFormattingConversionService 是适用于大多数环境的 FormatterRegistry 实现。您可以以编程方式或声明方式将此变体配置为 Spring bean ,例如使用 FormattingConversionServiceFactoryBean 。由于此实现还实现了ConversionService ,您可以直接配置它以与 Spring DataBinder 和 Spring 表达式语言 ( SpEL ) 一起使用。

package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

    void addPrinter(Printer<?> printer);

    void addParser(Parser<?> parser);

    void addFormatter(Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

FormatterRegistry SPI 允许您集中配置格式化规则,而不是在控制器中重复配置。例如,您可能希望强制所有日期字段以某种方式格式化,或者具有特定注解的字段以某种方式格式化。使用共享的 FormatterRegistry,您只需定义一次这些规则,并在需要格式化时应用它们。

  • org.springframework.format.FormatterRegistry

FormatterRegistrar SPI

FormatterRegistrar 是一个 SPI,用于通过 FormatterRegistry 注册格式化程序和转换器

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

在为给定的格式类别(例如日期格式)注册多个相关的转换器和格式化器时,FormatterRegistrar 很有用。在声明式注册不足的情况下,它也很有用。例如,当格式化程序需要在与其自身 <T> 不同的特定字段类型下进行索引时, 或者在注册 Printer / Parser 对时。

在 Spring MVC 中配置格式化

请参阅 Spring MVC 章节中的 转换和格式化

配置全局日期和时间格式

默认情况下,没有 @DateTimeFormat 注解的日期和时间字段使用 DateFormat.SHORT 样式从字符串转换。可以通过定义自己的全局格式来更改此设置。

为此,请确保 Spring 不注册默认格式化器。相反,请在以下帮助下手动注册格式化程序:

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

以下 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

<?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.standard.DateTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.standard.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

请注意,在 Web 应用程序中配置日期和时间格式时有一些额外的注意事项。请参阅 WebMVC 转换和格式化WebFlux 转换和格式化

Java Bean 验证

Spring Framework 提供对 Java Bean Validation API 的支持。

需要引入相关依赖

<!-- validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Bean 验证概述

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

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

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

    @Min(0)
    private int age;
}

Bean Validation 验证器然后根据声明的约束验证此类的实例。有关 API 的一般信息,请参阅 Bean 验证 。有关特定约束,请参阅 Hibernate Validator 文档。

配置 Bean 验证提供程序

Spring 完全支持 Bean Validation API ,包括将 Bean Validation 提供者引导为 Spring bean 。这使您可以在应用中任何需要验证的地方注入 javax.validation.ValidatorFactoryjavax.validation.Validator

您可以使用 LocalValidatorFactoryBean 将默认验证器配置为 Spring bean ,如以下示例所示:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

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

前面示例中的基本配置通过使用其默认引导机制触发 bean 验证进行初始化。Bean Validation 提供程序,例如 Hibernate Validator,出现在类路径中会被自动检测到。

  • org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
注入 Validator

LocalValidatorFactoryBean 实现了 javax.validation.ValidatorFactoryjavax.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 需要 Spring Validation API,可以注入 org.springframework.validation.Validator 引用,如以下示例所示:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}
  • javax.validation.Validator
  • org.springframework.validation.Validator
配置自定义约束

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

  • @Constraint 注解声明约束和注解的可配置属性
  • 实现约束行为的 javax.validation.ConstraintValidator 接口的实现

为了将声明与实现相关联,每个 @Constraint 注解引用一个相应的 ConstraintValidator 实现类。在运行时,当域模型中遇到约束注解时,ConstraintValidatorFactory 将实例化引用的实现。

默认情况下,LocalValidatorFactoryBean 配置了 SpringConstraintValidatorFactory 使用 Spring 创建 ConstraintValidator 实例。这让您的自定义 ConstraintValidators 可以像其他 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 它的依赖项 。

  • javax.validation.Constraint
  • javax.validation.ConstraintValidator
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();
    }
}
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

为了符合 Spring 驱动的方法验证的条件,所有目标类都需要使用 Spring 的 @Validated 注解进行注解,它也可以选择声明要使用的验证组。

方法验证依赖于围绕目标类的 AOP 代理,即接口方法的 JDK 动态代理或 CGLIB 代理。代理的使用存在某些限制,其中一些在 理解 AOP 代理中 进行了描述 。

注意:始终在代理类上使用方法和访问器;直接对属性进行访问将不起作用。

  • org.springframework.validation.beanvalidation.MethodValidationPostProcessor
  • org.springframework.validation.annotation.Validated
其他配置选项

大多数情况下默认 LocalValidatorFactoryBean 配置就足够了。对于各种 Bean 验证构造,有许多配置选项,从消息插值到遍历解析。

配置 DataBinder

从 Spring 3 开始,您可以使用 Validator 配置 DataBinder 实例,可以通过调用 binder.validate() 来调用 Validator 。任何验证 Errors 都会自动添加到绑定器( Binder )的 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.addValidatorsdataBinder.replaceValidators 配置具有多个 Validator 实例的 DataBinder 。这在将全局配置的 bean 验证与在 DataBinder 实例上本地配置的 Spring Validator 结合使用时非常有用。请参阅 Spring MVC 验证配置

Spring MVC 3 验证

请参阅 Spring MVC 章节中的 Validation

posted @ 2022-06-09 21:14  流星<。)#)))≦  阅读(52)  评论(0编辑  收藏  举报