欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

【译文】spring framework核心功能-数据验证
(last modified Nov 28, 2023, 9:33 PM )
by
侧边栏壁纸
  • 累计撰写 178 篇文章
  • 累计创建 62 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

【译文】spring framework核心功能-数据验证

橙序员
2022-08-20 / 0 评论 / 0 点赞 / 399 阅读 / 12 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

这种验证和数据绑定的设计有一些优点和缺点。优点包括:
1. 验证不绑定到Web层,使得验证逻辑更加清晰和可复用。
2. 易于本地化,可以根据不同地区的需求进行定制。
3. 可插入任何可用的验证器,使得验证逻辑更加灵活和可扩展。
然而,这种设计也存在一些缺点:
1. 验证和数据绑定的实现相对复杂,需要使用Spring提供的Validator和DataBinder。
2. 使用BeanWrapper和PropertyEditorSupport等类进行属性值的解析和格式化,可能需要理解JavaBeans规范和Spring框架的相关概念。
3. 在Web层中,需要注册控制器本地的Spring Validator实例,可能需要额外的配置和管理。
总的来说,使用Spring的验证和数据绑定设计可以提供灵活和可扩展的验证逻辑,但同时也需要一定的学习和配置成本。

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

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

数据绑定对于让用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。 Spring 提供了恰当命名的 DataBinder 来完成此任务。 ValidatorDataBinder组成了验证包,主要用于但不限于Web层。

BeanWrapper是 Spring 框架中的一个基本概念,在很多地方都有使用。但是,您可能不需要直接使用BeanWrapper。但是,因为这是参考文档,所以我们认为可能需要进行一些解释。我们将在本章中解释它,因为如果您要使用BeanWrapper,您很可能在尝试将数据绑定到对象时这样做。

SpringDataBinder和较低级别BeanWrapper都使用PropertyEditorSupport 实现来解析和格式化属性值。PropertyEditorPropertyEditorSupport类型是 JavaBeans 规范的一部分,本章也进行了说明。Spring 3 引入了一个提供通用类型转换工具的包core.convert,以及一个用于格式化 UI 字段值的更高级别的“格式”包。您可以将这些包用作实现 PropertyEditorSupport的更简单的替代方案。本章还将讨论它们。

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

3.1. 使用 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或空字符串。查看 ValidationUtilsjavadoc 以了解除了前面显示的示例之外它还提供了哪些功能。

虽然实现单个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对象。关于它提供的方法的更多信息可以在javadoc中找到。

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

我们涵盖了数据绑定和验证。本节介绍与验证错误相对应的输出消息。在上一节所示的示例中,我们拒绝了nameage字段。如果我们想通过使用MessageSource来输出错误消息 ,我们可以使用我们在拒绝字段时提供的错误代码(在本例中为“姓名”和“年龄”)来完成。当您调用(直接或间接,通过使用例如ValidationUtils类)rejectValueErrors接口中的其他reject方法之一时,底层实现不仅注册您传入的代码,而且还注册许多额外的错误代码。MessageCodesResolver 确定Errors接口寄存器的错误代码。默认情况下, 使用DefaultMessageCodesResolver,它(例如)不仅使用您提供的代码注册消息,而且还注册包含您传递给拒绝方法的字段名称的消息。所以,如果你用rejectValue("age", "too.darn.old") 拒绝一个字段,除了代码too.darn.old之外,Spring 还会注册too.darn.old.agetoo.darn.old.age.int(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员定位错误消息。

有关MessageCodesResolver和 默认策略的更多信息可以分别在MessageCodesResolverDefaultMessageCodesResolver的 javadoc 中找到 。

3.3. Bean 操作和BeanWrapper

org.springframework.beans包遵循 JavaBeans 标准。JavaBean 是具有默认无参数构造函数的类,并且遵循命名约定,其中(例如)命名bingoMadness的属性将具有setBingoMadness(..) setter 方法和getBingoMadness() getter 方法。有关 JavaBeans 和规范的更多信息,请参阅 javabeans

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

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

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

设置和获取属性是通过 BeanWrappersetPropertyValue getPropertyValue 重载方法变体完成的. 有关详细信息,请参阅他们的 Javadoc。下表显示了这些约定的一些示例:

表达式 解释
name 指示与 getName()isName() setName(..)方法对应的属性名称。
account.name 指示对应于(例如) getAccount().setName() getAccount().getName()方法的属性帐户的嵌套属性名称。
account[2] 指示索引属性account第三个元素。索引属性可以是arraylist或其他自然排序的集合。
account[COMPANYNAME] 指示由 account Map 属性的 COMPANYNAME 键索引的映射条目的值。

(如果您不打算直接使用 BeanWrapper,那么下一部分对您来说并不重要。如果您只使用DataBinderBeanFactory 以及它们的默认实现,您应该跳到关于 的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 s 和Employees 的某些属性:

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

3.3.2. 内置PropertyEditor实现

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

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

  • 在 bean 上设置属性是通过使用PropertyEditor实现来完成的。当您使用String在 XML 文件中声明的某个 bean 的属性值时,Spring(如果相应属性的设置器有Class 参数)ClassEditor会尝试将参数解析为Class对象。
  • 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种PropertyEditor实现来完成的,您可以在 CommandController.

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

班级 解释
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],与Locale 的方法toString() 相同)。也接受空格作为分隔符,作为下划线的替代。默认情况下,由 BeanWrapperImpl注册。
PatternEditor 可以将字符串解析为java.util.regex.Pattern对象,反之亦然。
PropertiesEditor 可以将java.util.Properties字符串(使用类的 javadoc 中定义的格式格式化 )转换为Properties对象。默认情况下,由BeanWrapperImpl 注册。
StringTrimmerEditor 修剪字符串的属性编辑器。可选地允许将空字符串转换为null值。默认情况下未注册 - 必须是用户注册的。
URLEditor 可以将 URL 的字符串表示解析为实际URL对象。默认情况下,由BeanWrapperImpl 注册。

Spring 使用java.beans.PropertyEditorManager为可能需要的属性编辑器设置搜索路径。搜索路径还包括sun.bean.editors,其中包括诸如FontColor和大多数基本类型的PropertyEditor实现。另请注意,标准 JavaBeans 基础结构会自动发现PropertyEditor类(无需显式注册它们),前提是它们与它们处理的类在同一个包中并且与该类具有相同的名称,并带有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引用类的以下 Java 源代码将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 容器最终使用标准 JavaBeansPropertyEditor实现将这些字符串转换为属性的复杂类型。Spring 预先注册了许多自定义PropertyEditor实现(例如,将表示为字符串的类名转换为Class对象)。此外,Java 的标准 JavaBeansPropertyEditor查找机制允许PropertyEditor 对类进行适当命名,并将其放置在与其提供支持的类相同的包中,以便可以自动找到它。

如果需要注册其他自定义PropertyEditors,可以使用多种机制。最手动的方法,通常不方便或不推荐,是使用 ConfigurableBeanFactory接口的registerCustomEditor()方法,假设您有参考BeanFactory。另一种(稍微方便一点)机制是使用一个特殊的 bean factory 后处理器,称为CustomEditorConfigurer. 尽管您可以将 bean factory 后处理器与BeanFactory实现一起使用,但CustomEditorConfigurer具有嵌套属性设置,因此我们强烈建议您将其与 ApplicationContext应用。

请注意,所有 bean 工厂和应用程序上下文都会自动使用许多内置属性编辑器,通过它们使用 BeanWrapper来处理属性转换。上一节列出了BeanWrapper 寄存器的标准属性编辑器。此外,ApplicationContext还覆盖或添加额外的编辑器,以便以适合特定应用程序上下文类型的方式处理资源查找。

标准 JavaBeansPropertyEditor实例用于将表示为字符串的属性值转换为属性的实际复杂类型。您可以使用 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()));
    }
}

最后,以下示例显示了如何使用CustomEditorConfigurerApplicationContext注册新的 PropertyEditor,然后可以根据需要使用它:

<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 (and DataBinder) 实现。PropertyEditorRegistrar实例与CustomEditorConfigurer在此处描述)结合使用时特别方便,它公开了一个名为setPropertyEditorRegistrars(..)的方法. 以这种方式添加到PropertyEditorRegistrarCustomEditorConfigurer 实例可以很容易地与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...
    }
}

另请参阅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 框架的人来说,将 PropertyEditorRegistrar与数据绑定 Web 控制器结合使用会非常方便。以下示例在@InitBinder 方法的实现中使用 PropertyEditorRegistrar

@Controller
public class RegisterUserController {

    private final PropertyEditorRegistrar customPropertyEditorRegistrar;

    RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
        this.customPropertyEditorRegistrar = propertyEditorRegistrar;
    }

    @InitBinder
    void initBinder(WebDataBinder binder) {
        this.customPropertyEditorRegistrar.registerCustomEditors(binder);
    }

    // other methods related to registering a User
}

这种注册PropertyEditor风格可以带来简洁的代码(@InitBinder方法的实现只有一行长),并且可以将通用的PropertyEditor 注册代码封装在一个类中,然后根据需要在尽可能多的控制器之间共享。

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),源参数保证不为空。如果转换失败, 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

当您需要集中整个类层次结构的转换逻辑时(例如,从转换StringEnum对象时),您可以实现 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 具有更灵活但类型更弱的签名,支持在多个源类型和目标类型之间进行转换。此外,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满足基本类型转换需求。

使用ConditionalGenericConverter

有时,您希望转换器仅在特定条件成立时运行。例如,您可能希望仅当目标字段上存在特定注解时才运行 Converter,或者仅当目标类上定义了特定方法(例如静态valueOf方法)时您可能希望运行 ConverterConditionalGenericConverterGenericConverter ConditionalConverter接口的联合,可让您定义此类自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

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

3.4.4. ConversionServiceAPI

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 注册默认值,添加以下 idconversionServicebean 定义:

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

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

<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 章节中的转换和格式化

在某些情况下,您可能希望在转换期间应用格式。有关使用参考 The FormatterRegistry SPI for details on using FormattingConversionServiceFactoryBean.

3.4.6. 以编程方式使用ConversionService

要以编程方式使用ConversionService实例,您可以像对任何其他 bean 一样注入对它的引用。以下示例显示了如何执行此操作:

@Service
public class MyService {

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

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

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

幸运的是,TypeDescriptor提供了各种选项来使操作变得简单,如以下示例所示:

DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ...
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

请注意,DefaultConversionService的自动注册适用于大多数环境的转换器。这包括集合转换器、标量转换器和基本ObjectString转换器。您可以使用DefaultConversionService类上的静态addDefaultConverters 方法向ConverterRegistry 注册相同的转换器。

值类型的转换器可用于数组和集合,因此无需创建特定的转换器来从 Collection <S>转换为 Collection <T>,假设标准集合处理是合适的。

3.5. spring字段格式

正如上一节所讨论的,core.convert是一个通用的类型转换系统。它提供了一个统一的ConversionServiceAPI 以及一个强类型的ConverterSPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用此系统绑定 bean 属性值。此外,Spring 表达式语言(SpEL)都使用这个系统来绑定字段值。例如,当 SpEL 需要将 Short强制转换为Long以完成一次expression.setValue(Object bean, Object value)尝试时,core.convert 系统会执行强制。

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

Converter通常,当您需要实现通用类型转换逻辑时,您可以使用SPI——例如,在 ajava.util.Date和 a之间进行转换LongFormatter当您在客户端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,您可以使用SPI。ConversionService 为两个 SPI 提供了统一的类型转换 API 。

3.5.1. FormatterSPI

实现字段格式化逻辑的FormatterSPI 简单且强类型。以下清单显示了Formatter接口定义:

package org.springframework.format;

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

FormatterPrinterParser构建块接口进行扩展。以下清单显示了这两个接口的定义:

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

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

为了方便起见,格式子包提供了几种 Formatter 实现。 number 包提供 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter来格式化使用 java.text.NumberFormatNumber 对象。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;
    }
}

Spring 团队欢迎社区驱动的Formatter贡献。请参阅 GitHub 问题以做出贡献。

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

创建一个实现:

  1. annotationType参数化为您希望与格式化逻辑相关联的字段 - 例如org.springframework.format.annotation.DateTimeFormat.
  2. getFieldTypes()已返回可以使用注解的字段类型。
  3. getPrinter()返回Printer以打印带注解的字段的值。
  4. 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

I org.springframework.format.annotation包中存在可移植格式注解 AP 。您可以使用@NumberFormat格式化Number字段,例如DoubleLong@DateTimeFormat格式化java.util.Date, java.util.CalendarLong (用于毫秒时间戳)以及 JSR-310 java.time

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

public class MyModel {

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

3.5.3. FormatterRegistrySPI

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

以下清单显示了FormatterRegistrySPI:

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

如前面的清单所示,您可以按字段类型或注解注册格式化程序。

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

3.5.4. FormatterRegistrarSPI

FormatterRegistrar是一个 SPI,用于通过 FormatterRegistry 注册格式化程序和转换器。下面的清单显示了它的接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

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

3.5.5. 在 Spring MVC 中配置格式化

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

3.6. 配置全局日期和时间格式

默认情况下,未使用@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 转换和格式化

3.7. Java Bean 验证

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

3.7.1. Bean 验证概述

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

考虑以下示例,该示例显示了一个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;
}

一个 Bean Validation 验证器然后根据声明的约束来验证这个类的实例。有关 API 的一般信息,请参阅Bean 验证。有关特定约束,请参阅Hibernate Validator文档。要了解如何将 bean 验证提供程序设置为 Spring bean,请继续阅读。

3.7.2. 配置 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,预计会出现在类路径中并且会被自动检测到。

注入验证器

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;
}
配置自定义约束

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

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

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

默认情况下,LocalValidatorFactoryBean 配置一个 SpringConstraintValidatorFactory,它使用 Spring 创建ConstraintValidator 实例。这可以让您的自定义ConstraintValidators像任何其他 Spring bean 一样从依赖注入中受益。

以下示例显示了一个自定义@Constraint声明,后跟一个使用 Spring 进行依赖注入的关联 ConstraintValidator实现:

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

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}

如前面的示例所示,ConstraintValidator 实现可以像任何其他 Spring bean 一样具有其依赖项 @Autowired

spring驱动的方法验证

您可以通过 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注解进行注解,它还可以选择声明要使用的验证组。请参阅 MethodValidationPostProcessor Hibernate Validator 和 Bean Validation 1.1 提供程序的设置详细信息。

方法验证依赖于目标类周围的AOP 代理,要么是接口上方法的 JDK 动态代理,要么是 CGLIB 代理。使用代理有某些限制,其中一些在 了解 AOP 代理中进行了描述。此外,请记住始终在代理类上使用方法和访问器;直接字段访问将不起作用。

其他配置选项

对于大多数情况,LocalValidatorFactoryBean默认配置就足够了。从消息插值到遍历解析,各种 Bean Validation 构造都有许多配置选项。有关这些选项的更多信息,请参阅 LocalValidatorFactoryBean javadoc。

3.7.3. 配置DataBinder

从 Spring 3 开始,您可以使用 Validator 配置 DataBinder 实例. 配置完成后,您可以通过调用binder.validate()来调用Validator. 任何验证 Errors都会自动添加到活页夹的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 验证配置。

3.7.4. Spring MVC 3 验证

请参阅Spring MVC 章节中的验证

0

评论区