Skip to content

Spring 字段格式化 (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 转换以支持客户端回表(postback)过程,以及转换回 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 接口定义:

java
package org.springframework.format;

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

Formatter 继承自 PrinterParser 构建块接口。以下列表显示了这两个接口的定义:

java
public interface Printer<T> {

	String print(T fieldValue, Locale locale);
}
java
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.DateFormatjava.util.Date 对象,以及一个 DurationFormatter 来格式化处于 @DurationFormat.Style 枚举中定义的不同样式的 Duration 对象。

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

java
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;
	}
}
kotlin
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
	}
}

注解驱动的格式化 (Annotation-driven Formatting)

字段格式化可以按字段类型或注解进行配置。要将注解绑定到 Formatter,请实现 AnnotationFormatterFactory。以下列表显示了 AnnotationFormatterFactory 接口的定义:

java
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
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());
		}
		// 否则
		return switch(annotation.style()) {
			case Style.PERCENT -> new PercentStyleFormatter();
			case Style.CURRENCY -> new CurrencyStyleFormatter();
			default -> new NumberStyleFormatter();
		};
	}
}
kotlin
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
public class MyModel {

	@NumberFormat(style=Style.CURRENCY)
	private BigDecimal decimal;
}
kotlin
class MyModel(
	@field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)

格式化注解 API (Format Annotation API)

org.springframework.format.annotation 包中存在一个便携式格式注解 API。你可以使用 @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
public class MyModel {

	@DateTimeFormat(iso=ISO.DATE)
	private Date date;
}
kotlin
class MyModel(
	@DateTimeFormat(iso=ISO.DATE) private val date: Date
)

有关更多详细信息,请参阅 @DateTimeFormat@DurationFormat@NumberFormat 的 JavaDoc。

警告

基于样式的格式化和解析依赖于区域设置敏感(locale-sensitive)的模式,这些模式可能会根据 Java 运行时的不同而改变。具体而言,依赖于日期、时间或数字解析和格式化的应用程序在 JDK 20 或更高版本上运行时可能会遇到不兼容的行为变化。

使用 ISO 标准化格式或你控制的特定模式,可以实现日期、时间及数字值的可靠且独立于系统和区域设置的解析和格式化。

对于 @DateTimeFormat,使用回退模式(fallback patterns)也有助于解决兼容性问题。

有关更多详细信息,请参阅相关页面:JDK 20 及更高版本下的日期和时间格式化

FormatterRegistry SPI

FormatterRegistry 是一个用于注册格式化器和转换器的 SPI。FormattingConversionService 是适用于大多数环境的 FormatterRegistry 实现。你可以通过编程方式或声明方式将此变体配置为 Spring Bean,例如使用 FormattingConversionServiceFactoryBean。因为此实现也实现了 ConversionService,所以你可以直接将其配置为供 Spring 的 DataBinder 和 Spring 表达式语言 (SpEL) 使用。

以下列表显示了 FormatterRegistry SPI:

java
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。以下列表显示了其接口定义:

java
package org.springframework.format;

public interface FormatterRegistrar {

	void registerFormatters(FormatterRegistry registry);
}

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

在 Spring MVC 中配置格式化

请参阅 Spring MVC 章节中的转换和格式化


补充教学

1. Formatter 与 Converter 的本质区别

  • Converter<S, T>:通用、全能。它可以处理任何类型到任何类型的转换(如 StringUser 对象,或 IntegerBoolean)。它不感知区域设置(Locale)。
  • Formatter<T>:专为 String <-> Object 转换设计,且 感知区域设置 (Locale-aware)。它适合处理展示层的数据,比如将日期显示为 2023年10月24日(中文环境)或 Oct 24, 2023(英文环境)。
  • 整合:Spring 内部会自动将 Formatter 适配为一个特殊的 Converter<String, T>

2. Spring Boot 的“自动魔力”

在 Spring Boot 中,你几乎不需要手动配置 FormatterRegistry

  • 自动注册:只要你的 Formatter 实现类被声明为 @Component,Spring Boot 就会自动发现它并将其注册到默认的 FormattingConversionService 中。
  • 默认格式:可以通过 application.properties 配置全局的日期格式:
    properties
    spring.mvc.format.date=yyyy-MM-dd
    spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss

3. @DateTimeFormat vs @JsonFormat

这是一个常见的混淆点:

  • @DateTimeFormat:它是 Spring 的注解,主要用于 非 JSON 环境。比如处理 GET 请求的 URL 参数(query params)或表单提交(form data)。
  • @JsonFormat:它是 Jackson 的注解,主要用于 JSON 环境。比如处理请求体(@RequestBody)或响应体(@ResponseBody)中的 JSON 数据。
  • 建议:在现代 RESTful API 开发中,通常两者都会加上,以确保在不同数据交互场景下都能获得正确的格式。

4. 线程安全提示

SimpleDateFormat 本身 不是 线程安全的。但在 Spring 的 DateFormatter 示例中,它是通过局部变量(在 getDateFormat(locale) 方法中新建实例)或使用线程安全的 JSR-310 解析器来规避此问题的。在自定义实现的 printparse 方法中,请务必注意不要共享非线程安全的解析器实例。

Based on Spring Framework.