Spring Field 格式化
正如前一节所讨论的,core.convert 是一个通用的类型转换系统。它提供了一个统一的 ConversionService
API 以及一个强类型的 Converter
SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用这个系统来绑定 bean 属性值。此外,Spring 表达式语言 (SpEL) 和 DataBinder
也使用这个系统来绑定字段值。例如,当 SpEL 需要将 Short
强制转换为 Long
以完成 expression.setValue(Object bean, Object value)
尝试时,core.convert
系统会执行此强制转换。
现在考虑一个典型客户端环境(如网页或桌面应用程序)的类型转换需求。在这些环境中,通常需要从 String
转换以支持客户端回发过程,并转换回 String
以支持视图渲染过程。此外,还经常需要对 String
值进行本地化。更通用的 core.convert
Converter
SPI 并未直接解决此类格式化需求。为直接解决这些需求,Spring 提供了一个方便的 Formatter
SPI,它为客户端环境提供了一个简单而强大的替代 PropertyEditor
实现的方法。
一般来说,当你需要实现通用的类型转换逻辑时,可以使用 Converter
SPI —— 例如,在 java.util.Date
和 Long
之间进行转换。当你在客户端环境(如 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
是一个 Formatter
实现示例:
- 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 Issues 进行贡献。
基于注解的格式化
可以通过字段类型或注解来配置字段格式。要将注解绑定到一个 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
注解绑定到一个格式化器,以便指定数字样式或模式:
- 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
一个可移植的格式注解 API 存在于 org.springframework.format.annotation
包中。你可以使用 @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。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,用于通过 FormatterRegistry 注册格式化器和转换器。以下列表显示了其接口定义:
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
FormatterRegistrar
在为给定的格式化类别(例如日期格式化)注册多个相关的转换器和格式化器时非常有用。当声明式注册不足时,它也很有用——例如,当格式化器需要在与其自身 <T>
不同的特定字段类型下进行索引时,或者在注册 Printer
/Parser
对时。下一节将提供有关转换器和格式化器注册的更多信息。
在 Spring MVC 中配置格式化
请参阅 Spring MVC 章节中的转换和格式化。