news 2026/9/13 17:36:13

Spring Formatter 格式化 SPI 源码解析:Printer、Parser 与注解驱动的日期格式化体系

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Formatter 格式化 SPI 源码解析:Printer、Parser 与注解驱动的日期格式化体系

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(本地化)相关的、面向人机交互的字符串往返。一个Formatterprintparse配对封装,保证"打印出来是什么格式,就能按什么格式解析回去",避免了双向格式不一致的问题。

一个典型的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/1212/09/2026、数字千分位分隔符1,234.561.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提示信息的基础;
  • PrinterParser一"出"一"入",共同构成Formatter<T>的全部语义。

四、从接口到实现:Formatter 体系的典型实现与注解驱动

4.1 体系中的两类实现路径

从源码结构看,Spring 的 format 包下围绕Formatter存在两条典型的实现路径:

  1. 直接实现Formatter<T>:如DateFormatter,一次实现双向能力,通过FormatterRegistry.addFormatter(...)注册后即可在数据绑定中使用;
  2. 通过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 接收的类型TfieldType不可赋值,Spring 会在调用 Printer 前先做一次从fieldTypeT的类型强制转换(coercion);
  • getParser(A annotation, Class<?> fieldType):同理返回Parser。若 Parser 返回的对象不可赋值给fieldType,在字段赋值前也会尝试一次 coercion。

这套设计的价值在于:格式化策略与字段类型解耦。同一个@DateTimeFormat注解,可以同时作用于DateCalendarLong等不同类型字段,具体策略由工厂按类型分发。

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

这段实现里有几个值得深挖的设计点:

  1. Printer 与 Parser 复用同一个 FormattergetPrintergetParser都返回getFormatter(...)得到的DateFormatter——因为DateFormatter implements Formatter<Date>,同时具备printparse能力,所以输出与输入天然对称;
  2. 三要素组装顺序styleisopattern。三者优先级由DateFormatter内部逻辑决定:stylePattern(如S-M-等 JDK 风格码)、iso(ISO 标准格式,如DATEDATE_TIME)、pattern(自定义模式串,如yyyy-MM-dd HH:mm:ss);
  3. 占位符解析resolveEmbeddedValue(...)来自父类EmbeddedValueResolutionSupport,意味着注解上的stylepattern支持写成占位符(如@DateTimeFormat(pattern = "${date.format.pattern}")),由容器解析属性值后再注入,实现格式化模式的外部化配置;
  4. 支持字段类型固定为三类DateCalendarLong(时间戳毫秒值),且封装为不可变集合(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 为例):

  1. 实现Formatter<T>接口(或复用框架自带的DateFormatter),保证printparse使用同一套格式模式;
  2. 通过FormatterRegistry注册:在配置类中实现WebMvcConfigurer#addFormatters(FormatterRegistry registry),调用registry.addFormatter(new DateFormatter("yyyy-MM-dd"))registry.addFormatterForFieldType(...)
  3. 基于注解的字段格式化:为实体字段标注@DateTimeFormat(pattern = "yyyy-MM-dd"),Spring 会在数据绑定阶段通过DateTimeFormatAnnotationFormatterFactoryDate/Calendar/Long字段类型分发到对应的DateFormatter,完成请求参数到领域对象的自动解析,以及渲染阶段的自动格式化;
  4. 自定义注解扩展:若需要全新的格式化注解,可实现自己的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
具体实现DateFormatterDateTimeParserMillisecondInstantPrinter落地 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:36:11

高效PPT制作:5类必备模板工具与实用技巧

1. 为什么我们需要PPT模板工具&#xff1f;做PPT这件事&#xff0c;估计是每个职场人的噩梦。明明内容都准备好了&#xff0c;却要花大把时间在排版设计上。我见过太多同事为了调一个色块的位置折腾半小时&#xff0c;也见过不少人在deadline前熬夜改格式。其实PPT制作完全可以…

作者头像 李华
网站建设 2026/9/13 17:32:14

全网 Service 不通与网络转发丢包排查周度合辑

全网 Service 不通与网络转发丢包排查周度合辑在第二周的 Kubernetes 生产排障实录中&#xff0c;我们针对全网跨集群、跨节点通信中出现的几起典型疑难网络故障进行了地毯式的排查与彻底整改。 从 CoreDNS 偶发 5 秒延迟与 conntrack 竞态丢包&#xff0c;到 IPVS 模式下 UDP …

作者头像 李华