Spring 源码解析:DateTimeFormatAnnotationFormatterFactory 与 @DateTimeFormat 注解驱动的日期格式化工厂
2026/9/13 2:03:24 网站建设 项目流程

Spring 源码解析:DateTimeFormatAnnotationFormatterFactory 与 @DateTimeFormat 注解驱动的日期格式化工厂

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter

导读

本文围绕 Spring 格式化框架中的核心工厂类org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory展开,剖析它是如何把@DateTimeFormat注解翻译成可复用的DateFormatter,从而让日期字段在 Web 请求绑定与响应输出时自动完成"字符串 ↔ 日期对象"的双向转换。读完本文,你将掌握AnnotationFormatterFactory工厂模式的落地写法、style / iso / pattern三种日期格式配置维度的优先级关系,以及EmbeddedValueResolutionSupport支持的${...}占位符解析能力,并能在自己的项目中复刻这套"注解驱动 + 工厂生产格式化器"的设计。

一、类定位:日期格式化工厂在整个格式化体系中的位置

在 Spring 的org.springframework.format包中,格式化能力由一对对称的顶层接口定义:

  • Printer:把对象打印成字符串,方法签名String print(T object, Locale locale),是@FunctionalInterface
  • Parser:把字符串解析成对象,方法签名T parse(String text, Locale locale) throws ParseException,同样是@FunctionalInterface
  • Formatter:同时继承Printer<T>Parser<T>,一个类即可完成双向转换,常见的DateFormatter就是该接口的实现;
  • AnnotationFormatterFactory:负责"根据注解 + 字段类型"生产出对应的PrinterParser,是注解驱动格式化的总开关。

DateTimeFormatAnnotationFormatterFactory正是AnnotationFormatterFactory<DateTimeFormat>在日期时间领域的具体实现:它为标注了@DateTimeFormat的字段,按注解上声明的格式要求动态地生产格式化器。从源码结构看,格式化工厂的调用链大致是:FormattingConversionService在注册时扫描所有AnnotationFormatterFactory,当遇到带@DateTimeFormat注解的字段时,委托该工厂生产Printer/Parser,再经由类型转换服务完成字段绑定。

下图是该类的继承关系与方法概览,可以看到它同时实现了AnnotationFormatterFactory接口并继承EmbeddedValueResolutionSupport(从而获得占位符解析能力):

二、类定义与双重身份:工厂接口 + 嵌入式值解析能力

类全路径:org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory

public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport implements AnnotationFormatterFactory<DateTimeFormat> { /** * 字段类型 */ private static final Set<Class<?>> FIELD_TYPES; ... }

DateTimeFormatAnnotationFormatterFactory拥有双重身份:

  1. 实现AnnotationFormatterFactory<DateTimeFormat>:以@DateTimeFormat为注解泛型,向 Spring 声明"凡是被该注解标注的字段,都由我来生产格式化器";
  2. 继承EmbeddedValueResolutionSupport:这是本类一个容易被忽略但非常关键的设计。EmbeddedValueResolutionSupport实现了EmbeddedValueResolverAware,内部持有EmbeddedValueResolver解析器,并暴露受保护的resolveEmbeddedValue(String)方法。正是借助它,注解上的stylepattern属性才能写成${my.date.style}这类占位符形式,由容器在运行时解析成真实配置值——这让格式化规则具备了"配置外部化"的能力,而不是把日期格式硬编码在注解里。

三、支持字段类型:FIELD_TYPES 静态集合

static { Set<Class<?>> fieldTypes = new HashSet<>(4); // 加入字段类型 fieldTypes.add(Date.class); fieldTypes.add(Calendar.class); fieldTypes.add(Long.class); FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); } @Override public Set<Class<?>> getFieldTypes() { return FIELD_TYPES; }

FIELD_TYPES在静态初始化块中构建,随后被包装成不可修改集合(Collections.unmodifiableSet),保证外部无法篡改。它声明了本工厂支持的三类字段:

字段类型说明
Datejava.util.Date,最常见的日期类型
Calendarjava.util.Calendar,日历类
Long以毫秒时间戳形式承载的日期

这里的Long值得单独说明:@DateTimeFormat可以标注在Long类型的字段上,此时该字段被视为毫秒级时间戳,工厂会把DateFormatter应用于毫秒值,实现时间戳与展示字符串之间的互转。这一点从同包下的 MillisecondInstantPrinter 可以得到印证——该 Printer 接收Long(instant)并借助JodaTimeContextHolder获取的DateTimeFormatter完成打印。

四、生产格式化器:getPrinter 与 getParser 的对称委托

@Override public Printer<?> getPrinter(DateTimeFormat annotation, Class<?> fieldType) { return getFormatter(annotation, fieldType); } @Override public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) { return getFormatter(annotation, fieldType); }

getPrintergetParser都直接委托给受保护的getFormatter(DateTimeFormat, Class<?>)。这正是Formatter<T>接口"一个类同时承担打印与解析"的体现:DateFormatter既实现了Printer<Date>print,也实现了Parser<Date>parse,因此工厂只需构建一个格式化器实例,即可同时应答"打印"与"解析"两个方向的请求。

从源码结构可以推断,Spring 的FormattingConversionService在绑定请求参数时会先查getFieldTypes()判断字段类型是否匹配,再调用getParser()完成"字符串 → 对象";而在渲染模型输出时调用getPrinter()完成"对象 → 字符串"。这也是@DateTimeFormat能同时作用于表单回显与参数接收的根本原因。

五、核心逻辑:getFormatter 的三维配置组装

getFormatter是本类的灵魂方法,它负责把注解上的配置翻译成一个配置完备的DateFormatter

protected Formatter<Date> getFormatter(DateTimeFormat annotation, Class<?> fieldType) { DateFormatter formatter = new DateFormatter(); // style String style = resolveEmbeddedValue(annotation.style()); // 判断时间格式是否存在 if (StringUtils.hasLength(style)) { formatter.setStylePattern(style); } // iso 设置 formatter.setIso(annotation.iso()); // date time pattern String pattern = resolveEmbeddedValue(annotation.pattern()); // 设置 if (StringUtils.hasLength(pattern)) { formatter.setPattern(pattern); } return formatter; }

整个组装过程清晰对应@DateTimeFormat注解的三个配置维度:

1. style —— 预定义风格样式

style属性对应DateTimeFormat.Style(如S/M/L/F及日期时间组合写法),本类先通过resolveEmbeddedValue(annotation.style())解析可能的占位符,再以StringUtils.hasLength判空——只有 style 非空时才调用formatter.setStylePattern(style)。这意味着:注解可以只写 style,由DateFormatter内部按风格模板生成格式。

2. iso —— ISO 标准格式

iso属性对应DateTimeFormat.ISO枚举(DATETIMEDATE_TIMENONE),通过formatter.setIso(annotation.iso())无条件设置ISO.NONE即表示不使用 ISO 格式)。与 style、pattern 不同,iso 是枚举而非字符串,因此不需要也无法进行占位符解析。

3. pattern —— 自定义模式串

pattern属性是最灵活的配置项,支持yyyy-MM-dd HH:mm:ss这类SimpleDateFormat模式串。与 style 相同,它也会先经过resolveEmbeddedValue解析——这也是pattern可以写成${...}占位符的原因。同样判空后才调用formatter.setPattern(pattern)

优先级与协作关系

从组装顺序可以归纳出三者的协作逻辑:

  • 注解上没有配置 style 时,跳过 style 设置;
  • iso总是被写入(默认NONE表示不启用 ISO);
  • 注解上没有配置 pattern 时,跳过 pattern 设置。

DateFormatter自身维护了 style / iso / pattern 三套内部状态,最终在print/parse时按"显式 pattern > ISO > style"的次序选择生效的格式模板。工厂在这里只负责"读注解、做占位符解析、装配 formatter",具体的格式推导与解析打印细节全部下沉到DateFormatter,职责划分非常清晰。

六、占位符解析能力的意义:配置外部化

EmbeddedValueResolutionSupport赋予的能力让如下写法成为可能:

public class User { // pattern 引用外部配置(如 application.properties 中的 my.date.pattern) @DateTimeFormat(pattern = "${my.date.pattern}") private Date birthday; // style 也可以走占位符 @DateTimeFormat(style = "${my.date.style}") private Date registerTime; // 直接写死模式串亦可 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date lastLoginTime; }

resolveEmbeddedValue会委托给容器注入的EmbeddedValueResolver(在 Spring MVC/Spring Boot 环境中通常是PropertySourcesPlaceholderConfigurer配套的解析器),把${my.date.pattern}解析为配置文件中实际的模式串。这意味着:

  • 日期格式可以从代码注解中剥离,集中到配置文件统一管理;
  • 同一套代码在不同环境(如 zh_CN 与 en_US 站点)可以注入不同的日期格式;
  • 修改格式无需重新编译。

七、支撑类型一览:DateFormatter 与 Joda 时代的周边实现

getFormatter最终产出的DateFormatter位于org.springframework.format.datetime包,它实现Formatter<Date>,内部将styleisopattern三种配置归一化为底层的格式模式。此外,在同包(及datetime.joda子包)中还分布着若干直接实现Printer/Parser的小工具类,它们体现了同一设计意图在不同日期库上的复用:

  • DateTimeParser:org.springframework.format.datetime.joda.DateTimeParser,实现Parser<DateTime>,构造时接收 JodaDateTimeFormatterparse时通过JodaTimeContextHolder.getFormatter(this.formatter, locale)结合当前 Locale 将字符串解析为 JodaDateTime
  • MillisecondInstantPrinter:org.springframework.format.datetime.joda.MillisecondInstantPrinter,实现Printer<Long>,借助 JodaDateTimeFormatter将毫秒时间戳打印成字符串。

可以看到,无论基于java.util还是 Joda Time,Spring 都遵循同一套Printer/Parser抽象,DateTimeFormatAnnotationFormatterFactory只是"注解 → 格式化器"这条生产线上与@DateTimeFormat对接的入口之一。

八、在 Spring MVC 中的实际应用场景

结合 Spring MVC 的数据绑定机制,DateTimeFormatAnnotationFormatterFactory的典型工作过程如下:

  1. 前端提交birthday=1995-08-24之类的字符串参数;
  2. WebDataBinder发现目标字段birthday标注了@DateTimeFormat,向FormattingConversionService查找对应的AnnotationFormatterFactory
  3. 命中DateTimeFormatAnnotationFormatterFactory,其getFieldTypes()确认Date在支持范围内,调用getParser获得配置好的DateFormatter
  4. DateFormatter.parse("1995-08-24", locale)按 pattern/iso/style 解析出Date对象并完成字段赋值;
  5. 响应渲染(如表单回显)时,通过getPrinter再按同样规则把Date打印回1995-08-24字符串。

整个过程对业务代码完全透明——开发者只需在字段上标注@DateTimeFormat,格式化的生产、装配与调用都由工厂类与转换服务协作完成。

九、总结

DateTimeFormatAnnotationFormatterFactory用不足百行代码,诠释了 Spring 格式化体系的核心设计:

关注点工厂的实现方式
支持哪些字段类型FIELD_TYPES静态集合声明DateCalendarLong(毫秒时间戳)
如何响应打印/解析getPrinter/getParser对称委托给getFormatter,一个DateFormatter双职复用
注解配置如何落地style(预定义风格)、iso(ISO 枚举)、pattern(自定义模式串)三维组装
如何支持动态配置继承EmbeddedValueResolutionSupport,对 style/pattern 执行${...}占位符解析
与周边组件的关系实现AnnotationFormatterFactory<DateTimeFormat>,接入FormattingConversionService注解驱动绑定链路

工厂 + 注解 +Printer/Parser抽象的搭配,让"如何格式化"与"何时格式化"彻底解耦:业务代码只声明意图(注解),Spring 负责在恰当的时机(数据绑定)用恰当的工厂(本类)生产恰当的格式化器。理解了这个类,也就掌握了 Spring 中所有AnnotationFormatterFactory实现(如数字、货币、集合等领域的同类工厂)的通用套路,可以轻松举一反三。

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询