Spring Framework核心技术-中文文档(校验、数据绑定、类型转换)


考虑将校验作为逻辑的一部有有点也有缺点,Spring提供了一种设计,既不排除校验也不排除数据绑定。特别是,校验不应该与Web层紧耦合,应该更容易本地化,它应该可以插入任何可用的校验器中。考虑到这些问题,Spring提供了 Validator约定,其又基本又极其适用于应用程序的每一个层面。

数据绑定让用户输入可以动态绑定到应用程序的域模型是非常有用的(或者你用于处理用户输入的任何对象)。Spring提供了名称为DataBinder的工具来做这这件事。ValidatorDataBinder组成validation包,其主要用于但不限于web层。

BeanWrapper是在Spring Framework中的基本概念并用在很多地方。但是,你可能不需要直接使用BeanWrapper。因为这是一个参考文档,因此我们觉得可能需要一些解释。我们在本章节解释BeanWrapper,如果你要使用它,最有可能的情况是,当你尝试将数据绑定到对象时,你差不多要这么做。

Spring的DataBinder和低级别的BeanWrapper同时使用PropertyEditorSupport实现来转换和格式化属性值。PropertyEditorPropertyEditorSupport类型是JavaBean规范的一部分并且也会在本章节进行说明。Spring的core.convert包提供了通用类型转换工具,还有一个高级别format包用于格式化UI字段值。你可以使用这些包作为对PropertyEditorSupport实现的简化版替代方案。他们也会在本章节进行讨论。

Spring通过设置基础设施支持Java Bean检验并适配Spring的自己的Validator约定。应用程序可以全局一次启用Bean Validation,正如在Java Bean Validation所描述的,将其作为所有验证需求的唯一选择。在web层,应用程序可以进一步通过DataBinder为每一个本地控制器注册Spring Validator实例,正如在Configuring a DataBinder,其对于插入自定义验证逻辑是非常有用的。

使用Spring的验证器接口验证

Spring提供了一个Validator接口,你可以用来验证对象。Validator接口可以通过使用Errors对象工作,在验证的过程中,验证器可以向Errors对象报告验证失败。

考虑以下小数据对象的示例:

public class Person {

	private String name;
	private int age;

	// the usual getters and setters...
}

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

  • supports(Class):此Validator可以验证提供的Class的实例?
  • validate(Object, org.springframework.validation.Errors):验证给定的对象,如果存在验证错误,则将其注册到给定的Errors对象中。

实现一个Validator接口相当简单,特别是当你知道Spring Framework也提供了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(..)方法用于如果属性为null或者空字符串则拒绝name属性。可以看看ValidationUtils来了解上面所展示之外的它提供的功能。

尽管它可以通过实现一个单独的Validator类来在一个丰富对象中验证每一个内嵌的对象,在它自己Validator实现中为每一个内嵌类封装验证可能更好。一个“丰富”对象的简单示例Customer,它有两个String属性(first和second名称)的组合和复杂的Address对象。Address对象是Customer独立使用,所以一个独特的AddressValidator已经实现。如果你想要你的CustomeValidator重用在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找到。

解析代码到错误信息

我们介绍了数据绑定和验证。此章节涵盖了相对应验证错误的输出信息。在前一章节展示的示例中,我们拒绝了nameage字段。如果我们想要通过使用MessageSource输出错误信息,我们可以使用我们拒绝nameage字段时提供的错误码这样做。当你调用来自Errors接口的rejectValue或者其他reject方法的其中一个时,底层实现不仅注册你传入的编码,而且还注册多个额外的错误编码。MessageCodesResolver决定Errors接口注册的错误编码。默认情况下,使用DefaultMessageCodesResolver,其不仅注册一个带有你给定的编码的信息,而且还注册包含你传入到拒绝方法的字段名称的信息。所以,如果你通过使用rejectValue("age", "too.darn.old")拒绝一个字段,除了too.darn.old编码之外,Spring也注册too.darn.old.agetoo.darn.old.age.int(第一个包含字段名称,第二个包含字段类型)。这是为了方便开发人员定位错误信息而进行的。

关于MessageCodesResolver的更多信息和默认策略,依次都可以在MessageCodesResolverDefaultMessageCodesResolver找到。

Bean填充和BeanWrapper

org.springframework.beans包遵守JavaBean标准。JavaBean是一个带有默认无参构造器的类并且遵循命名规范例如名称为bingoMadness的属性,会有一个setter方法setBingoMadness(..)和一个getter方法getBingoMadness()。关于JavaBean的规范的更多信息,请查看javabeans

在bean包中一个非常重要的类是BeanWrapper接口和它相对应的BeanWrapperImpl实现。正如javadoc所述,BeanWrapper提供set和get属性值的功能(逐个或者批量),获取属性描述符,查询属性他们是否可读和可写。而且,BeanWrapper提供对内嵌属性的支持,可以对无限制深度的子属性设置属性值。BeanWrapper也支持在目标类中无需支持代码的情况下添加标准JavaBean PropertyChangeListenersVetoableChangeListeners的能力。最后但同样重要的是,BeanWrapper提供对设置索引的属性的支持。BeanWrapper通常不会被应用程序代码直接使用,通过DataBingerBeanFactory使用。

BeanWrapper的工作方式通过它的名称可以得到提示:它封装了一个bean来执行该bean的行为,例如set和检索属性。

set和get基本和嵌套属性

set和get属性通过BeanWrapper重载的方法变体setPropertyValuegetPropertyValue完成。请查看他们的Javadoc了解细节。以下表格展示了这些规范的一些示例:

表达式说明
name表示属性name,相对应的getName()或者isName()setName(..)方法。
account.name表示属性account的内嵌属性name,相对应的getAccount().setName()或者getAccount().getName()方法
account[2]表示索引属性account的第三个元素。索引的属性可以是类型array,list,或者其他排序的集合。
account[COMPANYNAME]表示通过account Map属性的COMPANYNAME键的map条目的值

如果你不打算直接使用BeanWrapper,那么下一部分对你没那么至关重要。如果你只使用DataBinderBeanFactory和他们默认的实现,你应该跳到PropertyEditors章节

以下两个示例类使用BeanWrapper来get和set属性:

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

以下代码片段展示如何检索和填充实例化的CompanyEmployee的一些属性的示例:

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实现

Spring使用PropertyEditor的概念来实现ObjectString的转换。将属性以不同于对象本身的方式表示,会非常方便。例如,Date可以以人可读的方式表示(正如String:'2007-14-09'),而且我们仍可以将人类可读性转换为原始日期(或者更好的是,将人类可读的任何日期转换为日期对象)。此行为可以通过注册自定义类型为java.beans.PropertyEditor的修改器来实现。在BeanWrapper上注册自定义修改器或者替代方案是在一个特定的IoC容器中(正如在前一章提到的)注册自定义编辑器,使其具备将属性转换为所需类型的知识。要了解更多关于PropertyEditor,请查看来自Oracle的java.beans包的javadoc

在Spring中使用属性修改的几个示例:

  • 使用PropertyEditor实现完成bean的设置属性。当你使用String作为一些在XML文件中声明的bean的属性的值时,Spring(如果响应的属性的setter有Class参数)使用ClassEditor来尝试解析Class类的参数。
  • 使用所有类型的PropertyEditor实现完成在Spring的MVC框架中解析HTTP请求参数,你可以在CommandController的所有子类中手动绑定。
说明
ByteArrayPropertyEditor字节数组修改器。将对应的字符串转换为字节表示。通过BeanWrapperImpl默认注册。
ClassEditor将表示类的字符日字符串解析为实际类,反之亦然。当类没有被发现,IllegalArgumentException抛出。默认情况下,通过BeanWrapperImpl注册。
CustomBooleanEditor可自定义的属性修改器用于Boolean属性。默认情况下,通过BeanWrapperImpl注册,但是可以通过它的自定义实例作为一个自定义修改器来重写。
CustomCollectionEditor自定义的属性修改器用于集合。将任何源Collection转换为给定目标的Collection类型
CustomDateEditor可自定义的属性修改器用于java.util.Date,支持自定义DateFormat。默认情况下,不会注册。必须用户按需注册合适的格式。
CustomNumberEditor可自定义的属性修改器用于任何Number子类,例如Ingeter,Long,FloatDouble。默认情况下,通过BeanWrapperImpl注册,但是可以通过注册一个自定义实例进行重写。
FileEditor将字符串解析为java.io.File对象。默认情况下,BeanWrapperImpl注册。
InputStreamEditor单向属性修改器,可以接受一个字符串并产生(通过中间ResourceEditorResource)一个InputStream,以便InputStream属性可以作为字符串直接设置。注意默认用法不会关闭InputStream。默认情况下通过BeanWrapperImpl注册。
LocaleEditor可以将字符串解析为Locale对象,反之亦然(字符串格式是[language]_[country]_{variant],与LocaltoString()方法一样)。而且也接收空格作为分隔符,作为下划线的一种替代方案。默认情况下,通过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实现,和大多数原生类型的实现。注意,如果他们与他们处理的类在相同的包路径下并且与该类有相同的名称,只是在末尾添加了Editor,则标准的JavaBean底层结构自动发现PropertyEditor类(无需你必须显性地注册他们)。例如,一个存在以下类和包结构的示例,其将满足SomethingEditor类被识别,并当做处理Something-类型的PropertyEditor来使用。

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

注意,你也可以使用标准的BeanInfo JavaBean机制(请查看对一些扩展的描述)。以下示例使用BeanInfo机制来显性注册一个或者多个带有相关类属性的PropertyEditor实例:

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

以下依赖的SomethingBeanInfo类的Java源代码关联了一个具有Something类的age属性的CustomNumberEditor

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容器最终使用标准的JavaBean PropertyEditor实现来讲这些字符串转换为属性的复杂类型。Spring预注册多个自定义的PropertyEditor实现(例如,以字符串表示的类名称转换为Class对象)。除此之外,Java的标准JavaBean PropertyEditor查找机制允许为类命名适当的PropertyEditor,并将其放置到与它提供支持的类相同的包路径,这样它可以自动发现它。

如果有需求注册其他自定义PropertyEditors,几个机制是可以使用的。最手动方式,不太方便并且不太推荐,是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,假设你有一个BeanFactory引用的话。另外一种机制是使用称为CustomEditorConfigurer的特殊的bean后置处理器。尽管你可以使用BeanFactory的bean工厂后置处理器,CustomEditorConfigurer有一个内置的属性设置,所以我们强烈推荐你在ApplicationContext中使用它,你可以以任何其他bean类似的方式部署它,并且自动检测和提供它。

注意所有的bean工厂和应用程序上下文自动使用多个内建的属性修改器,通过使用BeanWrapper来处理属性转换。标准的属性修改器,在上一章节列出了BeanWrapper注册的。除此之外,ApplicationContext也重写或者添加额外的处理器,以便根据特定的应用上下文类型以适当的方式处理资源查找。

标准的JavaBean PropertyEditor实例用于将以字符串表达的属性值转换为实际的属性的复杂类型。你可以使用CustomEditorConfigurer,一个bean工厂后置处理器,要方便地将额外PropertyEditor实例的支持添加到一个ApplicationContext

package example;

public class ExoticType {

	private String name;

	public ExoticType(String name) {
		this.name = name;
	}
}

public class DependsOnExoticType {

	private ExoticType type;

	public void setType(ExoticType type) {
		this.type = type;
	}
}

当已经完全设置,我们想要可以将类型属性赋值为一个字符串,其PropertyEditor转换为实际ExoticeType实例。以下bean定义展示了如果设置此关系:

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

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

package example;

import java.beans.PropertyEditorSupport;

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

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

最后,以下示例展示了如何使用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的BeanWrapperDataBingder)实现的接口。PropertyEditorRegistrar实例与CustomEditorConfigurer在此描述)结合使用是相当方便的,其暴露了一个称为setPropertyEditorRegistrars(..)的属性。以这种方式添加到CustomEditorConfigurerPropertyEditorRegistrar实例可以容易地与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注册代码封装到一个类,然后可以被所需的多个控制器之间进行共享。

Spring类型转换

core.convert包提供了一个常用的类型转换系统。系统定义了一个SPI来实现类型转换逻辑并提供了一个API在运行时来执行类型转换。在Spring容器内,你可以使用此系统作为PropertyEditor实现的替代方案来讲外部的bean属性值字符串转换为要求的属性类型。你也可以在应用程序内任何位置的使用公用的API,以实现类型转换的需求。

转换器SPI

实现类型转换逻辑的SPI是简单的和强制类型的,正如以下接口定义所展示的:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

	T convert(S source);
}

要创建你自己的转换器,实现Converter接口和参数化S作为你要将从此类型进行转换,T作为你将要转换成的类型。如果集合或者数组的S需要被转换为一个数组或者集合的T,并且已经注册了委托的数组和集合的转换器,你还可以透明地应用这样的一个转换器(DefaultConversionService默认情况下就能做到)。

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

为了方便起见,几个转换器实现在core.convert.support包内提供。这些包含了从字符串转换为数字和其他常用类型的转换器。以下所列出的展示了StringToInteger类,是一个典型的Converter实现:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

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

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

使用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接口,你应该只有在需要的时候使用它。优先考虑Converter或者ConverterFactory用于基本的类型转换需求。

使用ConditionalGenericConverter

有时候,只有当特定的条件处理为true是才有你想要运行的Converter。例如,只有当特定的注解在目标字段存在时,你可能想要运行一个Converter,或者只有当特定的方法(例如static valueOf方法)在目标类定义时你可能才想要运行一个ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的结合,允许你定义这样的常用匹配标准:

public interface ConditionalConverter {

	boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter一个好的示例是IdToEntityConverter,它在持久化实体标识符和实体引用之间转换。这样一个IdToEntityConverter只有在目标实体类型申明了一个车静态finder方法(例如,findAccount(Long))时才会匹配。你可以在matchs(TypeDescriptor,TypeDescriptor)中执行这样的一个finder方法检查。

ConversionService API

ConversionService定义了一个一致性的,用于在运行时执行类型转换逻辑的API。Converters通常在以下外观接口运行:

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

稳固的ConversionService实现在core.convert.support包中提供。GenericConversionService是常用目标实现在大多数环境中适用于使用。ConversionServiceFactory提供一个方便的工厂来创建通用的ConversionService配置。

配置ConversionService

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

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

要Spring注册一个默认的ConversionService,添加以下idconversionService的bean定义。

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

默认的ConversionService可以在字符串,数组,枚举,集合,map和其他常用的类型之间转换。要使用你自己定义的转换器来补充或者重写默认的转换器,设置converters属性。属性值可以实现任何ConverterConverterFactory或者GenericConverter接口:

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

在Spring MVC应用程序中使用ConversionService也是比较常见的。请查看在Spring MVC章节中的转换和格式化

在某些场景中,你可能希望在转换期间进行格式化。请查看FormatterRegistry SPI了解更多关于使用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上注入相同的转换器。

值类型转换器对于数组和集合是重用的,所以无需创建特定的转换器来讲S集合转换为T集合,假设标准的集合处理是合理的。

Spring字段格式化

正如在前章节讨论的,core.convert是多用途的类型转换系统。它提供了一整套ConversionService API以及强类型的Converter SPI用于实现从一个类型到另外一个类型的转换逻辑。Spring容器使用此系统来绑定bean属性值。除此之外,Spring表达式语言(SpEL)和DataBinder使用此系统来绑定字段值。例如,当SpEL需要强制Short转换为Long来完成expression.setValue(Object bean, Object value)尝试,core.convert系统执行强制转换。

现在考虑典型的客户端环境的类型转换需求,例如web或者桌面应用程序。在这样的环境中,你通常将字符串转换为支持客户端回传过程的类型,以及返回String为支持展示呈现过程。除此之外,你经常需要本地化String 值。更加常用的core.convert Convert SPI不需要直接解决这样格式化需求。要直接处理他们,Spring提供了方便的Formatter SPI,其提供了一个简单的和强大的用于客户端环境的PropertyEditor实现的替代方案。

通常,当你需要实现多功能的类型转换逻辑时,你可以使用Converter SPI–例如,用于在java.util.DateLong之间的转换。当你在一个客户端程序(例如web应用程序)工作并且需要转换和打印本地化字段值时,你可以使用Formatter SPI。ConvertsionService提供了统一的类型转换API用于两个SPI。

Formatter SPI

Formatter SPI来实现字段格式化逻辑是简单的和强类型化的。以下列表展示了Formatter接口定义:

package org.springframework.format;

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

Formatter扩展自PrinterParser构建块接口。以下列表展示了这两个接口的定义:

public interface Printer<T> {

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

public interface Parser<T> {

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

要创建你自己的Formatter,实现正如早前展示的Formatter接口。参数化T是你希望格式化的对象类型–例如,java.util.Date。实现print()操作来打印一个T实例用于在客户端环境进行展示。实现parse()操作来转换来自客户端环境格式化表示的T实例。你的如果转换尝试失败,Formatter应该抛出一个ParseException或者一个IllegalArgumentException。注意确保你的Formatter实现是线程安全的。

为方便起见,format子包提供了几个Formatter实现。number包提供了NumberStyleFormatterCurrencyStyleFormaterPercentStyleFormatter来格式化Number对象,其使用了java.text.NumberFormatdatetime包提供了DateFromatter来使用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 Issue来进行贡献。

注解驱动的格式化

字段格式化可以通过字段类型或者注解进行配置。要将一个注解绑定到Formatter,实现AnnotationFormatterFactory。以下列表展示了AnnotationFormatterFactory接口的定义:

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

	Set<Class<?>> getFieldTypes();

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

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

要创建一个实现:

  1. 参数化A是字段annotationType,其就是你希望关联的格式化逻辑 – 例如org.springframework.format.annotation.DateTimeFormat
  2. getFiledTypes()返回被使用注解的字段类型。
  3. getPrinter()返回Printer来打印注解字段的值。
  4. getParser()返回Parser来转换注解字段的一个clientValue

以下示例AnnotationFormatterFactory实现将@NumberFormat注解绑定到一个formatter,以允许指定的数字风格或者格式。

public final class NumberFormatAnnotationFormatterFactory
		implements AnnotationFormatterFactory<NumberFormat> {

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

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

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

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

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

要触发格式,你可以使用NumberFormat注解字段,正如以下示例所示:

public class MyModel {

	@NumberFormat(style=Style.CURRENCY)
	private BigDecimal decimal;
}
格式注解API

方便的格式化注解API存在于org.springframework.format.annotation包。你可以使用@NumberFormat来格式化Number字段,例如DoubleLong,并且@DateTimeFormat来格式化java.util.Datejava.util.CalenderLong(毫秒的时间戳)以及JSR-310 java.time
以下示例使用@DateTimeFormat来格式化java.util.Date为一个ISO时间(yyyy-MM-dd):

public class MyModel {

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

FormatterRegistry SPI

FormatterRegistry是一个用于注册格式化器和转换器的SPI。FormattingConversionService是一个FormatterRegistry实现,适用于大多数环境。你可以通过FormattingConversionServiceFactoryBean程序化或者声明式地配置这个作为Spring Bean的变体,等等。因为此实现也实现了ConversionService,你可以直接配置它与Spring的DataBinder和Spring Expression Language(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);
}

正如前面列表所展示的,你可以通过字段类型或者注解注册格式化器。

FormatterRegistry SPI允许你集中配置格式化规则,而不是在控制器之间重复这样的配置。例如,你可以想要强制所有的时间字段以某种方式被格式化或者使用特定注解的字段以某种方式被格式化。使用共享的FormatterRegistry,你定义了这些规则一次,无论何时需要格式化都能应用他们。

FormatterRegistrar SPI

FormatterRegistrar是用于通过FormatterRegistry注册格式化器和转换器的SPI。以下列表展示了它的接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

	void registerFormatters(FormatterRegistry registry);
}

当对于一个给定的格式化分类注册多个相关的转换器和格式化器,FormatterRegistrar是非常有用的,例如时间格式化。当声明式注册不满足时,它也可以非常有用–例如,当需要将一个格式化器索引到一个与它自己的<T>不同的特定的字段类型下或者当注册一个Pringer/Parser对。下一章节提供了关于朱让其和格式化器注册的更多信息。

在Spring MVC 中配置格式化

请在Spring MVC章节中查看Conversion和Formatting

配置一个全局Date和Time格式

默认情况下,日期和时间字段不是使用@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 dateTimeRegistrar = new DateTimeFormatterRegistrar();
		dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
		dateTimeRegistrar.registerFormatters(conversionService);

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

		return conversionService;
	}
}

如果你更倾向于基于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的支持。

Bean校验概览

Bean校验通过约束声明和元数据为Java应用程序提供了一个校验的通用方式。要使用它,你使用声明式校验对域模型属性进行注解,然后这些约束在运行时强制执行。有一些内置约束,你也可以定义你自己的自定义约束。

考虑以下示例,其展示了一个简单有两个属性的PersonForm模式:

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

Bean验证允许你声明约束正如以下示例所示:

public class PersonForm {

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

	@Min(0)
	private int age;
}

一个Bean Validation 验证器,然后基于声明的约束校验此类的实例。请查看关于API通用信息的Bean Validation。请查看Hibernate Validator文档了解特定的约束。要学习如何设置bean验证提供者作为一个Spring bean,请继续阅读。

配置一个Bean Validation提供者

Spring提供对Bean Validation API包括Bean Validation提供者作为Spring bean启动的完整支持。这允许你注入jakarta.validation.ValidatorFactory或者jakarta.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提供者,例如Hibnernate Validator,应该在类路径中存在则自动化监测。

注入一个Validator

LocalValidatorFactoryBean同时实现jakarta.validation.ValidatorFactoryjakarta.validation.Validator,以及Spring的org.springframework.validation.Validator。你可以将特写接口任意一个引用注入到需要调用校验逻辑的bean。

如果你更倾向于直接与Bean Validation API一起工作,你可以注入jakarta.validation.Validator的引用,正如以下示例所示:

import jakarta.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注解,声明了约束和它的可配置属性。
  • jakarta.validation.ConstraintValidator接口的实现,其实现了约束行为。

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

默认情况下,LocalValidatorFactoryBean配置了SpringConstraintValidatorFactory,其使用了Spring来创建ConstraintValidator实例。这允许你的自定义ConstraintValidators从以来注入中受益,就像请他Spring bean。

以下示例展示了一个自定义的@Constraint声明,其遵守相关联的ConstraintValidator实现,使用了Spring来依赖注入:

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

public class MyConstraintValidator implements ConstraintValidator {

	@Autowired;
	private Foo aDependency;

	// ...
}

正如前面的示例所示,ConstraintValidator实现可以有它的依赖@Autowired,正如任何其他Spring bean。

Spring驱动的方法校验

你可以通过MethodValidationPostProcessorbean定义将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了解关于这些选项的更多信息。

配置一个DataBinder

你可以配置一个带有ValidatorDataBinder实例。一旦配置,你可以通过调用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 Validation

请查看在Spring MVC章节的Validation

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值