Spring Field Formatting

正如前一节中所讨论的,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。

The Formatter SPI

实现字段格式化逻辑的 Formatter SPI 简单且类型强。以下清单显示了 Formatter 接口定义:

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 的一个实例以在客户端区域设置中显示。实现 parse() 操作,从客户端区域设置返回的格式化表示中解析 T 的一个实例。如果解析尝试失败,您的 Formatter 应抛出 ParseException 或 IllegalArgumentException。注意确保您的 Formatter 实现是线程安全的。

format 子包提供了几个 Formatter 实现供您方便使用。number 包提供了 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter 来格式化使用 java.text.NumberFormat 的 Number 对象。datetime 包提供 DateFormatter 来使用 java.text.DateFormat 格式化 java.util.Date 对象。

以下 DateFormatter 是一个示例 Formatter 实现:

  • Java

  • Kotlin

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`贡献。请参阅https://github.com/spring-projects/spring-framework/issues[GitHub Issues]进行贡献。

Annotation-driven Formatting

可以通过字段类型或注释来配置字段格式化。要将注释绑定到 Formatter,请实现 AnnotationFormatterFactory。以下清单显示了 AnnotationFormatterFactory 接口的定义:

public interface AnnotationFormatterFactory<A extends Annotation> {

	Set<Class<?>> getFieldTypes();

	Printer<?> getPrinter(A annotation, Class<?> fieldType);

	Parser<?> getParser(A annotation, Class<?> fieldType);
}

要创建一个实现:

  1. A 参数化为要关联格式化逻辑(例如 org.springframework.format.annotation.DateTimeFormat)的字段 annotationType

  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

一个可移植格式注释 API 存在于 org.springframework.format.annotation 包中。您可以使用 @NumberFormat 格式化数字字段(例如 Double 和 Long),使用 @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
)

The FormatterRegistry SPI

FormatterRegistry 是一种用于注册格式化器和转换器的 SPI。FormattingConversionServiceFormatterRegistry 的一个实现,适合大多数环境。您可以以编程方式或声明方式配置此变量作为一个 Spring Bean,例如,通过使用 FormattingConversionServiceFactoryBean。由于此实现还实现了 ConversionService,您可以直接对其进行配置,以与 Spring 的 DataBinder 和 Spring 表达式语言 (SpEL) 配合使用。

以下清单显示了 FormatterRegistry SPI:

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,您只需定义一次这些规则,然后在需要格式化时应用它们。

The FormatterRegistrar SPI

FormatterRegistrar 是一种通过 FormatterRegistry 注册格式化器和转换器的 SPI。以下清单显示了它的接口定义:

public interface FormatterRegistrar {

	void registerFormatters(FormatterRegistry registry);
}

当为给定的格式化类别(例如日期格式化)注册多个相关的转换器和格式器时,FormatterRegistrar 非常有用。在声明注册不足时,它也可能非常有用,例如,当格式器需要在不同于其自己的 <T> 的特定字段类型下进行索引或注册 Printer/Parser 对时。下一节提供了有关转换器和格式器注册的更多信息。

Configuring Formatting in Spring MVC

请参阅 Spring MVC 章节中的Conversion and Formatting