Spring フィールドのフォーマット

前のセクションで説明したように、core.convert は汎用の型変換システムです。統一された ConversionService API と、ある型から別の型への変換ロジックを実装するための厳密に型指定された Converter SPI を提供します。Spring コンテナーは、このシステムを使用して Bean プロパティ値をバインドします。さらに、Spring Expression Language(SpEL)と DataBinder の両方がこのシステムを使用してフィールド値をバインドします。例: SpEL が expression.setValue(Object bean, Object value) の試行を完了するために Short を Long に強制する必要がある場合、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 ビルドブロックインターフェースから拡張されています。次のリストは、これら 2 つのインターフェースの定義を示しています。

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 パッケージは、java.text.NumberFormat を使用する Number オブジェクトをフォーマットするための NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter を提供します。datetime パッケージは、java.text.DateFormat を使用して java.util.Date オブジェクトをフォーマットするための DateFormatter と、@DurationFormat.Style 列挙で定義されたさまざまなスタイル (Format Annotation API を参照) で Duration オブジェクトをフォーマットするための DurationFormatter を提供します。

次の 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 の課題 (英語) を参照してください。

アノテーション駆動の書式設定

フィールドのフォーマットは、フィールド型またはアノテーションによって構成できます。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
)

Format Annotation API

org.springframework.format.annotation パッケージには、ポータブルフォーマットアノテーション API が存在します。@NumberFormat を使用して Double や Long などの Number フィールドをフォーマットしたり、@DurationFormat を使用して Duration フィールドを ISO-8601 および簡略化されたスタイルでフォーマットしたり、@DateTimeFormat を使用して java.util.Datejava.util.CalendarLong (ミリ秒のタイムスタンプ用) や 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 (Javadoc) @DurationFormat (Javadoc) @NumberFormat (Javadoc) の javadoc を参照してください。

スタイルベースの書式設定と解析は、Java ランタイムに応じて変更される可能性のあるロケール依存のパターンに依存します。具体的には、日付、時刻、数値の解析と書式設定に依存するアプリケーションは、JDK 20 以降で実行すると、互換性のない動作の変更が発生する可能性があります。

ISO 標準形式またはユーザーが制御する具体的なパターンを使用すると、システムやロケールに依存しない信頼性の高い日付、時刻、数値の解析と書式設定が可能になります。

@DateTimeFormat の場合、フォールバックパターンを使用すると互換性の課題に対処することもできます。

詳細については、Spring Framework wiki の JDK 20 以降での日付と時刻のフォーマット [GitHub] (英語) ページを参照してください。

FormatterRegistry SPI

FormatterRegistry は、フォーマッタとコンバーターを登録するための SPI です。FormattingConversionService は、ほとんどの環境に適した FormatterRegistry の実装です。たとえば、FormattingConversionServiceFactoryBean を使用して、このバリアントを Spring Bean としてプログラムまたは宣言的に構成できます。この実装は 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 の章の変換とフォーマットを参照してください。