对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

Spring 字段格式化

正如上一节所讨论的,core.convert 是一个通用的类型转换系统。它提供了一个统一的 ConversionService API,以及一个强类型的 Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。Spring 容器使用该系统来绑定 Bean 属性值。此外,Spring 表达式语言 (SpEL) 和 DataBinder 也使用该系统来绑定字段值。例如,当 SpEL 需要将 Short 强制转换为 Long 以完成 expression.setValue(Object bean, Object value) 尝试时,core.convert 系统会执行该强制转换。spring-doc.cadn.net.cn

现在考虑典型客户端环境(例如 Web 应用程序或桌面应用程序)中的类型转换需求。在这些环境中,通常需要将 String 转换为其他类型以支持客户端的回传(postback)过程,同时也需要将其他类型转换回 String 以支持视图渲染过程。此外,通常还需要对 String 值进行本地化处理。core.convert 中更通用的 Converter SPI 并未直接解决此类格式化需求。为了直接应对这些需求,Spring 提供了一个便捷的 Formatter SPI,它为客户环境提供了一种简单而健壮的替代方案,用以取代 PropertyEditor 的实现。spring-doc.cadn.net.cn

通常,当你需要实现通用的类型转换逻辑时(例如,在 Converterjava.util.Date 之间进行转换),可以使用 Long SPI。 当你在客户端环境(例如 Web 应用程序)中工作,并且需要解析和打印本地化的字段值时,可以使用 Formatter SPI。 ConversionService 为这两个 SPI 提供了统一的类型转换 API。spring-doc.cadn.net.cn

FormatterSPI

用于实现字段格式化逻辑的 Formatter SPI 简单且具有强类型。以下代码清单展示了 Formatter 接口的定义:spring-doc.cadn.net.cn

package org.springframework.format;

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

Formatter 继承自 PrinterParser 这两个基础接口。以下代码清单展示了这两个接口的定义:spring-doc.cadn.net.cn

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实现是线程安全的。spring-doc.cadn.net.cn

format 子包为方便起见提供了多种 Formatter 实现。 number 包提供了 NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter,用于格式化使用 java.text.NumberFormatNumber 对象。 datetime 包提供了一个 DateFormatter,用于通过 java.text.DateFormat 格式化 java.util.Date 对象。spring-doc.cadn.net.cn

以下的 DateFormatter 是一个 Formatter 实现示例:spring-doc.cadn.net.cn

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以参与贡献。spring-doc.cadn.net.cn

注解驱动的格式化

字段格式化可以通过字段类型或注解进行配置。要将一个注解绑定到Formatter,需实现AnnotationFormatterFactory接口。以下代码清单展示了AnnotationFormatterFactory接口的定义:spring-doc.cadn.net.cn

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);
}

要创建一个实现:spring-doc.cadn.net.cn

  1. A 参数化为希望关联格式化逻辑的字段 annotationType —— 例如 org.springframework.format.annotation.DateTimeFormatspring-doc.cadn.net.cn

  2. getFieldTypes() 返回该注解可用于哪些字段类型的类型。spring-doc.cadn.net.cn

  3. getPrinter() 返回一个 Printer,用于打印带注解字段的值。spring-doc.cadn.net.cn

  4. getParser() 返回一个 Parser,用于解析带注解字段的 clientValuespring-doc.cadn.net.cn

以下示例中的 AnnotationFormatterFactory 实现将 @NumberFormat 注解绑定到一个格式化器,以允许指定数字的样式或模式:spring-doc.cadn.net.cn

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 注解字段,如下例所示:spring-doc.cadn.net.cn

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 来格式化 Number 字段(例如 DoubleLong),并使用 @DateTimeFormat 来格式化 java.util.Datejava.util.CalendarLong(用于毫秒时间戳)以及 JSR-310 的 java.timespring-doc.cadn.net.cn

以下示例使用 @DateTimeFormatjava.util.Date 格式化为 ISO 日期 (yyyy-MM-dd):spring-doc.cadn.net.cn

public class MyModel {

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

FormatterRegistrySPI

FormatterRegistry 是一个用于注册格式化器(formatter)和转换器(converter)的 SPI 接口。 FormattingConversionServiceFormatterRegistry 的一种实现,适用于大多数环境。你可以通过编程方式或声明方式将此实现配置为 Spring Bean,例如使用 FormattingConversionServiceFactoryBean。由于该实现同时也实现了 ConversionService 接口,因此你可以直接将其配置用于 Spring 的 DataBinder 和 Spring 表达式语言(SpEL)。spring-doc.cadn.net.cn

以下代码清单展示了 FormatterRegistry SPI:spring-doc.cadn.net.cn

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);
}

如上所示,您可以按字段类型或按注解注册格式化器。spring-doc.cadn.net.cn

FormatterRegistry SPI 允许您集中配置格式化规则,而无需在各个控制器中重复此类配置。例如,您可能希望强制所有日期字段都以某种特定方式格式化,或者带有特定注解的字段以某种特定方式格式化。通过共享的 FormatterRegistry,您只需定义一次这些规则,它们就会在需要格式化时自动应用。spring-doc.cadn.net.cn

FormatterRegistrarSPI

FormatterRegistrar 是一个 SPI(服务提供者接口),用于通过 FormatterRegistry 注册格式化器(formatter)和转换器(converter)。以下代码清单展示了其接口定义:spring-doc.cadn.net.cn

package org.springframework.format;

public interface FormatterRegistrar {

	void registerFormatters(FormatterRegistry registry);
}

FormatterRegistrar 在为某一格式化类别(例如日期格式化)注册多个相关的转换器(converter)和格式化器(formatter)时非常有用。当声明式注册方式不够用时,它也很有帮助——例如,当某个格式化器需要根据与其自身 <T> 类型不同的特定字段类型进行索引,或者在注册 Printer/Parser 对时。下一节将提供更多关于转换器和格式化器注册的信息。spring-doc.cadn.net.cn

在 Spring MVC 中配置格式化

参见 Spring MVC 章节中的转换与格式化spring-doc.cadn.net.cn