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 實現的一個簡單而強大的替代方案。
通常,當您需要實現通用型別轉換邏輯時,可以使用 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 物件(請參閱格式化註解 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
org.springframework.format.annotation 包中存在一個可移植的格式化註解 API。您可以使用 @NumberFormat 格式化 Double 和 Long 等 Number 欄位,使用 @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 標準化格式或您控制的具體模式可以實現日期、時間值和數字值的可靠的、獨立於系統和區域的解析和格式化。 對於 有關更多詳細資訊,請參閱 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 是透過 FormatterRegistry 註冊格式化程式和轉換器的 SPI。以下列表顯示了其介面定義
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
當註冊給定格式化類別(例如日期格式化)的多個相關轉換器和格式化程式時,FormatterRegistrar 非常有用。在宣告式註冊不足的情況下也很有用,例如,當格式化程式需要索引到與其自己的 <T> 不同的特定欄位型別下,或者當註冊 Printer/Parser 對時。下一節將提供有關轉換器和格式化程式註冊的更多資訊。
在 Spring MVC 中配置格式化
請參閱 Spring MVC 章節中的 轉換和格式化。