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.DateLong 之間轉換。當您在用戶端環境(例如 Web 應用程式)中工作,並且需要剖析和列印本地化欄位值時,可以使用 Formatter SPI。ConversionService 為這兩個 SPI 提供統一的型別轉換 API。

Formatter SPI

用於實作欄位格式化邏輯的 Formatter SPI 簡單且強型別。以下清單顯示 Formatter 介面定義

package org.springframework.format;

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

FormatterPrinterParser 建構區塊介面延伸而來。以下清單顯示這兩個介面的定義

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 應該擲回 ParseExceptionIllegalArgumentException。請注意確保您的 Formatter 實作是執行緒安全的。

format 子套件為了方便起見,提供了數個 Formatter 實作。number 套件提供 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter,以使用 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);
}

若要建立實作

  1. A 參數化為您要與格式化邏輯建立關聯的欄位 annotationType,例如 org.springframework.format.annotation.DateTimeFormat

  2. getFieldTypes() 傳回註解可以使用的欄位型別。

  3. getPrinter() 傳回 Printer 以列印已註解欄位的值。

  4. 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 欄位,例如 DoubleLong@DurationFormat 以 ISO-8601 和簡化樣式格式化 Duration 欄位,以及 @DateTimeFormat 格式化欄位,例如 java.util.Datejava.util.CalendarLong (用於毫秒時間戳記),以及 JSR-310 java.time 型別。

以下範例使用 @DateTimeFormatjava.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 中的 Date and Time Formatting with JDK 20 and higher 頁面。

FormatterRegistry SPI

FormatterRegistry 是一個 SPI,用於註冊格式化器和轉換器。FormattingConversionServiceFormatterRegistry 的實作,適用於大多數環境。您可以程式設計方式或宣告方式將此變體組態為 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 章節中的 轉換和格式化