接前一篇 Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
本篇主要内容:Spring Type Conversion(ConversionService)、Spring Field Formatting、globle date & time format、Spring Validation。
本篇上承自前一篇,建议先看前一篇。
4、Spring Type Conversion (Spring类型转换)
上一篇有提到,Spring早期使用PropertyEditor进行Object与String的转换。
Spring 3 引入了core.convert 包,提供了通用的类型转换系统。该系统定义了一个SPI -- 实现了类型转换逻辑,还定义了一个API -- 用于在runtime执行类型转换。在Spring容器内,该系统可以用作PropertyEditors 的替代品来转换外部bean property value strings 到需要的property 类型。
插一句,SPI 是 Service Provider Interface,与API的区别见 difference-between-spi-and-api 。
4.1 Converter SPI
接口很简单:
1 package org.springframework.core.convert.converter; 2 3 public interface Converter<S, T> { 4 5 T convert(S source); 6 7 }
想要创建自己的converter,实现这个接口即可。
需要注意集合/数组的转换需要额外的支持。一般情况下不必考虑这个,了解即可。
Spring在core.convert.support 包中以提供了几个converter实现,包括从String到Number以及其他常见类型的转换。
来看一下 StringToInteger:
1 package org.springframework.core.convert.support; 2 3 final class StringToInteger implements Converter<String, Integer> { 4 5 public Integer convert(String source) { 6 return Integer.valueOf(source); 7 } 8 9 }
4.2 ConverterFactory
当你需要从类层次上的转换逻辑时,例如,从String转成java.lang.Enum对象时,实现ConverterFactory即可:
1 package org.springframework.core.convert.converter; 2 3 public interface ConverterFactory<S, R> { 4 5 <T extends R> Converter<S, T> getConverter(Class<T> targetType); 6 7 }
例子:
1 package org.springframework.core.convert.support; 2 3 final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { 4 5 public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { 6 return new StringToEnumConverter(targetType); 7 } 8 9 private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> { 10 11 private Class<T> enumType; 12 13 public StringToEnumConverter(Class<T> enumType) { 14 this.enumType = enumType; 15 } 16 17 public T convert(String source) { 18 return (T) Enum.valueOf(this.enumType, source.trim()); 19 } 20 } 21 }
4.3、GenericConverter(略)
4.4、ConditionalGenericConverter(略)
4.5、ConversionService API 转换服务API
ConversionService定义了一个统一的API以执行类型转换逻辑,是一个facade(门面)接口。
1 package org.springframework.core.convert; 2 3 public interface ConversionService { 4 // 判断,能否将源类型转成目标类型 5 boolean canConvert(Class<?> sourceType, Class<?> targetType); 6 // 转换 7 <T> T convert(Object source, Class<T> targetType); 8 // 判断 9 boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); 10 // 转换 11 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); 12 13 }
从上面的代码可以推理,该接口的实现必定是先判断能否转换,如能转换再调用相应的converter进行转换。--问题,从哪里调用converter?ConverterRegistry!
所以多数 ConversionService的实现类 也实现了 ConverterRegistry,因为它提供了一个 SPI 以注册 converters 。在内部,一个ConversionService的实现委托注册的converters来执行类型转换逻辑。
重点:Spring在core.convert.support 包中提供了一个健全的实现。GenericConversionService 是一个通用的实现,适用于多数环境。ConversionServiceFactory 提供了一个便捷的工厂以创建常见的ConversionService配置。
4.6、Configuring a ConversionService (配置)
一个ConversionService是一个 stateless 的对象,被设计成在应用启动时被实例化,然后在多个线程之间共享。(PropertyEditor则是线程不安全的)
在一个Spring应用中,可以给每个容器配置一个ConversionService。Spring会加载并调用它。你也可以注入需要的地方,直接调用它。
注意:如果没有ConversionService被注册,会使用原始的基于PropertyEditor的系统。
注册一个默认的ConversionService(id不可修改):
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>
注意,ConversionServiceFactoryBean的Javadoc说 返回的conversionService是DefaultConversionService 实例。
DefaultConversionService 可以在strings、numbers、enums、collections、maps以及其他常用类型之间进行转换。
要补充或者覆盖默认的转换器,需要设置converters 属性。其值可以是Converter、ConverterFactory或者GenericConverter的实现。
1 <bean id="conversionService" 2 class="org.springframework.context.support.ConversionServiceFactoryBean"> 3 <property name="converters"> 4 <set> 5 <bean class="example.MyCustomConverter"/> 6 </set> 7 </property> 8 </bean>
个人经验:
1、非web项目的Spring应用,不会自动注册ConversionService bean,就是说默认基于PropertyEditor。(web项目暂时没测)
2、@Configuration class 中进行ConversionService bean定义时,有几个选择:使用其实现类,如GenericConversionService、DefaultConversionService 或者其他;也可以,使用ConversionServiceFactoryBean。区别在于,GenericConversionService 默认没有注册converters,DefaultConversionService 注册了很多converters,ConversionServiceFactoryBean 则提供了DefaultConversionService。
-- 所以,一般情况下,直接使用ConversionServiceFactoryBean 即可。
针对第二点,代码如下:
package cn.larry.config; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.support.ConversionServiceFactoryBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.validation.DataBinder; /** * @author LarryZeal * */ //@Profile("conversionService") @Configuration public class ConversionServiceConfig { /* * @Autowired * public ConversionService conversionService; // spring boot 默认木有这个,所以无法注入啊。 * * @PostConstruct * public void run(){ * Integer convert = conversionService.convert("123", int.class); * System.out.println(convert); * } */ // GenericConversionService 没有注册converters,需要手动注册。 /* @Bean public ConversionService conversionService() { GenericConversionService conversionService = new GenericConversionService(); boolean canConvert = conversionService.canConvert(String.class, int.class); System.out.println("------------"+canConvert+"----------"); //GenericConversionService默认没有convert Integer convert = conversionService.convert("123", int.class); System.out.println(convert); return conversionService; }*/ // DefaultConversionService 才默认注册了converters。 /* @Bean public ConversionService conversionService1() { DefaultConversionService conversionService = new DefaultConversionService(); boolean canConvert = conversionService.canConvert(String.class, int.class); System.out.println("------------"+canConvert+"----------"); Integer convert = conversionService.convert("123", int.class); System.out.println(convert); return conversionService; }*/ /** * 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(); } private DataBinder dataBinder; }
另,
1,通常也可以在Spring MVC应用中使用ConversionService。待后续。See Section 22.16.3, “Conversion and Formatting” in the Spring MVC chapter.
2,在特定环境下,可能希望在转换时应用格式。See Section 9.6.3, “FormatterRegistry SPI” for details on using FormattingConversionServiceFactoryBean.
4.7、编码使用ConversionService
bean定义后注入即可使用,上面代码已展示了如何定义bean。
1 @Service 2 public class MyService { 3 4 @Autowired 5 public MyService(ConversionService conversionService) { 6 this.conversionService = conversionService; 7 } 8 9 public void doIt() { 10 this.conversionService.convert(...) 11 } 12 }
多数情况下不能用于复合类型,如集合。例如,如果需要编码转换List<Integer>到List<String>,需要提供源类型和目标类型的正式定义。
幸运的是,TypeDescriptor 提供了多种选项,使其变得简单直接:
1 DefaultConversionService cs = new DefaultConversionService(); 2 3 List<Integer> input = .... 4 cs.convert(input, 5 TypeDescriptor.forObject(input), // List<Integer> type descriptor 6 TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
再次提醒,DefaultConversionService 自动注册的converters,可用于大多数环境。其中包含了collection converters、scalar converters,以及基本的Object到String的converter。
另,同样的converters也可以使用DefaultConversionService 的static addDefaultConverters 注册到任意ConverterRegistry 中。
值类型的转换器,会被自动复用到值类型的array和collection中,不必再去创建。 -- 值类型这个概念,应该就是基本类型吧。
5、Spring Field Formatting (Spring字段格式化)
上面有提到, core.convert 是一个通用目的的类型转换系统。提供了统一的ConversionService API和强类型的Converter SPI,以实现转换逻辑。Spring容器使用该系统来绑定bean property values。
此外,SpEL 和 DataBinder 使用该系统绑定 field values。例如,当SpEL需要强制 a Short to a Long来完成expression.setValue(Object bean, Object value)尝试时,core.convert系统负责执行该强制。
---------------------------------上面是回顾,现在开始新内容--------------------------------
但是,除了格式转换,你还经常需要本地化String values -- 就是以当地格式展示,如货币、日期等。通用的core.convert Converter SPI不能直接完成格式化需求。
基于此,Spring 3 引入了 Formatter SPI,相比PropertyEditors简单直接。
ConversionService 为Converter SPI和Formatter SPI提供了统一的 API。
个人体会:一个是粗粒度的,一个是细粒度的。
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; }
要创建自己的Formatter,实现Formatter接口即可。注意异常和线程安全。
format 包的子包中提供了几个Formatter的实现类。number,datetime,datetime.joda。参考DateFormatter :
1 package org.springframework.format.datetime; 2 3 public final class DateFormatter implements Formatter<Date> { 4 5 private String pattern; 6 7 public DateFormatter(String pattern) { 8 this.pattern = pattern; 9 } 10 11 public String print(Date date, Locale locale) { 12 if (date == null) { 13 return ""; 14 } 15 return getDateFormat(locale).format(date); 16 } 17 18 public Date parse(String formatted, Locale locale) throws ParseException { 19 if (formatted.length() == 0) { 20 return null; 21 } 22 return getDateFormat(locale).parse(formatted); 23 } 24 25 protected DateFormat getDateFormat(Locale locale) { 26 DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale); 27 dateFormat.setLenient(false); 28 return dateFormat; 29 } 30 31 }
5.2、注解驱动的Formatting
绑定一个注解到一个formatter,实现 AnnotationFormatterFactory 即可。详见这里。略。
5.2.1、Format Annotation API
便携的格式化注解API存在于org.springframework.format.annotation包中。
Use @NumberFormat to format java.lang.Number fields. Use @DateTimeFormat to format java.util.Date, java.util.Calendar, java.util.Long, or Joda Time fields.
例子:
public class MyModel { @DateTimeFormat(iso=ISO.DATE) private Date date; }
5.3、FormatterRegistry SPI
这是一个SPI,用于注册formatters和converters。其实现FormattingConversionService 适用于多数环境。
该实现可以编码式配置或者声明式配置,通过FormattingConversionServiceFactoryBean。
另外,其还实现了ConversionService,所以可以配置和DataBinder、SpEL一起使用。
5.4、FormatterRegistrar SPI
通过FormatterRegistry注册formatters和converters。 -- 蛋疼,两个词分不清。稍后研究下。
6、配置全局 date & time 格式
默认,如果没有使用注解@DateTimeFormat,会使用 DateFormat.SHORT 风格。当然,你可以定义自己的全局格式。
需要确保Spring没有注册默认的formatters,然后手动注册所有的formatters。使用 DateFormat.SHORT 或 DateFormatterRegistrar -- 这取决于你是否使用Joda Time 库。
例如,
1 @Configuration 2 public class AppConfig { 3 4 @Bean 5 public FormattingConversionService conversionService() { 6 7 // Use the DefaultFormattingConversionService but do not register defaults 8 DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); 9 10 // Ensure @NumberFormat is still supported 11 conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); 12 13 // Register date conversion with a specific global format 14 DateFormatterRegistrar registrar = new DateFormatterRegistrar(); 15 registrar.setFormatter(new DateFormatter("yyyyMMdd")); 16 registrar.registerFormatters(conversionService); 17 18 return conversionService; 19 } 20 }
注意,如果使用Spring MVC,记住,需要显式的配置使用conversion service。对于基于Java的@Configuration来说,这意味着继承WebMvcConfigurationSupport
并重写mvcConversionService()。对于XML来说,应该使用<mvc:annotation-driven>元素的'conversion-service'属性。See Section 22.16.3, “Conversion and Formatting” for details.
7、Spring Validation
Spring 3 引入了validation support的几个增强。
第一,完整的支持JSR-303 Bean Validation API。
第二,编码式使用时,Spring的DataBinder现在可以validate objects,也可以bind to them。
第三,Spring MVC现在支持声明式校验@Controller的输入。
7.1、JSR-303 Bean Validation API概览
允许定义声明式校验限制。如:
public class PersonForm { @NotNull @Size(max=64) private String name; @Min(0) private int age; }
For general information on JSR-303/JSR-349, see the Bean Validation website. For information on the specific capabilities of the default reference implementation, see the Hibernate Validator documentation.
7.2、配置一个Bean Validation Provider
Spring提供了对Bean Validation API的完整的支持。包括启动其实现(provider),并将其作为Spring的bean。
所以,需要validation时,可以注入 javax.validation.ValidatorFactory 或者 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’s org.springframework.validation.Validator
.
可以注入这几个接口的任意一个引用来使用。
①如果倾向于使用Bean Validation API,直接注入javax.validation.Validator
:
1 import javax.validation.Validator; 2 3 @Service 4 public class MyService { 5 6 @Autowired 7 private Validator validator; 8 9 }
②如果倾向于使用Spring Validation API,可以注入org.springframework.validation.Validator:
1 import org.springframework.validation.Validator; 2 3 @Service 4 public class MyService { 5 6 @Autowired 7 private Validator validator; 8 9 }
7.2.2、配置自定义限制(Constraints)
每个Bean Validation constraint都由两部分组成。首先,一个@Constraint注解,声明了the constraint和其可配置的properties。其次,javax.validation.ConstraintValidator接口的实现,实现了the constraint's behavior。为了将声明与实现关联起来,每个@Constraint注解引用了一个相关的ValidationConstraint实现类。在运行时,当一个ConstraintValidatorFactory在你的domain model中遇到constraint 注解时会实例化被引用的实现。
默认的,LocalValidatorFactoryBean 配置了一个SpringConstraintValidatorFactory ,其使用Spring来创建ConstraintValidator实例。
下面是一个例子,一个定制的 @Constraint 声明,紧接着相关联的ConstraintValidator 实现--使用Spring来依赖注入:
1 @Target({ElementType.METHOD, ElementType.FIELD}) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Constraint(validatedBy=MyConstraintValidator.class) 4 public @interface MyConstraint { 5 }
1 import javax.validation.ConstraintValidator; 2 3 public class MyConstraintValidator implements ConstraintValidator { 4 5 @Autowired; 6 private Foo aDependency; 7 8 ... 9 }
As you can see, a ConstraintValidator implementation may have its dependencies @Autowired like any other Spring bean.
7.2.3、Spring-driven Method Validation
方法验证,由Bean Validation 1.1 支持,作为一个自定义的扩展同样也被Hibernate Validator 4.3支持,通过一个 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
从Spring 3开始,可以通过一个Validator来配置一个DataBinder实例。一旦配置了,就可以通过调用binder.validate()来调用Validator。所有校验Errors会自动添加到binder的BindingResult。
当编码式使用DataBinder时,这样可以用于在绑定后调用校验逻辑:
1 Foo target = new Foo(); 2 DataBinder binder = new DataBinder(target); 3 binder.setValidator(new FooValidator()); 4 5 // bind to the target object 6 binder.bind(propertyValues); 7 8 // validate the target object 9 binder.validate(); 10 11 // get BindingResult that includes any validation errors 12 BindingResult results = binder.getBindingResult();
一个DataBinder也可以配置多个Validator实例 -- 通过dataBinder.addValidators和dataBinder.replaceValidators。