ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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.Formatterpublic interface FormatterT extends PrinterT, ParserT { }接口体是空的它本身不声明任何新方法而是通过继承把两条能力线合并到了一起父接口方向核心方法职责PrinterT对象 → 字符串输出/展示String print(T object, Locale locale)将领域对象格式化为可供界面展示的文本ParserT字符串 → 对象输入/回填T parse(String text, Locale locale) throws ParseException将用户提交的文本解析回领域对象从设计上看FormatterT是 Spring 在ConversionService类型转换体系之上抽象出的面向展示层的格式化组件类型转换Conversion关心的是任意类型之间的转换而格式化Format关心的是与 Locale本地化相关的、面向人机交互的字符串往返。一个Formatter把print与parse配对封装保证打印出来是什么格式就能按什么格式解析回去避免了双向格式不一致的问题。一个典型的Formatter实现示例public class DateFormatter implements FormatterDate { 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 PrinterT { /** * 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,56Printer把地区信息显式交给实现者处理返回的String是面向显示的文本不保证能够被机器无损还原但配合对称的Parser可以做到格式化与解析闭环。原文档中的类图见 Spring-Parser.md展示了该体系在 Spring 中的完整类图结构三、Parser字符串转对象输入方向类全路径org.springframework.format.Parser类作用将字符串转换成 Java 对象FunctionalInterface public interface ParserT { /** * 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一出一入共同构成FormatterT的全部语义。四、从接口到实现Formatter 体系的典型实现与注解驱动4.1 体系中的两类实现路径从源码结构看Spring 的 format 包下围绕Formatter存在两条典型的实现路径直接实现FormatterT如DateFormatter一次实现双向能力通过FormatterRegistry.addFormatter(...)注册后即可在数据绑定中使用通过AnnotationFormatterFactoryA按注解分发根据字段上标注的注解如DateTimeFormat与字段类型动态返回对应的Printer/Parser实现一个注解覆盖多种字段类型的格式化策略典型代表是DateTimeFormatAnnotationFormatterFactory。4.2 AnnotationFormatterFactory注解驱动的工厂接口类全路径org.springframework.format.AnnotationFormatterFactorypublic interface AnnotationFormatterFactoryA extends Annotation { /** * The types of fields that may be annotated with the A annotation. * 字段类型 */ SetClass? 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.mdgetFieldTypes()声明哪些字段类型可以被注解A标注。工厂注册后Spring 只会对该集合内的字段类型生效getPrinter(A annotation, Class? fieldType)根据注解实例与被标注字段的类型返回对应的Printer。若返回的 Printer 接收的类型T与fieldType不可赋值Spring 会在调用 Printer 前先做一次从fieldType到T的类型强制转换coerciongetParser(A annotation, Class? fieldType)同理返回Parser。若 Parser 返回的对象不可赋值给fieldType在字段赋值前也会尝试一次 coercion。这套设计的价值在于格式化策略与字段类型解耦。同一个DateTimeFormat注解可以同时作用于Date、Calendar、Long等不同类型字段具体策略由工厂按类型分发。4.3 DateTimeFormatAnnotationFormatterFactoryDateTimeFormat 的落地实现类全路径org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory该类是AnnotationFormatterFactoryDateTimeFormat的经典实现其类图如下见 Spring-DateTimeFormatAnnotationFormatterFactory.md核心源码public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport implements AnnotationFormatterFactoryDateTimeFormat { private static final SetClass? FIELD_TYPES; Override public SetClass? 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 FormatterDate 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 { SetClass? fieldTypes new HashSet(4); // 加入字段类型 fieldTypes.add(Date.class); fieldTypes.add(Calendar.class); fieldTypes.add(Long.class); FIELD_TYPES Collections.unmodifiableSet(fieldTypes); } }这段实现里有几个值得深挖的设计点Printer 与 Parser 复用同一个 FormattergetPrinter与getParser都返回getFormatter(...)得到的DateFormatter——因为DateFormatter implements FormatterDate同时具备print与parse能力所以输出与输入天然对称三要素组装顺序style→iso→pattern。三者优先级由DateFormatter内部逻辑决定stylePattern如S-、M-等 JDK 风格码、isoISO 标准格式如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.DateTimeParserpublic final class DateTimeParser implements ParserDateTime { 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.MillisecondInstantPrinterpublic final class MillisecondInstantPrinter implements PrinterLong { 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 为例实现FormatterT接口或复用框架自带的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完成请求参数到领域对象的自动解析以及渲染阶段的自动格式化自定义注解扩展若需要全新的格式化注解可实现自己的AnnotationFormatterFactoryA实现getFieldTypes/getPrinter/getParser三个方法后注册即可获得与DateTimeFormat完全一致的分发机制。需要说明的是上述注册与绑定机制属于 Spring 框架的标准用法本仓库的源码笔记聚焦于接口定义与工厂实现本身读者可结合 Spring 官方文档与框架源码进一步验证调用链。六、小结从 Spring-Formatter.md 出发我们可以看到 Spring 格式化 SPI 的完整设计层次层次组件职责双向统一接口FormatterT组合PrinterParser一次实现双向格式化单向能力接口PrinterT/ParserT分别负责输出对象→字符串与输入字符串→对象均为FunctionalInterface注解分发工厂AnnotationFormatterFactoryA按注解 字段类型返回对应的 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),仅供参考
返回列表