Spring Formatter 格式化 SPI 源码解析:Printer、Parser 与注解驱动的日期格式化体系
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
导读
org.springframework.format.Formatter是 Spring 框架格式化(Format)体系的核心接口,它将"对象 → 字符串"(输出)与"字符串 → 对象"(输入)两类转换能力统一封装在同一个组件中,是 Spring MVC 表单绑定、@RequestParam参数解析、@PathVariable路径变量转换以及@DateTimeFormat、@NumberFormat等注解式格式化能力的底层基石。本文以仓库中的源码笔记 Spring-Formatter.md 为骨架,结合同目录下的 Spring-Parser.md、Spring-Printer.md、Spring-AnnotationFormatterFactory.md 及 Spring-DateTimeFormatAnnotationFormatterFactory.md 等文档,完整剖析 Formatter 的接口设计、两个父接口的职责分工、注解驱动工厂的组装逻辑,以及 Joda-Time 体系下的具体 Parser/Printer 实现。读完本文,你将掌握 Spring 格式化 SPI 的整体类图脉络,并能够自定义 Formatter 实现日期、金额等字段的格式化与反格式化。
一、Formatter 接口:一条接口,双向能力
先看接口的定义(摘自 Spring-Formatter.md):
- 类全路径:
org.springframework.format.Formatter
public interface Formatter<T> extends Printer<T>, Parser<T> { }接口体是空的,它本身不声明任何新方法,而是通过继承把两条能力线合并到了一起:
| 父接口 | 方向 | 核心方法 | 职责 |
|---|---|---|---|
Printer<T> | 对象 → 字符串(输出/展示) | String print(T object, Locale locale) | 将领域对象格式化为可供界面展示的文本 |
Parser<T> | 字符串 → 对象(输入/回填) | T parse(String text, Locale locale) throws ParseException | 将用户提交的文本解析回领域对象 |
从设计上看,Formatter<T>是 Spring 在ConversionService类型转换体系之上抽象出的"面向展示层"的格式化组件:类型转换(Conversion)关心的是任意类型之间的转换,而格式化(Format)关心的是与 Locale(本地化)相关的、面向人机交互的字符串往返。一个Formatter把print与parse配对封装,保证"打印出来是什么格式,就能按什么格式解析回去",避免了双向格式不一致的问题。
一个典型的Formatter实现示例:
public class DateFormatter implements Formatter<Date> { @Override public String print(Date object, Locale locale) { // Date -> String,例如 "2026-09-12" } @Override public Date parse(String text, Locale locale) throws ParseException { // String -> Date,与 print 保持对称 } }原文档特别指出:比较常见的实现就是DateFormatter,它正是org.springframework.format.datetime包中日期格式化能力的核心载体,后面的DateTimeFormatAnnotationFormatterFactory就是围绕它进行组装。
二、Printer:对象转字符串(输出方向)
- 类全路径:
org.springframework.format.Printer - 类作用:将对象转换成字符串
@FunctionalInterface public interface Printer<T> { /** * Print the object of type T for display. * 打印对象 * @param object the instance to print * @param locale the current user locale * @return the printed text string */ String print(T object, Locale locale); }要点解析:
- 接口被标注为
@FunctionalInterface,因此可以被 lambda 表达式直接实现,适合在代码中快速声明轻量级的打印逻辑; - 方法签名中的
Locale locale是格式化体系与普通类型转换的关键区别:同一对象在不同地区可能有不同的展示格式(例如日期2026/09/12与12/09/2026、数字千分位分隔符1,234.56与1.234,56),Printer把地区信息显式交给实现者处理; - 返回的
String是"面向显示的文本",不保证能够被机器无损还原,但配合对称的Parser可以做到格式化与解析闭环。
原文档中的类图(见 Spring-Parser.md)展示了该体系在 Spring 中的完整类图结构:
三、Parser:字符串转对象(输入方向)
- 类全路径:
org.springframework.format.Parser - 类作用:将字符串转换成 Java 对象
@FunctionalInterface public interface Parser<T> { /** * Parse a text String to produce a T. * 将字符串转换成对象 * @param text the text string * @param locale the current user locale * @return an instance of T * @throws ParseException when a parse exception occurs in a java.text parsing library * @throws IllegalArgumentException when a parse exception occurs */ T parse(String text, Locale locale) throws ParseException; }要点解析:
- 与
Printer一样是函数式接口,可 lambda 化; - 输入是文本与地区,输出是泛型对象
T; - 异常约定值得注意:当底层使用
java.text解析库时抛ParseException(如SimpleDateFormat),当解析逻辑自身失败时抛IllegalArgumentException。这一约定是 Spring 数据绑定(DataBinder)捕获转换异常并生成FieldError提示信息的基础; Printer与Parser一"出"一"入",共同构成Formatter<T>的全部语义。
四、从接口到实现:Formatter 体系的典型实现与注解驱动
4.1 体系中的两类实现路径
从源码结构看,Spring 的 format 包下围绕Formatter存在两条典型的实现路径:
- 直接实现
Formatter<T>:如DateFormatter,一次实现双向能力,通过FormatterRegistry.addFormatter(...)注册后即可在数据绑定中使用; - 通过
AnnotationFormatterFactory<A>按注解分发:根据字段上标注的注解(如@DateTimeFormat)与字段类型,动态返回对应的Printer/Parser,实现"一个注解覆盖多种字段类型"的格式化策略,典型代表是DateTimeFormatAnnotationFormatterFactory。
4.2 AnnotationFormatterFactory:注解驱动的工厂接口
- 类全路径:
org.springframework.format.AnnotationFormatterFactory
public interface AnnotationFormatterFactory<A extends Annotation> { /** * The types of fields that may be annotated with the <A> annotation. * 字段类型 */ Set<Class<?>> getFieldTypes(); /** * Get the Printer to print the value of a field of {@code fieldType} annotated with * {@code annotation}. * 通过注解和字段类型获取输出接口 */ Printer<?> getPrinter(A annotation, Class<?> fieldType); /** * Get the Parser to parse a submitted value for a field of {@code fieldType} * annotated with {@code annotation}. * 通过注解和字段类型获取解析接口 */ Parser<?> getParser(A annotation, Class<?> fieldType); }三个方法的语义(详见 Spring-AnnotationFormatterFactory.md):
getFieldTypes():声明哪些字段类型可以被注解<A>标注。工厂注册后,Spring 只会对该集合内的字段类型生效;getPrinter(A annotation, Class<?> fieldType):根据注解实例与被标注字段的类型返回对应的Printer。若返回的 Printer 接收的类型T与fieldType不可赋值,Spring 会在调用 Printer 前先做一次从fieldType到T的类型强制转换(coercion);getParser(A annotation, Class<?> fieldType):同理返回Parser。若 Parser 返回的对象不可赋值给fieldType,在字段赋值前也会尝试一次 coercion。
这套设计的价值在于:格式化策略与字段类型解耦。同一个@DateTimeFormat注解,可以同时作用于Date、Calendar、Long等不同类型字段,具体策略由工厂按类型分发。
4.3 DateTimeFormatAnnotationFormatterFactory:@DateTimeFormat 的落地实现
- 类全路径:
org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory
该类是AnnotationFormatterFactory<DateTimeFormat>的经典实现,其类图如下(见 Spring-DateTimeFormatAnnotationFormatterFactory.md):
核心源码:
public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport implements AnnotationFormatterFactory<DateTimeFormat> { private static final Set<Class<?>> FIELD_TYPES; @Override public Set<Class<?>> getFieldTypes() { return FIELD_TYPES; } @Override public Printer<?> getPrinter(DateTimeFormat annotation, Class<?> fieldType) { return getFormatter(annotation, fieldType); } @Override public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) { return getFormatter(annotation, fieldType); } 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; } static { Set<Class<?>> fieldTypes = new HashSet<>(4); // 加入字段类型 fieldTypes.add(Date.class); fieldTypes.add(Calendar.class); fieldTypes.add(Long.class); FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); } }这段实现里有几个值得深挖的设计点:
- Printer 与 Parser 复用同一个 Formatter:
getPrinter与getParser都返回getFormatter(...)得到的DateFormatter——因为DateFormatter implements Formatter<Date>,同时具备print与parse能力,所以输出与输入天然对称; - 三要素组装顺序:
style→iso→pattern。三者优先级由DateFormatter内部逻辑决定:stylePattern(如S-、M-等 JDK 风格码)、iso(ISO 标准格式,如DATE、DATE_TIME)、pattern(自定义模式串,如yyyy-MM-dd HH:mm:ss); - 占位符解析:
resolveEmbeddedValue(...)来自父类EmbeddedValueResolutionSupport,意味着注解上的style与pattern支持写成占位符(如@DateTimeFormat(pattern = "${date.format.pattern}")),由容器解析属性值后再注入,实现格式化模式的外部化配置; - 支持字段类型固定为三类:
Date、Calendar、Long(时间戳毫秒值),且封装为不可变集合(Collections.unmodifiableSet)防止运行时被篡改。
4.4 Joda-Time 体系下的 Parser 与 Printer 实现
除 JDK 日期体系外,Spring 还为 Joda-Time 提供了独立的格式化组件,仓库中记录了其中两个典型实现:
DateTimeParser:字符串 → Joda DateTime
- 类全路径:
org.springframework.format.datetime.joda.DateTimeParser
public final class DateTimeParser implements Parser<DateTime> { private final DateTimeFormatter formatter; /** * Create a new DateTimeParser. * @param formatter the Joda DateTimeFormatter instance */ public DateTimeParser(DateTimeFormatter formatter) { this.formatter = formatter; } @Override public DateTime parse(String text, Locale locale) throws ParseException { // DateTimeFormatter 转换字符串为时间类型 return JodaTimeContextHolder.getFormatter(this.formatter, locale).parseDateTime(text); } }要点(详见 Spring-DateTimeParser.md):
- 构造时注入一个 Joda 的
DateTimeFormatter,采用组合而非继承的方式复用 Joda 的解析能力; parse阶段通过JodaTimeContextHolder.getFormatter(this.formatter, locale)获取与当前线程 Local 上下文绑定的格式化器,再调用parseDateTime(text)完成解析。JodaTimeContextHolder是 Spring 提供的 ThreadLocal 载体,允许在持有 JodaDateTimeFormatter的同时按请求动态覆盖格式化规则;- 由于持有了
ParseException的抛出能力,可直接对接java.text体系的异常约定。
MillisecondInstantPrinter:毫秒时间戳 → 字符串
- 类全路径:
org.springframework.format.datetime.joda.MillisecondInstantPrinter
public final class MillisecondInstantPrinter implements Printer<Long> { private final DateTimeFormatter formatter; /** * Create a new ReadableInstantPrinter. * @param formatter the Joda DateTimeFormatter instance */ public MillisecondInstantPrinter(DateTimeFormatter formatter) { this.formatter = formatter; } @Override public String print(Long instant, Locale locale) { // DateTimeFormatter.print return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(instant); } }要点(详见 Spring-MillisecondInstantPrinter.md):
- 泛型参数是
Long,说明该 Printer 专门处理毫秒级时间戳字段的展示; print直接委托给 JodaDateTimeFormatter.print(Long)完成毫秒值 → 格式化文本的转换;- 与
DateTimeParser呼应:一个负责把文本解析回DateTime对象,一个负责把Long时间戳打印成文本,二者组合即可覆盖"时间戳字段的展示 + 表单回填"场景。
五、实战:在 Spring 中注册与使用自定义 Formatter
结合上述源码脉络,一个可落地的实践路径如下(以注册一个自定义日期 Formatter 为例):
- 实现
Formatter<T>接口(或复用框架自带的DateFormatter),保证print与parse使用同一套格式模式; - 通过
FormatterRegistry注册:在配置类中实现WebMvcConfigurer#addFormatters(FormatterRegistry registry),调用registry.addFormatter(new DateFormatter("yyyy-MM-dd"))或registry.addFormatterForFieldType(...); - 基于注解的字段格式化:为实体字段标注
@DateTimeFormat(pattern = "yyyy-MM-dd"),Spring 会在数据绑定阶段通过DateTimeFormatAnnotationFormatterFactory按Date/Calendar/Long字段类型分发到对应的DateFormatter,完成请求参数到领域对象的自动解析,以及渲染阶段的自动格式化; - 自定义注解扩展:若需要全新的格式化注解,可实现自己的
AnnotationFormatterFactory<A>,实现getFieldTypes/getPrinter/getParser三个方法后注册,即可获得与@DateTimeFormat完全一致的分发机制。
需要说明的是:上述注册与绑定机制属于 Spring 框架的标准用法,本仓库的源码笔记聚焦于接口定义与工厂实现本身,读者可结合 Spring 官方文档与框架源码进一步验证调用链。
六、小结
从 Spring-Formatter.md 出发,我们可以看到 Spring 格式化 SPI 的完整设计层次:
| 层次 | 组件 | 职责 |
|---|---|---|
| 双向统一接口 | Formatter<T> | 组合Printer+Parser,一次实现双向格式化 |
| 单向能力接口 | Printer<T>/Parser<T> | 分别负责输出(对象→字符串)与输入(字符串→对象),均为@FunctionalInterface |
| 注解分发工厂 | AnnotationFormatterFactory<A> | 按注解 + 字段类型返回对应的 Printer/Parser,支持类型 coercion |
| 具体实现 | DateFormatter、DateTimeParser、MillisecondInstantPrinter等 | 落地 JDK 日期与 Joda-Time 两套时间体系的格式化逻辑 |
这套 SPI 的价值在于:格式化关注点从类型转换中独立出来,并与 Locale、注解、字段类型深度绑定。无论是@DateTimeFormat驱动的日期格式化,还是自定义注解驱动的业务字段格式化,最终都收敛到Formatter这一个统一接口上。理解了这个接口及其工厂分发机制,就把握住了 Spring MVC 表单绑定、参数解析与展示渲染中"格式化"这条主线的核心枢纽。
延伸阅读:本仓库docs/Spring/clazz/format/目录下还收录了完整的格式化体系笔记,包括 AnnotationFormatterFactory 接口解析、DateTimeFormatAnnotationFormatterFactory 工厂实现、DateTimeParser 解析器 与 MillisecondInstantPrinter 打印器,可配合本文对照阅读。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考