本篇太乱,请移步:
Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
写了删删了写,反复几次,对自己的描述很不满意,表达能力还是不行啊。另外,格式太花了,很不喜欢。
前提
1、什么是JavaBean?
简单类,无参构造,有SETTER/GETTER,其对应的字段称为属性。--其实还有isXxx,用于布尔类型的属性,略。
详见 https://en.wikipedia.org/wiki/JavaBeans
注意,这里的JavaBean不是Spring的bean。
好了,现在已经有了属性(property)的概念,下面所有的东西都是围绕它来进行的。
2、需求
我们应该明白一点,所有的技术都是源自需求而出现的(不严谨)。搞明白需求就搞明白了它们的应用场景。
假定我们有一个桌面应用(下文用 app 代替 桌面应用),需要你输入生日,然后该app会输出你的年龄。看起来很简单吧,app的后台逻辑只要用当前日期减去你的生日即可,但是,现实不是这样子的。
问题一,我们输入的永远是字符串,字符串需要转成日期格式才能被我们的app用使用。--对应 类型转换
问题二,我们输入的字符串转成的日期怎么给app后台逻辑使用? --对应 数据绑定
问题三,人的年龄是有限制的,不能为负数,不能太大(例如超过了200)。 --对应 校验
看到这里,你应该已经搞明白Validation、Data Binding、Type Conversion三者之间的关系了。
同样的问题也出现在浏览器与服务器的交互之中,因为请求与响应,大都是被解析成字符串。
简介
1、什么是Validation?
Validation 就是对属性的值进行校验。--【谁的属性?JavaBean的!】
例如,User的属性age,我的预期是[0, 100],其他所有值都是非法的,那就校验一下好了。
Spring提供了Validator
接口,能在应用的任意layer使用,来完成Validation的工作。
2、什么是数据绑定?
数据绑定就是将数据绑定到属性上!--【谁的属性?JavaBean的!】
两种方法,通过对象调用SETTER,或者,通过反射调用对象的SETTER,二者都是给属性设值。--谁的对象?JavaBean的!
例如,user.setAge(20)。或者通过反射 -- 在各种框架中常用。
Spring提供了DataBinder
来做具体的Data binding工作。
注意:Validator
和DataBinder
构成了validation
包。
3、什么是类型转换?
将一个类型的对象转成另一个类型的对象,例如String和Date互转等。 --【谁的类型?属性的!】
Spring提供了PropertyEditor
以及core.convert 包和format 包。后两者是Spring 3 引入的,可以看作PropertyEditor
的替代品,更简单。
注意:PropertyEditor
概念是 JavaBeans specification 的一部分。
BeanWrapper
,这是一个接口,Spring还提供了一个实现BeanWrapperImpl
。但是,这个东西是很底层的概念,用户一般不必直接使用它,了解即可。DataBinder
和底层 BeanWrapper
都是使用 PropertyEditor
s 来解析和格式化属性值。PropertyEditor
的替代品,更简单。稍后谈。 正文
1、使用Validator接口进行Validation
Validator
接口进行校验,该接口只有两个方法,support(..)用于判断是否支持某类型,validate(..)则进行校验。Validator
接口形参是一个Errors
对象,当校验时,可以将失败结果报告给该对象。public class Person { private String name;
private int age; // the usual getters and setters...略
}
public class PersonValidator implements Validator { public boolean supports(Class clazz) {
return Person.class.equals(clazz); // 仅支持Person类
} public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) { // 年龄不能小于0
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) { // 年龄不能大于110
e.rejectValue("age", "too.darn.old");
}
}
}
上面的代码一目了然,实现Validator
接口,完成两个方法即可。
如果是复合类(包含其他类字段),还可以注入相应的Validator -- 复用,高效。
2、错误码与错误消息(暂略)
MessageCodesResolver
DefaultMessageCodesResolver
3、Bean操作 和 BeanWrapper
BeanWrapper
接口和其实现 BeanWrapperImpl
。BeanWrapper
就是封装一个bean,对其进行操作,例如set/get property。支持嵌套属性等特点,详见 table 9.1 。BeanWrapper
支持添加标准JavaBean PropertyChangeListeners
and VetoableChangeListeners
的能力,无须在目标类中编码。(监听器)DataBinder
and the BeanFactory
中。3.1、set/get属性
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.2、内建的PropertyEditor实现
Object
and a String
之间的转换。-- 注意,是对象和字符串之间的转换,不是任意类型间的转换。Date
可以用人类可读的方式来描述。这可以通过注册自定义的java.beans.PropertyEditor类型的editor来实现(--卧槽,刚意识到这不是Spring的东西),嗯,注册在BeanWrapper
上或者IoC容器中。详见Javadoc。PropertyEditor
的例子:ClassEditor
来转换成相应的类。PropertyEditors
来完成的。 PropertyEditors
,它们都在org.springframework.beans.propertyeditors 包中。它们的多数,默认已经被BeanWrapperImpl注册了。当然,你也可以注册自己的变体来覆盖默认的:Table 9.2. Built-in PropertyEditors
Class | Explanation |
---|---|
|
Editor for byte arrays. Strings will simply be converted to their corresponding byte representations. Registered by default by |
|
Parses Strings representing classes to actual classes and the other way around. When a class is not found, an |
|
Customizable property editor for |
|
Property editor for Collections, converting any source |
|
Customizable property editor for java.util.Date, supporting a custom DateFormat. NOT registered by default. Must be user registered as needed with appropriate format.默认没注册! |
|
Customizable property editor for any Number subclass like |
|
Capable of resolving Strings to |
|
One-way property editor, capable of taking a text string and producing (via an intermediate |
|
Capable of resolving Strings to |
|
Capable of resolving Strings to |
|
Capable of converting Strings (formatted using the format as defined in the javadocs of the |
|
Property editor that trims Strings. Optionally allows transforming an empty string into a |
|
Capable of resolving a String representation of a URL to an actual |
Font
、Color
以及大多数基本类型的PropertyEditor
的实现。PropertyEditor
类(不需要显式的注册它们)--如果它们和它们处理的类在相同包下,且其名字以被其处理类的名字加上“Editor”的话。com
chank
pop
Foo
FooEditor // the PropertyEditor for the Foo class
注意,② 还可以使用标准BeanInfo
JavaBean机制。下例就使用了该机制来显式注册相关类属性的一个或多个PropertyEditor
实例。(使用已有的editor)
com
chank
pop
Foo
FooBeanInfo // the BeanInfo for the Foo class
public class FooBeanInfo extends SimpleBeanInfo { public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
};
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
3.2.1、注册额外定制的PropertyEditor
BeanFactory
实现中使用,但更建议在ApplicationContext中使用。BeanWrapper
来处理property conversions。(前面有提到,BeanWrapperImpl
会自动注册一些内建的editors)。此外,具体的ApplicationContexts
还会覆盖或者添加额外的editors。(这句很重要)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
--注意,这里的value是String,后台editor会将其转成 ExoticType类型。
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
该editor实现大概类似这样:
// 将String转成ExoticType对象
package example; public class ExoticTypeEditor extends PropertyEditorSupport { public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
然后,就到了最后一步,也是这里的主题,使用CustomEditorConfigurer
将新的PropertyEditor
注册到ApplicationContext。
如下:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
③ 使用PropertyEditorRegistrars,需要手动创建它。在复用时很有用,因为它打包了一组editor,拿来即用。(听起来,是类似map或者set之类的集合??)
提醒,下面这段话可能比较绕,建议略过,直接看代码,简洁明了。
PropertyEditorRegistrars
配合接口 PropertyEditorRegistry 使用。这个接口被BeanWrapper(还有DataBinder)实现了。PropertyEditorRegistrars
配合CustomEditorConfigurer
使用时特别方便,后者有一个方法setPropertyEditorRegistrars(..),以这种方式添加到CustomEditorConfigurer
中的PropertyEditorRegistrars
可以轻松的共享给DataBinder
和Spring MVC Controllers。 Furthermore, it avoids the need for synchronization on custom editors:每个bean创建时,PropertyEditorRegistrar
都会创建新的PropertyEditor
实例。PropertyEditorRegistrar
实现:package com.foo.editors.spring; public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { public void registerCustomEditors(PropertyEditorRegistry registry) { // 需要PropertyEditor实例
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); // 可以注册任意多的PropertyEditor...
}
}
插一句,org.springframework.beans.support.ResourceEditorRegistrar 也是一个实现,可以参考下。
CustomEditorConfigurer
,注入我们的CustomPropertyEditorRegistrar
:<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean> <bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最后,在使用Spring MVC框架时,使用PropertyEditorRegistrars
配合data-binding Controllers(如SimpleFormController)会是非常方便的(--暂时不明白,以后再来看吧)。见下例:
public final class RegisterUserController extends SimpleFormController { private final PropertyEditorRegistrar customPropertyEditorRegistrar; public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
} protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
} // other methods to do with registering a User
}
这种风格的PropertyEditor
注册能简洁代码(the implementation of initBinder(..)
is just one line long!),且允许通用的PropertyEditor
注册代码包含在一个类中--然后由所有需要的Controllers
共享。
4、Spring 类型转换
4.1、Converter SPI
package org.springframework.core.convert.converter; public interface Converter<S, T> { T convert(S source); }
想要创建自己的converter,实现这个接口即可。
DefaultConversionService
默认已注册了)。StringToInteger
:package org.springframework.core.convert.support; final class StringToInteger implements Converter<String, Integer> { public Integer convert(String source) {
return Integer.valueOf(source);
} }
4.2、ConverterFactory
package org.springframework.core.convert.converter; public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); }
例子:
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());
}
}
}
4.3、GenericConverter(略)
4.4、ConditionalGenericConverter(略)
4.5、ConversionService API 转换服务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); }
从上面的代码可以推理,该接口的实现必定是先判断能否转换,如能转换再调用相应的converter进行转换。--问题,从哪调用converter?ConverterRegistry!
所以多数 ConversionService的实现 也实现了 ConverterRegistry,它提供了一个 SPI 以注册 converters 。在内部,一个ConversionService的实现委托注册的converters来执行类型转换逻辑。
GenericConversionService
是一个通用的实现,适用于多数环境。ConversionServiceFactory
提供了一个便捷的工厂以创建常见的ConversionService配置。4.6、Configuring a ConversionService 配置
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>
注意,ConversionServiceFactoryBean的Javadoc说返回的conversionService是DefaultConversionService 实例。
默认的ConversionService可以在strings、numbers、enums、collections、maps以及其他常用类型之间进行转换。
converters
属性。其值可以是Converter、ConverterFactory或者GenericConverter的实现。<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>
个人心得:
①、非web项目的Spring应用,不会自动注册ConversionService bean。(web项目暂时没测)
②、@Configuration class 中进行ConversionService bean定义时,有几个选择:使用其实现,如GenericConversionService、DefaultConversionService 或者其他;使用ConversionServiceFactoryBean。区别在于,GenericConversionService 没有注册converters,DefaultConversionService 注册了很多converters,ConversionServiceFactoryBean 则提供了DefaultConversionService。-- 所以,一般情况下,直接使用ConversionServiceFactoryBean 即可。如下:
/**
* This implementation creates a DefaultConversionService.
* Subclasses may override createConversionService() in order to return a GenericConversionService instance of their choosing.
*
* 这个可以取代上面的DefaultConversionService,基本一致。
*
* @return
*/
@Bean
public ConversionServiceFactoryBean conversionService(){
return new ConversionServiceFactoryBean();
}
另,
FormattingConversionServiceFactoryBean
.4.7、编码使用ConversionService
@Service
public class MyService { @Autowired
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
} public void doIt() {
this.conversionService.convert(...)
}
}
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
自动注册的converters,可以适用于大多数环境。其中包含了collection converters、scalar converters,以及基本的Object到String的converter。同样的converters也可以使用DefaultConversionService
的static addDefaultConverters
注册到任意ConverterRegistry
中。5、Spring Field Formatting (Spring字段格式化)
core.convert
是一个通用目的的类型转换系统。提供了统一的ConversionService API和强类型的Converter SPI,以实现转换逻辑。Spring容器使用该系统来绑定bean property values。Short
to a Long
来完成expression.setValue(Object bean, Object value)尝试时,core.convert系统负责执行该强制。5.1、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;
}
format
包的子包中提供了几个Formatter的实现。number,datetime,datetime.joda。DateFormatter
。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;
} }
5.2、注解驱动的Formatting
5.2.1、Format Annotation API
public class MyModel { @DateTimeFormat(iso=ISO.DATE)
private Date date; }
5.3、FormatterRegistry SPI
FormattingConversionService
适用于多数环境。5.4、FormatterRegistrar SPI
6、配置全局 date & time 格式
DateFormatterRegistrar
-- 这取决于你是否使用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;
}
}
WebMvcConfigurationSupport
并重写mvcConversionService()。对于XML来说,应该使用<mvc:annotation-driven>元素的'conversion-service'属性。See Section 22.16.3, “Conversion and Formatting” for details.7、Spring Validation
7.1、JSR-303 Bean Validation API概览
public class PersonForm { @NotNull
@Size(max=64)
private String name; @Min(0)
private int age; }
7.2、配置一个Bean Validation Provider
javax.validation.ValidatorFactory
or javax.validation.Validator
。LocalValidatorFactoryBean
来配置默认的Validator:<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
上面,会引动Bean Validation使用其自身的启动机制来初始化。如果在classpath中有JSR-303/JSR-349 provider,如Hibernate Validator,会被自动探测到。
7.2.1、注入一个Validator
LocalValidatorFactoryBean
implements both javax.validation.ValidatorFactory
and javax.validation.Validator
, as well as Spring’sorg.springframework.validation.Validator
. javax.validation.Validator
:import javax.validation.Validator; @Service
public class MyService { @Autowired
private Validator validator; }
②如果倾向于使用Spring Validation API,可以注入org.springframework.validation.Validator:
import org.springframework.validation.Validator; @Service
public class MyService { @Autowired
private Validator validator; }
7.2.2、配置自定义限制(Constraints)
LocalValidatorFactoryBean
配置了一个SpringConstraintValidatorFactory
,其使用Spring来创建ConstraintValidator实例。@Constraint
声明,紧接着相关联的ConstraintValidator
实现--使用Spring来依赖注入:@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
import javax.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator { @Autowired;
private Foo aDependency; ...
}
7.2.3、Spring-driven Method Validation
MethodValidationPostProcessor
bean定义,它可以被集成到Spring context中。<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
为了支持Spring-driven method validation,所有的目标类都需要使用Spring的@Validated注解,可以选择声明校验组。详见MethodValidationPostProcessor的javadoc。
7.2.4、更多配置选项(略)
7.3、配置一个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();