文章摘要(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
来完成此任务。 Validator
和DataBinder
组成了验证包,主要用于但不限于Web层。
BeanWrapper
是 Spring 框架中的一个基本概念,在很多地方都有使用。但是,您可能不需要直接使用BeanWrapper
。但是,因为这是参考文档,所以我们认为可能需要进行一些解释。我们将在本章中解释它,因为如果您要使用BeanWrapper
,您很可能在尝试将数据绑定到对象时这样做。
SpringDataBinder
和较低级别BeanWrapper
都使用PropertyEditorSupport
实现来解析和格式化属性值。PropertyEditor
和 PropertyEditorSupport
类型是 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
或空字符串。查看 ValidationUtils
javadoc 以了解除了前面显示的示例之外它还提供了哪些功能。
虽然实现单个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. 将代码解析为错误消息
我们涵盖了数据绑定和验证。本节介绍与验证错误相对应的输出消息。在上一节所示的示例中,我们拒绝了name
和age
字段。如果我们想通过使用MessageSource
来输出错误消息 ,我们可以使用我们在拒绝字段时提供的错误代码(在本例中为“姓名”和“年龄”)来完成。当您调用(直接或间接,通过使用例如ValidationUtils
类)rejectValue
或Errors
接口中的其他reject
方法之一时,底层实现不仅注册您传入的代码,而且还注册许多额外的错误代码。MessageCodesResolver
确定Errors
接口寄存器的错误代码。默认情况下, 使用DefaultMessageCodesResolver
,它(例如)不仅使用您提供的代码注册消息,而且还注册包含您传递给拒绝方法的字段名称的消息。所以,如果你用rejectValue("age", "too.darn.old")
拒绝一个字段,除了代码too.darn.old
之外,Spring 还会注册too.darn.old.age
和 too.darn.old.age.int
(第一个包含字段名称,第二个包含字段类型)。这样做是为了方便开发人员定位错误消息。
有关MessageCodesResolver
和 默认策略的更多信息可以分别在MessageCodesResolver
和 DefaultMessageCodesResolver
的 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 的能力, PropertyChangeListeners
和VetoableChangeListeners
无需在目标类中支持代码。最后但并非最不重要的一点是,BeanWrapper
提供了对设置索引属性的支持。通常不由应用程序代码直接使用BeanWrapper
,而是由 DataBinder
和BeanFactory
.
BeanWrapper
工作方式部分由其名称表示:它包装一个 bean 以对该 bean 执行操作,例如设置和检索属性。
3.3.1. 设置和获取基本和嵌套属性
设置和获取属性是通过 BeanWrapper
的 setPropertyValue
和 getPropertyValue
重载方法变体完成的. 有关详细信息,请参阅他们的 Javadoc。下表显示了这些约定的一些示例:
表达式 | 解释 |
---|---|
name |
指示与 getName() 或 isName() 和setName(..) 方法对应的属性名称。 |
account.name |
指示对应于(例如) getAccount().setName() 或getAccount().getName() 方法的属性帐户的嵌套属性名称。 |
account[2] |
指示索引属性account 的第三个元素。索引属性可以是array 、list 或其他自然排序的集合。 |
account[COMPANYNAME] |
指示由 account Map 属性的 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
s 和Employee
s 的某些属性:
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
的概念来实现 Object
和String
之间的转换。以与对象本身不同的方式表示属性可能很方便。例如,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 属性编辑器,例如Integer 、Long 、Float 或 Double . 默认情况下,注册者为BeanWrapperImpl ,但可以通过将其自定义实例注册为自定义编辑器来覆盖。 |
FileEditor |
将字符串解析为java.io.File 对象。默认情况下,由BeanWrapperImpl 注册 。 |
InputStreamEditor |
单向属性编辑器,可以接受一个字符串并产生(通过一个中间ResourceEditor 和Resource )InputStream ,以便InputStream 可以将属性直接设置为字符串。请注意,默认用法不会为您关闭InputStream 。默认情况下,由 BeanWrapperImpl 注册。 |
LocaleEditor |
可以将字符串解析为Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant] ,与Locale 的方法toString() 相同)。也接受空格作为分隔符,作为下划线的替代。默认情况下,由 BeanWrapperImpl 注册。 |
PatternEditor |
可以将字符串解析为java.util.regex.Pattern 对象,反之亦然。 |
PropertiesEditor |
可以将java.util.Properties 字符串(使用类的 javadoc 中定义的格式格式化 )转换为Properties 对象。默认情况下,由BeanWrapperImpl 注册。 |
StringTrimmerEditor |
修剪字符串的属性编辑器。可选地允许将空字符串转换为null 值。默认情况下未注册 - 必须是用户注册的。 |
URLEditor |
可以将 URL 的字符串表示解析为实际URL 对象。默认情况下,由BeanWrapperImpl 注册。 |
Spring 使用java.beans.PropertyEditorManager
为可能需要的属性编辑器设置搜索路径。搜索路径还包括sun.bean.editors
,其中包括诸如Font
、Color
和大多数基本类型的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 源代码将CustomNumberEditor
与Something
类的age
属性相关联:
public class SomethingBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
@Override
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
}
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
注册其他自定义PropertyEditor
实现
当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 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()));
}
}
最后,以下示例显示了如何使用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>
使用PropertyEditorRegistrar
向 Spring 容器注册属性编辑器的另一种机制是创建和使用PropertyEditorRegistrar
. 当您需要在几种不同的情况下使用同一组属性编辑器时,此接口特别有用。您可以编写相应的注册器并在每种情况下重复使用它。 PropertyEditorRegistrar
实例与名为 PropertyEditorRegistry
的接口一起工作,该接口由 Spring BeanWrapper
(and DataBinder
) 实现。PropertyEditorRegistrar
实例与CustomEditorConfigurer
( 在此处描述)结合使用时特别方便,它公开了一个名为setPropertyEditorRegistrars(..)
的方法. 以这种方式添加到PropertyEditorRegistrar
的CustomEditorConfigurer
实例可以很容易地与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
当您需要集中整个类层次结构的转换逻辑时(例如,从转换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
具有更灵活但类型更弱的签名,支持在多个源类型和目标类型之间进行转换。此外,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
满足基本类型转换需求。
使用ConditionalGenericConverter
有时,您希望转换器仅在特定条件成立时运行。例如,您可能希望仅当目标字段上存在特定注解时才运行 Converter
,或者仅当目标类上定义了特定方法(例如静态valueOf
方法)时您可能希望运行 Converter
。 ConditionalGenericConverter
是 GenericConverter
和ConditionalConverter
接口的联合,可让您定义此类自定义匹配条件:
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
ConditionalGenericConverter
很好的例子是IdToEntityConverter
在持久实体标识符和实体引用之间进行转换。IdToEntityConverter
只有当目标实体类型声明了静态查找器方法(例如, findAccount(Long)
)时,这样的才可能匹配。您可以在matches(TypeDescriptor, TypeDescriptor)
的实现中执行此类查找器方法检查。
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 注册默认值,添加以下 id
为 conversionService
的 bean
定义:
<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 章节中的转换和格式化。
在某些情况下,您可能希望在转换期间应用格式。有关使用参考 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(...)
}
}
对于大多数用例,您可以使用指定 targetType
的convert
方法,但它不适用于更复杂的类型,例如参数化元素的集合。例如,如果您想以编程方式将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
的自动注册适用于大多数环境的转换器。这包括集合转换器、标量转换器和基本Object
到String
转换器。您可以使用DefaultConversionService
类上的静态addDefaultConverters
方法向ConverterRegistry
注册相同的转换器。
值类型的转换器可用于数组和集合,因此无需创建特定的转换器来从 Collection <S>
转换为 Collection <T>
,假设标准集合处理是合适的。
3.5. spring字段格式
正如上一节所讨论的,core.convert
是一个通用的类型转换系统。它提供了一个统一的ConversionService
API 以及一个强类型的Converter
SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用此系统绑定 bean 属性值。此外,Spring 表达式语言(SpEL)都使用这个系统来绑定字段值。例如,当 SpEL 需要将 Short
强制转换为Long
以完成一次expression.setValue(Object bean, Object value)
尝试时,core.convert
系统会执行强制。
现在考虑典型客户端环境的类型转换要求,例如 Web 或桌面应用程序。在这样的环境中,您通常转换String
以支持客户端回发过程,以及转换回String
以支持视图呈现过程。此外,您经常需要本地化String
值。更通用的core.convert
Converter
SPI 不直接解决此类格式要求。为了直接解决这些问题,Spring 3 引入了一个方便的Formatter
SPI,它为客户端环境的实现提供了一个简单而健壮的替代方案。PropertyEditor
Converter
通常,当您需要实现通用类型转换逻辑时,您可以使用SPI——例如,在 ajava.util.Date
和 a之间进行转换Long
。Formatter
当您在客户端环境(例如 Web 应用程序)中工作并且需要解析和打印本地化的字段值时,您可以使用SPI。ConversionService
为两个 SPI 提供了统一的类型转换 API 。
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;
}
要创建您自己的格式化程序,请实现前面所示的格式化程序接口。将 T
参数化为您希望格式化的对象类型——例如 java.util.Date
。实现 print()
操作以打印 T
的实例以在客户端区域设置中显示。实现 parse()
操作以从客户端语言环境返回的格式化表示中解析 T
的实例。如果解析尝试失败,您的 Formatter
应该抛出 ParseException
或 IllegalArgumentException
。请注意确保您的 Formatter 实现是线程安全的。
为了方便起见,格式子包提供了几种 Formatter
实现。 number
包提供 NumberStyleFormatter
、CurrencyStyleFormatter
和PercentStyleFormatter
来格式化使用 java.text.NumberFormat
的 Number
对象。datetime
包提供了一个 DateFormatter
来使用java.text.DateFormat
来格式化java.util.Date
对象。
下面DateFormatter
是一个实现Formatter
的示例:
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
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);
}
创建一个实现:
- 将
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
I org.springframework.format.annotation
包中存在可移植格式注解 AP 。您可以使用@NumberFormat
格式化Number
字段,例如Double
和 Long
,@DateTimeFormat
格式化java.util.Date
, java.util.Calendar
,Long
(用于毫秒时间戳)以及 JSR-310 java.time
。
以下示例用于@DateTimeFormat
将java.util.Date
格式化为 ISO 日期 (yyyy-MM-dd):
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
3.5.3. FormatterRegistry
SPI
FormatterRegistry
是一个用于注册格式化程序和转换器的 SPI。 FormattingConversionService
是FormatterRegistry
适用于大多数环境的实现。您可以以编程方式或声明方式将此变体配置为 Spring bean,例如使用FormattingConversionServiceFactoryBean
. 由于此实现还实现了ConversionService
,因此您可以直接将其配置为与 SpringDataBinder
和 Spring 表达式语言 (SpEL) 一起使用。
以下清单显示了FormatterRegistry
SPI:
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. FormatterRegistrar
SPI
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.ValidatorFactory
或javax.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.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 需要 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 章节中的验证。
评论区