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> {
}

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 应该抛出 ParseExceptionIllegalArgumentException。请注意确保您的 Formatter 实现是线程安全的。

format 子包提供了几个 Formatter 实现以方便使用。number 包提供了 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter,用于格式化使用 java.text.NumberFormatNumber 对象。datetime 包提供了一个 DateFormatter,用于使用 java.text.DateFormat 格式化 java.util.Date 对象,以及一个 DurationFormatter,用于以 @DurationFormat.Style 枚举中定义的各种样式格式化 Duration 对象(请参阅 格式化注解 API)。

以下 DateFormatterFormatter 实现的一个示例:

  • 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

org.springframework.format.annotation 包中存在一个可移植的格式化注解 API。您可以使用 @NumberFormat 格式化 DoubleLongNumber 字段,使用 @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、https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/format/annotation/DurationFormat.html[@DurationFormat] 和 @NumberFormat 的 javadoc。

基于样式的格式化和解析依赖于区域设置敏感的模式,这些模式可能会根据 Java 运行时而改变。具体来说,依赖日期、时间或数字解析和格式化的应用程序在 JDK 20 或更高版本上运行时可能会遇到不兼容的行为更改。 使用 ISO 标准化格式或您控制的具体模式允许对日期、时间、和数字值进行可靠的独立于系统和区域设置的解析和格式化。 对于 @DateTimeFormat,使用回退模式也有助于解决兼容性问题。 有关更多详细信息,请参阅 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 章中的 转换和格式化