Spring字段格式化
如前一节所讨论的,core.convert是一个通用的类型转换系统。它提供了一个统一的ConversionService API,以及一个强类型的Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring容器使用该系统来绑定bean属性值。此外,Spring表达式语言(SpEL)和DataBinder也使用该系统来绑定字段值。例如,当SpEL需要将Short强制转换为Long以完成expression.setValue(Object bean, Object value)操作时,core.convert系统会执行这种强制转换。
现在考虑一个典型客户端环境(如Web或桌面应用程序)的类型转换需求。在这样的环境中,通常需要将数据从String类型转换过来以支持客户端的回发过程,同时还需要将其转换回String类型以支持视图渲染过程。此外,还经常需要对String值进行本地化处理。更通用的core.convert Converter SPI并不能直接满足这些格式化需求。为了直接解决这些问题,Spring提供了便捷的Formatter SPI,它为客户端环境提供了一种简单而强大的替代方案,可以替代PropertyEditor的实现。
一般来说,当你需要实现通用类型转换逻辑时(例如,在 java.util.Date 和 Long 之间进行转换),可以使用 Converter SPI。当你在客户端环境中工作(如 Web 应用程序)并需要解析和打印本地化的字段值时,可以使用 Formatter SPI。ConversionService 为这两种 SPI 提供了一个统一的类型转换 API。
Formatter SPI
Formatter SPI用于实现字段格式化逻辑,其设计简洁且类型定义明确。以下是Formatter接口的定义:
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,请实现前面展示的 Formatter 接口。将参数 T 指定为您希望格式化的对象类型——例如 java.util.Date。实现 print() 方法,以便在客户端本地化环境中打印 T 的实例。实现 parse() 方法,从客户端本地化环境返回的格式化表示中解析 T 的实例。如果解析尝试失败,您的 Formatter 应该抛出 ParseException 或 IllegalArgumentException。请确保您的 Formatter 实现是线程安全的。
format 子包提供了几种 Formatter 实现,以方便使用。number 包提供了 NumberStyleFormatter、CurrencyStyleFormatter 和 PercentStyleFormatter,用于格式化使用 java.text.NumberFormat 的 Number 对象。datetime 包提供了一个 DateFormatter,用于使用 java.text.DateFormat 格式化 java.util.Date 对象;同时还有一个 DurationFormatter,可以按照 @DurationFormat.Style 枚举中定义的不同样式来格式化 Duration 对象(详见 Format Annotation API)。
以下是DateFormatter的一个示例实现:
- Java
- Kotlin
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;
}
}
class DateFormatter(private val pattern: String) : Formatter<Date> {
override fun print(date: Date, locale: Locale)
= getDateFormat(locale).format(date)
@Throws(ParseException::class)
override fun parse(formatted: String, locale: Locale)
= getDateFormat(locale).parse(formatted)
protected fun getDateFormat(locale: Locale): DateFormat {
val dateFormat = SimpleDateFormat(this.pattern, locale)
dateFormat.isLenient = false
return dateFormat
}
}
Spring团队欢迎社区推动的Formatter贡献。如欲贡献,请查看GitHub问题。
注解驱动的格式化
字段格式化可以根据字段类型或注释来配置。要将注释与Formatter关联起来,需要实现AnnotationFormatterFactory接口。以下代码展示了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参数化为您希望关联格式化逻辑的字段annotationType—— 例如org.springframework.format.annotation.DateTimeFormat。 -
使
getFieldTypes()返回可以应用该注解的字段类型。 -
使
getPrinter()返回一个Printer,用于打印带有注解的字段的值。 -
使
getParser()返回一个Parser,用于解析带有注解的字段的clientValue。
以下示例中的AnnotationFormatterFactory实现将@NumberFormat注解与一个格式化器(formatter)绑定起来,以便指定数字的样式或格式:
- Java
- Kotlin
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {
private static final Set<Class<?>> FIELD_TYPES = Set.of(Short.class,
Integer.class, Long.class, Float.class, Double.class,
BigDecimal.class, BigInteger.class);
public Set<Class<?>> getFieldTypes() {
return FIELD_TYPES;
}
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 NumberStyleFormatter(annotation.pattern());
}
// else
return switch(annotation.style()) {
case Style.PERCENT -> new PercentStyleFormatter();
case Style.CURRENCY -> new CurrencyStyleFormatter();
default -> new NumberStyleFormatter();
};
}
}
class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory<NumberFormat> {
override fun getFieldTypes(): Set<Class<*>> {
return setOf(Short::class.java, Int::class.java, Long::class.java, Float::class.java, Double::class.java, BigDecimal::class.java, BigInteger::class.java)
}
override fun getPrinter(annotation: NumberFormat, fieldType: Class<*>): Printer<Number> {
return configureFormatterFrom(annotation, fieldType)
}
override fun getParser(annotation: NumberFormat, fieldType: Class<*>): Parser<Number> {
return configureFormatterFrom(annotation, fieldType)
}
private fun configureFormatterFrom(annotation: NumberFormat, fieldType: Class<*>): Formatter<Number> {
return if (annotation.pattern.isNotEmpty()) {
NumberStyleFormatter(annotation.pattern)
} else {
val style = annotation.style
when {
style === NumberFormat.Style.PERCENT -> PercentStyleFormatter()
style === NumberFormat.Style.CURRENCY -> CurrencyStyleFormatter()
else -> NumberStyleFormatter()
}
}
}
}
要触发格式化,你可以像以下示例所示,使用@NumberFormat来标记字段:
- Java
- Kotlin
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
class MyModel(
@field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)
格式注释 API
在org.springframework.format.annotation包中存在一个可移植的格式注解API。你可以使用@NumberFormat来格式化Number类型的字段,如Double和Long;使用@DurationFormat来以ISO-8601标准和简化样式格式化Duration类型的字段;使用@DateTimeFormat来格式化java.util.Date、java.util.Calendar以及Long(用于毫秒时间戳)类型的字段,同时也可以格式化JSR-310中的java.time类型字段。
以下示例使用 @DateTimeFormat 将 java.util.Date 格式化为 ISO 日期(yyyy-MM-dd):
- Java
- Kotlin
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
class MyModel(
@DateTimeFormat(iso=ISO.DATE) private val date: Date
)
有关更多详细信息,请参阅@DateTimeFormat、@DurationFormat和@NumberFormat的javadoc。
基于样式的格式化和解析依赖于区域设置敏感的模式,这些模式可能会根据Java运行时环境的不同而发生变化。具体来说,那些依赖于日期、时间或数字的解析和格式化的应用程序在JDK 20或更高版本上运行时,可能会遇到兼容性问题。
使用ISO标准化格式或由你控制的特定模式,可以实现对日期、时间和数字值的可靠、与系统无关且与区域设置无关的解析和格式化。
对于@DateTimeFormat,使用备用模式也有助于解决兼容性问题。
有关更多详细信息,请参阅Spring Framework wiki中的使用JDK 20及更高版本进行日期和时间格式化页面。
FormatterRegistry SPI
FormatterRegistry 是一个用于注册格式化器和转换器的 SPI(Service Provider Interface)。FormattingConversionService 是一种适用于大多数环境的 FormatterRegistry 实现。你可以通过编程方式或声明式配置这种实现作为 Spring bean,例如,使用 FormattingConversionServiceFactoryBean。由于该实现还实现了 ConversionService,因此你可以直接将其配置为与 Spring 的 DataBinder 和 Spring 表达式语言(SpEL)一起使用。
以下列表展示了FormatterRegistry SPI:
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}
如前面的列表所示,您可以根据字段类型或注释来注册格式化规则。
FormatterRegistry SPI 允许你集中配置格式化规则,而无需在各个控制器中重复这些配置。例如,你可能希望强制所有日期字段都以某种方式格式化,或者具有特定注解的字段也以某种方式格式化。通过使用共享的 FormatterRegistry,你可以一次性定义这些规则,然后在需要格式化的任何时候都会应用这些规则。
FormatterRegistrar SPI
FormatterRegistrar 是一个 SPI(Service Provider Interface),用于通过 FormatterRegistry 注册格式化器(formatters)和转换器(converters)。以下列出了其接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
当需要为给定的格式化类别(如日期格式化)注册多个相关的转换器和格式化器时,FormatterRegistrar非常有用。在声明式注册不足以满足需求的情况下,它也表现出其价值——例如,当某个格式化器需要根据与其自身类型 <T> 不同的特定字段类型进行索引时,或者在注册 Printer/Parser 对时。下一节将提供更多关于转换器和格式化器注册的信息。
在Spring MVC中配置格式化
请参阅Spring MVC章节中的转换和格式化。