6.6Spring3 字段格式化
正如在前面的小节中说的那样,core.convert是一个通用的类型转换系统。它提供了一个统一的ConversionServiceAPI和一个强类型的Converter SPI来实现类型转换逻辑。Spring容器使用这个系统来绑定bean的属性值。另外,SpEL和DataBinder也都利用这个系统来绑定字段值。比如,当SpEL需要将一个Short强转成Long来完成expression.setValue(Objectbean, Object value)的操作时,将会由core.convert系统来执行这次强转。
现在来考虑一个典型的客户环境(比如一个web或是桌面应用)的类型转换需求。在这样的环境中,你需要将String转换成其他类型来支持客户回传过程(译者注:也就是用户传值给服务器的过程),另外你也需要将它再转成String来支持页面渲染过程(译者注:也就是服务器将值传给用户的过程)。另外,通常你需要将String值本地化。更为通用的core.convert的Converter SPI并不直接支持这样的格式化需求。所以为了直接支持它,Spring3提出了一个方便的FormatterSPI,它作为PropertyEditors的替代,为客户环境提供了一个简单而健壮的实现。
总的来说,当你需要实现通用的类型转换逻辑时使用Converter SPI,比如java.utils.Date和java.lang.Long类型之间互相转换时。而当你处于客户端环境时,比如在web应用中,需要将字段值解析并以本地化格式输出时使用FormatterSPI。ConversionService为两种SPI提供了一个统一的类型转换API。
FormatterSPI
实现了字段格式化逻辑的Formatter SPI简单且是强类型的:
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter继承了Printer和Parser接口这两个building-block(组成部分,很有意思的单词)
public interface Printer<T> {
String print(T object, 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包提供了一个NumberFormatter,CurrencyFormatter和PercentFormatter,它们使用java.text.NumberFormat来格式化java.lang.Number对象。datetime包提供了一个DateFormatter,它使用java.text.DateFormat来格式化java.util.Date对象。datetime.joda包提供了一个基于Joda Time库的全面的datetime格式化的支持。
考虑一个Formatter实现类的例子——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;
}
}
Spring团队欢迎来自社区的对于Formatter的贡献。可以通过http://jira.springframework.org来投稿。
注解驱动的格式化
一会你将会看到的,字段格式化可以通过字段类型或注解配置。为了绑定一个注解和一个格式化,实现AnnotationFormatterFactory(译者注:将A和这个由A而使这个Factory产生的Printer和Parser关联起来):
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);
}
参数A是要关联格式化逻辑的字段的注解类型,比如org.springframework.format.annotation.DateTimeFormat。getFieldTypes()方法来返回可能会被使用注解的字段类型。getPrinter()方法用来返回一个Printer来输出被注解的字段的值。而getParser()方法用来返回一个Parser来将一个被注解的字段解析成客户端要显示的值。
下面的AnnotationFormatterFactory将@NumberFormat注解绑定到了一个formatter。这个注解允许定义一个数字风格(style,译者注:比如CURRENCY、PERCENT)或是一个数字格式(pattern,译者注:比如#,###):
public final class NumberFormatAnnotationFormatterFactory implements
AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(newClass<?>[] {
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 NumberFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyFormatter();
} else {
return new NumberFormatter();
}
}
}
}
为了触发格式化,只需要在字段上添加@NumberFormat注解即可:
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
格式化注解API
org.springframework.format.annocation包中包含了一个便捷的格式化注解API。使用@NumberFormat来格式化java.lang.Number字段。使用@DateTimeFormat来格式化java.util.Date,java.util.Calendar,java.util.Long或Joda Time字段。
下面的例子使用@DateTimeFormat将java.util.Date格式化为ISO日期类型(yyyy-MM-dd):
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
FormatterRegistry SPI
FormatterRegistry是一个注册formatters和converters的SPI。FormattingConversionService是FormatterRegistry的一个实现类,并且适用于大多数环境。这个实现类可以使用FormattingConversionServiceFactoryBean来编程式或声明式的配置成为一个Spring的Bean。由于这个实现也实现了ConversionService,所以它可以直接配置并和Spring的DataBinder和SpEL一起使用。
下面看一下FormatterRegistry SPI:
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Formatter<?> formatter);
void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}
正如上面所示,Formatters可以通过字段类型或者注解来注册。
FormatterRegistry SPI允许你集中配置格式化规则,而不是在各个Controller中重复这样的配置。比如,你可能想要将所有的Date字段都用一定的方式来格式化,或者是具有某一注解的字段都也用一定的方式来格式化。通过一个共享的FormatterRegistry,一旦你定义了规则,那么它们将随时都可以应用到格式化中。
FormatterRegistrar SPI
FormatterRegistrar是一个SPI,它用来通过FormatterRegistry来注册formatters和converters。
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
当需要为指定的格式化类别注册多个相关的converters和fomatters,比如Date类型的格式化时,FormatterRegistrar非常有用。
在Spring MVC中配置Formatting
在Spring MVC应用中,你可以精确地配置一个自定义的ConversionService实例并将它作为一个MVC命名空间中annotation-driven元素的属性。这个ConversionService可以在Controller的数据绑定时任何需要类型转换的地方使用。如果没有精确地配置ConversionService,那么SpringMVC将会自动注册一个默认的formatters和converters,可以转换一些常见的类型,比如说numbers和dates(译者注:这里也是需要配置上<mvc:annotation-driven>的)。
为了依赖默认的格式化规则,在你的Spring MVC配置XML文件中不要加上额外的配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc" 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-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<mvc:annotation-driven />
</beans>
通过这么一行的配置,为Numbers和Date类型格式化的默认formatters就被注册好了。包括对@NumberFormat和@DateTimeFormat注解的支持。如果classpath下有Joda Time的jar包的话,那么还能提供对Joda Time格式化库的支持。
为了注入一个包含自定义formatters和converters的ConversionSerivce实例,我们需要设置conversion-service属性(mvc命名空间的annotation-driven中),然后在conversionService实例中指定自定义的converters,formatters或者FormatterRegistrars作为FormattingConversionServiceFactoryBean的属性:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc" 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-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<mvc:annotation-driven conversion-service="conversionService" />
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter" />
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter" />
<bean class="org.example.MyAnnotationFormatterFactory" />
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar" />
</set>
</property>
</bean>
</beans>
注意:当你要使用FormatterRegistrars时,可以阅读“FormatterRegistrarSPI”这一节得到更多关于FormattingConversionServiceFactoryBean的信息。