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:负责"根据注解 + 字段类型"生产出对应的
Printer与Parser,是注解驱动格式化的总开关。
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拥有双重身份:
- 实现
AnnotationFormatterFactory<DateTimeFormat>:以@DateTimeFormat为注解泛型,向 Spring 声明"凡是被该注解标注的字段,都由我来生产格式化器"; - 继承
EmbeddedValueResolutionSupport:这是本类一个容易被忽略但非常关键的设计。EmbeddedValueResolutionSupport实现了EmbeddedValueResolverAware,内部持有EmbeddedValueResolver解析器,并暴露受保护的resolveEmbeddedValue(String)方法。正是借助它,注解上的style与pattern属性才能写成${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),保证外部无法篡改。它声明了本工厂支持的三类字段:
| 字段类型 | 说明 |
|---|---|
Date | java.util.Date,最常见的日期类型 |
Calendar | java.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); }getPrinter与getParser都直接委托给受保护的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枚举(DATE、TIME、DATE_TIME、NONE),通过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>,内部将style、iso、pattern三种配置归一化为底层的格式模式。此外,在同包(及datetime.joda子包)中还分布着若干直接实现Printer/Parser的小工具类,它们体现了同一设计意图在不同日期库上的复用:
- DateTimeParser:
org.springframework.format.datetime.joda.DateTimeParser,实现Parser<DateTime>,构造时接收 JodaDateTimeFormatter,parse时通过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的典型工作过程如下:
- 前端提交
birthday=1995-08-24之类的字符串参数; WebDataBinder发现目标字段birthday标注了@DateTimeFormat,向FormattingConversionService查找对应的AnnotationFormatterFactory;- 命中
DateTimeFormatAnnotationFormatterFactory,其getFieldTypes()确认Date在支持范围内,调用getParser获得配置好的DateFormatter; DateFormatter.parse("1995-08-24", locale)按 pattern/iso/style 解析出Date对象并完成字段赋值;- 响应渲染(如表单回显)时,通过
getPrinter再按同样规则把Date打印回1995-08-24字符串。
整个过程对业务代码完全透明——开发者只需在字段上标注@DateTimeFormat,格式化的生产、装配与调用都由工厂类与转换服务协作完成。
九、总结
DateTimeFormatAnnotationFormatterFactory用不足百行代码,诠释了 Spring 格式化体系的核心设计:
| 关注点 | 工厂的实现方式 |
|---|---|
| 支持哪些字段类型 | FIELD_TYPES静态集合声明Date、Calendar、Long(毫秒时间戳) |
| 如何响应打印/解析 | 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),仅供参考