3. Validation, Data Binding, and Type Conversion
----本章提供了很多验证,类型转换及格式化的接口,应用场景是什么,真的会比自己实现这些功能来的更方便吗? 还是只有在很少见的场景比如确实会存在大量需要转换的程序中
验证,数据绑定及类型转换
验证:spring提供了一个Validator接口,该接口在应用程序的每一层中都是基础与实用的
数据绑定:数据绑定对于让用户输入动态绑定到应用程序的域模型(或者用于处理用户输入的任何对象)非常有用。Spring提供了一个恰当命名的DataBinder 。Validator和DataBinder组成了验证包,它主要用于但不局限于MVC框架。
Spring 3引入了一个 core.convert包,它提供了一般类型转换工具,以及用于格式化UI字段值的更高级“格式”包
从版本4.0开始,Spring Frameworks支持Bean Validation 1.0(JSR-303)和Bean Validation 1.1(JSR-349),用于设置支持并使它们适应Spring的Validator接口
3.1. Validation by Using Spring’s Validator Interface
1、数据绑定失败:比如需要数字却输入了字母;
2、数据不合法:可以认为是业务错误,通过自定义验证器验证,如用户名长度必须在5-20之间,我们却输入了100个字符等;
3、错误对象:当我们数据绑定失败或验证失败后,错误信息存放的对象,我们叫错误对象,在Spring Web MVC中Errors是具体的代表者;线程不安全对象;
4、错误消息:是硬编码,还是可配置?实际工作应该使用配置方式,我们只是把错误码(errorCode)放入错误对象,在展示时读取相应的错误消息配置文件来获取要显示的错误消息(errorMessage);
一个使用Validator接口验证的简单示例
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
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) {
//rejectIfEmpty这个静态方法用于验证name属性非空
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
//如果验证失败 如果验证错误,则注册具有给定Errors对象的对象
//age 表示错误代码 "negativevalue" 表示错误信息
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
@Test
public void test() {
PersonValidator pers = new PersonValidator();
Person p = new Person("111", 120);
//具体erros接口实例见Spring API
BindException errors = new BindException(p, "person");
pers.validate(p, errors);
List<ObjectError> allErrors = errors.getAllErrors();
System.out.println("size="+allErrors.size());
//如果有多个异常,一样可以输出
for (int i=0;i<allErrors.size();i++) {
System.out.println(allErrors.get(i).getCode());
}
}
对于组合类型的验证,被依赖的对象如果有自己的Validator,在当前对象的Validator中可直接饮用
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();
}
}
}
3.2. Resolving Codes to Error Messages
MessageCodesResolver
DefaultMessageCodesResolver
首先进行数据绑定验证,如果验证失败会通过MessageCodesResolver生成错误码放入Errors错误对象;内部使用MessageCodesResolver解析数据绑定错误到错误码,默认DefaultMessageCodesResolver-----这个还是需要api看看怎么用
3.3. Bean Manipulation and the BeanWrapper
org.springframework.beans软件包遵循JavaBeans标准。JavaBean是一个具有默认无参数构造函数的类,它遵循命名约定
beans包中一个非常重要的类是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符和查询属性以确定它们是可读还是可写的功能;此外,BeanWrapper提供了对嵌套属性的支持,允许将子属性上的属性设置为无限的深度。BeanWrapper还支持添加标准JavaBeans PropertyChangeListeners和VetoableChangeListeners的能力,而不需要支持目标类中的代码。最后但并非最不重要的是,BeanWrapper提供了用于设置索引属性的支持。BeanWrapper通常不直接由应用程序代码使用,而是由DataBinder和BeanFactory使用。-----这个是对bean的封装,提供对bean操作的功能
3.3.1. Setting and Getting Basic and Nested Properties
设置和获取基本和嵌套属性
示例为普通属性设置及嵌套属性设置
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. Built-in PropertyEditor Implementations
内置PropertyEditor实现
Spring使用PropertyEditor的概念来实现对象和字符串之间的转换。它可以方便地以不同的方式表示属性,而不是对象本身。例如,Date可以用人类可读的方式表示(如String:‘2007-14-09’),而我们仍然可以将人类可读表单转换回原始日期(或者,更好的是,将人类可读表单中输入的任何日期转换回Date对象)。这种行为可以通过注册java.beans.PropertyEditor编辑器的自定义编辑器来实现。在BeanWrapper上或在特定的IoC容器中注册自定义编辑器(如前一章所述),可以让其了解如何将属性转换为所需的类型
提供的实现
类 | 说明 |
---|---|
ByteArrayPropertyEditor | 字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认注册BeanWrapperImpl |
ClassEditor | 解析表示类到实际类的字符串,反之亦然。当找不到某个类时,IllegalArgumentException会抛出一个类。默认情况下,注册者 BeanWrapperImpl |
CustomBooleanEditor | 属性的可自定义属性编辑器Boolean。默认情况下,注册 BeanWrapperImpl但可以通过将其自定义实例注册为自定义编辑器来覆盖 |
CustomCollectionEditor | 集合的属性编辑器,将任何源Collection转换为给定的目标 Collection类型 |
CustomDateEditor | 可自定义的属性编辑器java.util.Date,支持自定义DateFormat。未默认注册。必须根据需要使用适当的格式进行用户注册 |
CustomNumberEditor | 定制的属性编辑器Number的子类,如Integer,Long,Float,或 Double。默认情况下,注册BeanWrapperImpl但可以通过将其自定义实例注册为自定义编辑器来覆盖 |
FileEditor | 将字符串解析为java.io.File对象。默认情况下,注册者 BeanWrapperImpl |
InputStreamEditor | 单向属性编辑器,可以获取字符串并生成(通过中间ResourceEditor和Resource),InputStream以便InputStream 属性可以直接设置为字符串。请注意,默认用法不会InputStream为您关闭。默认情况下,注册者BeanWrapperImpl |
LocaleEditor | 可以将字符串解析为Locale对象,反之亦然(字符串格式 [country][variant]与toString()方法 相同Locale)。默认情况下,注册者BeanWrapperImpl |
PatternEditor | 可以将字符串解析为java.util.regex.Pattern对象,反之亦然 |
PropertiesEditor | 可以将字符串(使用java.util.Properties类的javadoc中定义的格式进行格式化 )转换为Properties对象。默认情况下,注册者BeanWrapperImpl |
StringTrimmerEditor | 修剪字符串的属性编辑器。(可选)允许将空字符串转换为null值。默认情况下未注册 - 必须是用户注册的 |
URLEditor | 可以将URL的字符串表示形式解析为实际URL对象。默认情况下,注册者BeanWrapperImpl |
示例:–可能不是这么用的,再开发功能
PropertyEditor proEdi = new PropertiesEditor();
proEdi.setAsText("1231");
System.out.println(proEdi.getValue());
使用Spring容器注册属性编辑器的另一种机制是创建和使用PropertyEditorRegistrar。当您需要在几种不同情况下使用同一组属性编辑器时,此接口特别有用。您可以编写相应的注册商,并在每种情况下重复使用它。PropertyEditorRegistrar实例与一个名为PropertyEditorRegistry的接口一起工作,该接口由Spring BeanWrapper(和DataBinder)实现
这种PropertyEditor注册方式可以导致简洁的代码(实现initBinder(…)只有一行),并允许将通用PropertyEditor 注册代码封装在一个类中,然后Controllers根据需要共享
3.4. Spring Type Conversion
Spring 3引入了一个core.convert提供通用类型转换系统的包。系统定义了一个用于实现类型转换逻辑的SPI和一个用于在运行时执行类型转换的API。在Spring容器中,您可以使用此系统作为PropertyEditor实现的替代方法,以将外部化的bean属性值字符串转换为必需的属性类型。您还可以在需要进行类型转换的应用程序中的任何位置使用公共API。
3.4.1. Converter SPI
spring提供的转换接口,自定义只需实现该接口,将s状态转换为t状态
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
3.4.2. Using ConverterFactory
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
从String转换为java.lang.Enum对象的示例
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. Using GenericConverter
当您需要复杂的Converter实现时,请考虑使用该GenericConverter 接口;该接口支持在多个源类型和目标类型之间进行转换,还可以在实现转换逻辑时使用可用的源和目标字段上下文。这样的上下文允许类型转换由字段注释或在字段签名上声明的通用信息驱动
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
为了实现GenericConverter,让getConvertibleTypes()返回支持的源_目标类型对。然后实现转换(object,Type描述符,TypeDescriptor)以包含转换逻辑。源类型描述符提供对保存转换值的源字段的访问。目标TypeDescriptor提供对要设置转换值的目标字段的访问。
因为GenericConverter是一个更复杂的SPI接口,所以只有在需要时才能使用它。支持Converter或ConverterFactory基本类型转换需求
有时,Converter只有在特定条件成立时才需要运行。例如,您可能希望Converter仅在目标字段上存在特定注释时运行,或者Converter仅static valueOf在目标类上定义特定方法(例如方法)时才运行。 ConditionalGenericConverter是GenericConverter和 ConditionalConverter接口的联合,可以让您定义这样的自定义匹配条件
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
3.4.4. The 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. Configuring a ConversionService
ConversionService是一个无状态对象,设计用于在应用程序启动时实例化,然后在多个线程之间共享。在Spring应用程序中,通常ConversionService为每个Spring容器(或ApplicationContext)配置一个实例。ConversionService当需要框架执行类型转换时,Spring会选择并使用它。您也可以将其 ConversionService注入任何bean并直接调用它
如果没有ConversionService在Spring中注册,PropertyEditor则使用基于原始的系统
要注册一个默认ConversionService使用Spring,用添加以下bean定义id的conversionService
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认值ConversionService可以在字符串,数字,枚举,集合,映射和其他常见类型之间进行转换。要使用您自己的自定义转换器补充或覆盖默认转换器,请设置该converters属性。属性值可以实现任何的Converter,ConverterFactory或者GenericConverter接口
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>
3.4.6. Using a ConversionService Programmatically
要以ConversionService编程方式处理实例,您可以像对任何其他bean一样注入对它的引用
@Service
public class MyService {
@Autowired
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
TypeDescriptor提供了各种选项来实现更复杂的场景,比如:List的Integer到List的String
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转换器。您可以ConverterRegistry使用类addDefaultConverters 上的静态方法向任何注册器注册相同的转换器DefaultConversionService。
值类型转换器重新用于数组和集合,所以没有必要创建一个特定的转换器从转换Collection的S到 Collection的T,假设标准收集处理是适当的
3.5. Spring Field Formatting
core.convert是一种通用类型转换系统。它提供了统一的ConversionServiceAPI以及强类型ConverterSPI,用于实现从一种类型到另一种类型的转换逻辑。Spring容器使用此系统绑定bean属性值。另外,Spring Expression Language(SpEL)和DataBinder使用此系统绑定字段值
3.5.1. The Formatter SPI
Formatter用于实现字段格式化逻辑的SPI简单且强类型化
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
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;
}
以时间转换为例
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;
}
}
3.5.2. Annotation-driven Formatting
示例:更多见api
可能还是需要容器管理时才生效,需要测试
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
3.5.3. The FormatterRegistry SPI
FormatterRegistry是一个用于注册格式化器和转换器的SPI。 FormattingConversionService是FormatterRegistry适合大多数环境的实现。您可以使用编程或声明性地将此实现配置为Spring bean FormattingConversionServiceFactoryBean。由于此实现也实现了ConversionService,您可以直接将其配置为与Spring DataBinder和Spring表达式语言(SpEL)一起使用
3.5.4. The FormatterRegistrar SPI
FormatterRegistrar是一个SPI,用于通过FormatterRegistry注册格式化程序和转换器
3.5.5. Configuring Formatting in Spring MVC
springmvc中的章节:
https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/web.html#mvc-config-conversion
3.6. Configuring a Global Date and Time Format
配置全局日期和时间格式
为此,您需要确保Spring不会注册默认格式化程序。相反,您应该手动注册所有格式化程序。使用 org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar或 org.springframework.format.datetime.DateFormatterRegistrar类,具体取决于您是否使用Joda-Time库。
示例:此示例不依赖于Joda-Time库
@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 date conversion with a specific global format
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);
return conversionService;
}
}
xml形式
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="registerDefaultFormatters" value="false" />
<property name="formatters">
<set>
<bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
</bean>
</property>
</bean>
</set>
</property>
</bean>
</beans>
Joda-Time提供单独的不同类型来表示日期、时间和日期时间值。JodaTimeFormatterRegistrar的dateFormatter、timeFormatter和dateTimeFormatter属性应该用于为每种类型配置不同的格式。DateTimeFormatterFactoryBean 提供了一种创建格式化程序的便捷方法。
如果您使用Spring MVC,请记住明确配置使用的转换服务。对于基于Java的@Configuration,这意味着扩展 WebMvcConfigurationSupport类并重写该mvcConversionService()方法。对于XML,您应该使用元素的conversion-service属性 mvc:annotation-driven
3.7. Spring Validation
Spring 3为其验证支持引入了几项增强功能。首先,完全支持JSR-303 Bean Validation API。其次,当以编程方式使用时,Spring DataBinder可以验证对象以及绑定它们。第三,Spring MVC支持声明性地验证@Controller输入
3.7.1. Overview of the JSR-303 Bean Validation API
JSR-303标准化了Java平台的验证约束声明和元数据。通过使用此API,您可以使用声明性验证约束来注释域模型属性,并且运行时会强制执行它们。您可以使用许多内置约束。您还可以定义自己的自定义约束。
//可以为普通的属性增加声明性验证约束
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
有关JSR-303和JSR-349的一般信息,请参阅Bean Validation网站。有关默认参考实现的特定功能的信息,请参阅Hibernate Validator文档
3.7.2. Configuring a Bean Validation Provider
Spring提供对Bean Validation API的完全支持。这包括方便地支持将JSR-303或JSR-349 Bean Validation提供程序作为Spring bean引导。这使您 可以在应用程序中注入javax.validation.ValidatorFactory或javax.validation.Validator需要验证
- 注入验证器:LocalValidatorFactoryBean同时实现了javax.validation.ValidatorFactory和 javax.validation.Validator,以及Spring的 org.springframework.validation.Validator。您可以将这些接口中的任何一个引用注入到需要调用验证逻辑的bean中
示例:Validator作为bean属性 - 配置自定义约束:每个bean验证约束由两部分组成:*一个@Constraint声明约束及其可配置属性的注释。*实现javax.validation.ConstraintValidator约束行为的接口的实现。
要将声明与实现相关联,每个@Constraint注释都引用相应的ConstraintValidator实现类。在运行时,ConstraintValidatorFactory在域模型中遇到约束注释时, 实例化引用的实现。
默认情况下,LocalValidatorFactoryBean配置SpringConstraintValidatorFactory 使用Spring创建ConstraintValidator实例的a。这使得您的自定义 ConstraintValidators可以像任何其他Spring bean一样受益于依赖注入
示例:
@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;
...
}
- 验证方法:
您可以通过bean定义将Bean Validation 1.1支持的方法验证功能(以及作为自定义扩展,也可以通过Hibernate Validator 4.3)集成到Spring上下文中MethodValidationPostProcessor
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
要获得Spring驱动的方法验证资格,所有目标类都需要使用Spring的@Validated注释进行注释。(或者,您也可以声明要使用的验证组。)
3.7.3. Configuring a DataBinder
从Spring 3开始,您可以使用a配置DataBinder实例Validator。配置完成后,您可以Validator通过调用来调用binder.validate()。任何验证都会 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多个Validator实例 。将全局配置的bean验证与在DataBinder实例上本地配置的Spring组合时,这非常有用
3.7.4. Spring MVC 3 Validation
见springmvc