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
的例項,用於在客戶端 locale 中顯示。實現 parse()
操作以從客戶端 locale 返回的格式化表示中解析 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
物件(參閱格式註解 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
註解繫結到一個 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
一個可移植的格式註解 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。
基於樣式的格式化和解析依賴於對 locale 敏感的模式,這些模式可能會根據 Java 執行時而變化。具體來說,依賴日期、時間或數字解析和格式化的應用程式在 JDK 20 或更高版本上執行時,可能會遇到行為不相容的變化。 使用 ISO 標準化格式或你控制的具體模式可以實現獨立於系統和 locale 的可靠日期、時間及數字值解析和格式化。 對於 更多詳細資訊,請參閱 Spring Framework wiki 中的使用 JDK 20 及更高版本進行日期和時間格式化頁面。 |
FormatterRegistry
SPI
FormatterRegistry
是用於註冊 formatter 和 converter 的 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);
}
如前所示,你可以透過欄位型別或註解註冊 formatter。
FormatterRegistry
SPI 允許你集中配置格式化規則,而不是在控制器中重複配置。例如,你可能希望強制所有日期欄位以特定方式格式化,或者帶有特定註解的欄位以特定方式格式化。使用共享的 FormatterRegistry
,你只需定義一次這些規則,它們將在需要格式化時應用。
FormatterRegistrar
SPI
FormatterRegistrar
是一個 SPI,用於透過 FormatterRegistry 註冊 formatter 和 converter。以下列表顯示了其介面定義
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
當需要為給定的格式化類別(例如日期格式化)註冊多個相關的 converter 和 formatter 時,FormatterRegistrar
非常有用。在宣告式註冊不足的情況下(例如,當需要將 formatter 索引到與其自身 <T>
不同的特定欄位型別下,或者註冊 Printer
/Parser
對時),它也很有用。下一節提供了有關 converter 和 formatter 註冊的更多資訊。
在 Spring MVC 中配置格式化
請參閱 Spring MVC 章中的轉換與格式化。