5.6 Spring字段格式化

如上一节所述,core.convert包是一个通用类型转换系统,它提供了统一的ConversionService API以及强类型的Converter SPI用于实现将一种类型转换成另外一种的转换逻辑。Spring容器使用这个系统来绑定bean属性值,此外,Spring表达式语言(SpEL)和DataBinder也都使用这个系统来绑定字段值。举个例子,当SpEL需要将Short强制转换成Long来完成一次expression.setValue(Object bean, Object value)尝试时,core.convert系统就会执行这个强制转换。

现在让我们考虑一个典型的客户端环境如web或桌面应用程序的类型转换要求,在这样的环境里,你通常会经历将字符串进行转换以支持客户端回传的过程以及转换回字符串以支持视图渲染的过程。此外,你经常需要对字符串值进行本地化。更通用的core.convert包中的Converter SPI不直接解决这种格式化要求。Spring 3为此引入了一个方便的Formatter SPI来直接解决这些问题,这个接口为客户端环境提供一种简单强大并且替代PropertyEditor的方案。

一般来说,当你需要实现通用的类型转换逻辑时请使用Converter SPI,例如,在java.util.Date和java.lang.Long之间进行转换。当你在一个客户端环境(比如web应用程序)工作并且需要解析和打印本地化的字段值时,请使用Formatter SPI。ConversionService接口为这两者提供了一套统一的类型转换API。

5.6.1 Formatter SPI

Formatter SPI实现字段格式化逻辑是简单并且强类型的:

package org.springframework.format; 

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

Formatter接口扩展了Printer和Parser这两个基础接口:

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接口。泛型参数T代表你想要格式化的对象的类型,例如,java.util.Date。实现print()操作可以将类型T的实例按客户端区域设置的显示方式打印出来。实现parse()操作可以从依据客户端区域设置返回的格式化表示中解析出类型T的实例。如果解析尝试失败,你的格式化器应该抛出一个ParseException或者IllegalArgumentException。请注意确保你的格式化器实现是线程安全的。

为方便起见,format子包中已经提供了一些格式化器实现。number包提供了NumberFormatter、CurrencyFormatter和PercentFormatter,它们通过使用java.text.NumberFormat来格式化java.lang.Number对象 。datetime包提供了DateFormatter,其通过使用java.text.DateFormat来格式化java.util.Date。datetime.joda包基于Joda Time library提供了全面的日期时间格式化支持。

考虑将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贡献,可以登陆网站jira.spring.io了解如何参与贡献。

5.6.2 注解驱动的格式化

如你所见,字段格式化可以通过字段类型或者注解进行配置,要将一个注解绑定到一个格式化器,可以实现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); 

} 

泛型参数A代表你想要关联格式化逻辑的字段注解类型,例如org.springframework.format.annotation.DateTimeFormat。让getFieldTypes()方法返回可能使用注解的字段类型,让getPrinter()方法返回一个可以打印被注解字段的值的打印机(Printer),让getParser()方法返回一个可以解析被注解字段的客户端值的解析器(Parser)。

下面这个AnnotationFormatterFactory实现的示例把@NumberFormat注解绑定到一个格式化器,此注解允许指定数字样式或模式:

public final class NumberFormatAnnotationFormatterFactory 
        implements AnnotationFormatterFactory<NumberFormat> { 

    public Set<Class<?>> getFieldTypes() { 
        return new HashSet<Class<?>>(asList(new Class<?>[] { 
            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; 

} 

Format Annotation API

org.springframework.format.annotation包中存在一套可移植(portable)的格式化注解API。请使用@NumberFormat格式化java.lang.Number字段,使用@DateTimeFormat格式化java.util.Date、java.util.Calendar、*java.util.Long(注:此处可能是原文错误,应为java.lang.Long)*或者Joda Time字段。

下面这个例子使用@DateTimeFormat将java.util.Date格式化为ISO时间(yyyy-MM-dd)

public class MyModel { 

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

} 

5.6.3 FormatterRegistry SPI

FormatterRegistry是一个用于注册格式化器和转换器的服务提供接口(SPI)。FormattingConversionService是一个适用于大多数环境的FormatterRegistry实现,可以以编程方式或利用FormattingConversionServiceFactoryBean声明成Spring bean的方式来进行配置。由于它也实现了ConversionService,所以可以直接配置它与Spring的DataBinder以及Spring表达式语言(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); 

} 

如上所示,格式化器可以通过字段类型或者注解进行注册。

FormatterRegistry SPI允许你集中地配置格式化规则,而不是在你的控制器之间重复这样的配置。例如,你可能要强制所有的时间字段以某种方式被格式化,或者是带有特定注解的字段以某种方式被格式化。通过一个共享的FormatterRegistry,你可以只定义这些规则一次,而在需要格式化的时候应用它们。

5.6.4 FormatterRegistrar SPI

FormatterRegistrar是一个通过FormatterRegistry注册格式化器和转换器的服务提供接口(SPI):

package org.springframework.format; 

public interface FormatterRegistrar { 

    void registerFormatters(FormatterRegistry registry); 

} 

当要为一个给定的格式化类别(比如时间格式化)注册多个关联的转换器和格式化器时,FormatterRegistrar会非常有用。

下一部分提供了更多关于转换器和格式化器注册的信息。

5.6.5 在Spring MVC中配置格式化

请查看Spring MVC章节的Section 18.16.3 “Conversion and Formatting”