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.DateLong 之間進行轉換),可以使用 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 擴充套件自 PrinterParser 構建塊介面。以下列表顯示了這兩個介面的定義

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

format 子包提供了一些方便的 Formatter 實現。number 包提供了 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter 用於格式化使用 java.text.NumberFormatNumber 物件。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 註解繫結到一個 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 欄位(如 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。

基於樣式的格式化和解析依賴於對 locale 敏感的模式,這些模式可能會根據 Java 執行時而變化。具體來說,依賴日期、時間或數字解析和格式化的應用程式在 JDK 20 或更高版本上執行時,可能會遇到行為不相容的變化。

使用 ISO 標準化格式或你控制的具體模式可以實現獨立於系統和 locale 的可靠日期、時間及數字值解析和格式化。

對於 @DateTimeFormat,使用回退模式也有助於解決相容性問題。

更多詳細資訊,請參閱 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 章中的轉換與格式化