目录

    • 本文提纲
    • 版本约定
  • ✍正文
    • Printer&Parser
    • Formatter
      • 时间日期格式化
        • Date类型
          • 代码示例
        • JSR 310类型
          • 整合DateTimeFormatter
          • 代码示例
      • 数字格式化
        • 数字格式化
        • 百分数格式化
        • 钱币格式化
  • ✍总结
    • ♨本文思考题♨
    • ♚声明♚
    • ☀推荐阅读☀

你好,我是A哥(YourBatman)。

上篇文章 介绍了java.text.Format格式化体系,作为JDK 1.0就提供的格式化器,除了设计上存在一定缺陷,过于底层无法标准化对使用者不够友好,这都是对格式化器提出的更高要求。Spring作为Java开发的标准基建,本文就来看看它做了哪些补充。

本文提纲

版本约定

  • Spring Framework:5.3.x
  • Spring Boot:2.4.x

✍正文

在应用中(特别是web应用),我们经常需要将前端/Client端传入的字符串转换成指定格式/指定数据类型,同样的服务端也希望能把指定类型的数据按照指定格式 返回给前端/Client端,这种情况下Converter已经无法满足我们的需求了。为此,Spring提供了格式化模块专门用于解决此类问题。

首先可以从宏观上先看看spring-context对format模块的目录结构安排:

public interface Formatter<T> extends Printer<T>, Parser<T> {}

可以看到,该接口本身没有任何方法,而是聚合了另外两个接口Printer和Parser。

Printer&Parser

这两个接口是相反功能的接口。

  • Printer:格式化显示(输出)接口。将T类型转为String形式,Locale用于控制国际化
@FunctionalInterface
public interface Printer<T> {// 将Object写为String类型String print(T object, Locale locale);
}
  • Parser:解析接口。将String类型转到T类型,Locale用于控制国际化。
@FunctionalInterface
public interface Parser<T> {T parse(String text, Locale locale) throws ParseException;
}

Formatter

格式化器接口,它的继承树如下:

由图可见,格式化动作只需关心到两个领域:

  • 时间日期领域
  • 数字领域(其中包括货币)

时间日期格式化

Spring框架从4.0开始支持Java 8,针对JSR 310日期时间类型的格式化专门有个包org.springframework.format.datetime.standard

值得一提的是:在Java 8出来之前,Joda-Time是Java日期时间处理最好的解决方案,使用广泛,甚至得到了Spring内置的支持。现在Java 8已然成为主流,JSR 310日期时间API 完全可以 代替Joda-Time(JSR 310的贡献者其实就是Joda-Time的作者们)。因此joda库也逐渐告别历史舞台,后续代码中不再推荐使用,本文也会选择性忽略。

除了Joda-Time外,Java中对时间日期的格式化还需分为这两大阵营来处理:

Date类型

虽然已经2020年了(Java 8于2014年发布),但谈到时间日期那必然还是得有java.util.Date,毕竟积重难返。所以呢,Spring提供了DateFormatter用于支持它的格式化。

因为Date早就存在,所以DateFormatter是伴随着Formatter的出现而出现,@since 3.0

// @since 3.0
public class DateFormatter implements Formatter<Date> {private static final TimeZone UTC = TimeZone.getTimeZone("UTC");private static final Map<ISO, String> ISO_PATTERNS;static {Map<ISO, String> formats = new EnumMap<>(ISO.class);formats.put(ISO.DATE, "yyyy-MM-dd");formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");ISO_PATTERNS = Collections.unmodifiableMap(formats);}
}

默认使用的TimeZone是UTC标准时区,ISO_PATTERNS代表ISO标准模版,这和@DateTimeFormat注解的iso属性是一一对应的。也就是说如果你不想指定pattern,可以快速通过指定ISO来实现。

另外,对于格式化器来说有这些属性你都可以自由去定制:

DateFormatter:@Nullableprivate String pattern;private int style = DateFormat.DEFAULT;@Nullableprivate String stylePattern;@Nullableprivate ISO iso;@Nullableprivate TimeZone timeZone;

它对Formatter接口方法的实现如下:

DateFormatter:@Overridepublic String print(Date date, Locale locale) {return getDateFormat(locale).format(date);}@Overridepublic Date parse(String text, Locale locale) throws ParseException {return getDateFormat(locale).parse(text);}// 根据pattern、ISO等等得到一个DateFormat实例protected DateFormat getDateFormat(Locale locale) { ... }

可以看到不管输入还是输出,底层依赖的都是JDK的java.text.DateFormat(实际为SimpleDateFormat),现在知道为毛上篇文章要先讲JDK的格式化体系做铺垫了吧,万变不离其宗。

因此可以认为,Spring为此做的事情的核心,只不过是写了个根据Locale、pattern、IOS等参数生成DateFormat实例的逻辑而已,属于应用层面的封装。也就是需要知晓getDateFormat()方法的逻辑,此部分逻辑绘制成图如下:

因此:pattern、iso、stylePattern它们的优先级谁先谁后,一看便知。

代码示例
@Test
public void test1() {DateFormatter formatter = new DateFormatter();Date currDate = new Date();System.out.println("默认输出格式:" + formatter.print(currDate, Locale.CHINA));formatter.setIso(DateTimeFormat.ISO.DATE_TIME);System.out.println("指定ISO输出格式:" + formatter.print(currDate, Locale.CHINA));formatter.setPattern("yyyy-mm-dd HH:mm:ss");System.out.println("指定pattern输出格式:" + formatter.print(currDate, Locale.CHINA));
}

运行程序,输出:

默认输出格式:2020-12-26
指定ISO输出格式:2020-12-26T13:06:52.921Z
指定pattern输出格式:2020-06-26 21:06:52

注意:ISO格式输出的时间,是存在时差问题的,因为它使用的是UTC时间,请稍加注意。

还记得本系列前面介绍的CustomDateEditor这个属性编辑器吗?它也是用于对String -> Date的转化,底层依赖也是JDK的DateFormat,但使用灵活度上没这个自由,已被抛弃/取代。

关于java.util.Date类型的格式化,在此,语重心长的号召一句:如果你是项目,请全项目禁用Date类型吧;如果你是新代码,也请不要再使用Date类型,太拖后腿了。

JSR 310类型

JSR 310日期时间类型是Java8引入的一套全新的时间日期API。新的时间及日期API位于java.time中,此包中的是类是不可变且线程安全的。下面是一些关键类

  • Instant——代表的是时间戳(另外可参考Clock类)
  • LocalDate——不包含具体时间的日期,如2020-12-12。它可以用来存储生日,周年纪念日,入职日期等
  • LocalTime——代表的是不含日期的时间,如18:00:00
  • LocalDateTime——包含了日期及时间,不过没有偏移信息或者说时区
  • ZonedDateTime——包含时区的完整的日期时间还有时区,偏移量是以UTC/格林威治时间为基准的
  • Timezone——时区。在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到

同时还有一些辅助类,如:Year、Month、YearMonth、MonthDay、Duration、Period等等。

从上图Formatter的继承树来看,Spring只提供了一些辅助类的格式化器实现,如MonthFormatter、PeriodFormatter、YearMonthFormatter等,且实现方式都是趋同的:

class MonthFormatter implements Formatter<Month> {@Overridepublic Month parse(String text, Locale locale) throws ParseException {return Month.valueOf(text.toUpperCase());}@Overridepublic String print(Month object, Locale locale) {return object.toString();}}

这里以MonthFormatter为例,其它辅助类的格式化器实现其实基本一样:

那么问题来了:Spring为毛没有给LocalDateTime、LocalDate、LocalTime这种更为常用的类型提供Formatter格式化器呢?

其实是这样的:JDK 8提供的这套日期时间API是非常优秀的,自己就提供了非常好用的java.time.format.DateTimeFormatter格式化器,并且设计、功能上都已经非常完善了。既然如此,Spring并不需要再重复造轮子,而是仅需考虑如何整合此格式化器即可。

整合DateTimeFormatter

为了完成“整合”,把DateTimeFormatter融入到Spring自己的Formatter体系内,Spring准备了多个API用于衔接。

  • DateTimeFormatterFactory

java.time.format.DateTimeFormatter的工厂。和DateFormatter一样,它支持如下属性方便你直接定制:

DateTimeFormatterFactory:@Nullableprivate String pattern;@Nullableprivate ISO iso;@Nullableprivate FormatStyle dateStyle;@Nullableprivate FormatStyle timeStyle;@Nullableprivate TimeZone timeZone;// 根据定制的参数,生成一个DateTimeFormatter实例public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) { ... }


优先级关系二者是一致的:

  • pattern
  • iso
  • dateStyle/timeStyle

说明:一致的设计,可以给与开发者近乎一致的编程体验,毕竟JSR 310和Date表示的都是时间日期,尽量保持一致性是一种很人性化的设计考量。

  • DateTimeFormatterFactoryBean

顾名思义,DateTimeFormatterFactory用于生成一个DateTimeFormatter实例,而本类用于把生成的Bean放进IoC容器内,完成和Spring容器的整合。客气的是,它直接继承自DateTimeFormatterFactory,从而自己同时就具备这两项能力:

  1. 生成DateTimeFormatter实例
  2. 将该实例放进IoC容器

多说一句:虽然这个工厂Bean非常简单,但是它释放的信号可以作为编程指导

  1. 一个应用内,对日期、时间的格式化尽量只存在1种模版规范。比如我们可以向IoC容器里扔进去一个模版,需要时注入进来使用即可

    1. 注意:这里指的应用,一般不包含协议转换层使用的模版规范。如Http协议层可以使用自己单独的一套转换模版机制
  2. 日期时间模版不要在每次使用时去临时创建,而是集中统一创建好管理起来(比如放IoC容器内),这样维护起来方便很多

说明:DateTimeFormatterFactoryBean这个API在Spring内部并未使用,这是Spring专门给使用者用的,因为Spring也希望你这么去做从而把日期时间格式化模版管理起来

代码示例
@Test
public void test1() {// DateTimeFormatterFactory dateTimeFormatterFactory = new DateTimeFormatterFactory();// dateTimeFormatterFactory.setPattern("yyyy-MM-dd HH:mm:ss");// 执行格式化动作System.out.println(new DateTimeFormatterFactory("yyyy-MM-dd HH:mm:ss").createDateTimeFormatter().format(LocalDateTime.now()));System.out.println(new DateTimeFormatterFactory("yyyy-MM-dd").createDateTimeFormatter().format(LocalDate.now()));System.out.println(new DateTimeFormatterFactory("HH:mm:ss").createDateTimeFormatter().format(LocalTime.now()));System.out.println(new DateTimeFormatterFactory("yyyy-MM-dd HH:mm:ss").createDateTimeFormatter().format(ZonedDateTime.now()));
}

运行程序,输出:

2020-12-26 22:44:44
2020-12-26
22:44:44
2020-12-26 22:44:44

说明:虽然你也可以直接使用DateTimeFormatter#ofPattern()静态方法得到一个实例,但是 若在Spring环境下使用它我还是建议使用Spring提供的工厂类来创建,这样能保证统一的编程体验,B格也稍微高点。

使用建议:以后对日期时间类型(包括JSR310类型)就不要自己去写原生的SimpleDateFormat/DateTimeFormatter了,建议可以用Spring包装过的DateFormatter/DateTimeFormatterFactory,使用体验更佳。

数字格式化

通过了上篇文章的学习之后,对数字的格式化就一点也不陌生了,什么数字、百分数、钱币等都属于数字的范畴。Spring提供了AbstractNumberFormatter抽象来专门处理数字格式化议题:

public abstract class AbstractNumberFormatter implements Formatter<Number> {...@Overridepublic String print(Number number, Locale locale) {return getNumberFormat(locale).format(number);}@Overridepublic Number parse(String text, Locale locale) throws ParseException {// 伪代码,核心逻辑就这一句return getNumberFormat.parse(text, new ParsePosition(0));}// 得到一个NumberFormat实例protected abstract NumberFormat getNumberFormat(Locale locale);...
}

这和DateFormatter的实现模式何其相似,简直一模一样:底层实现依赖于(委托给)java.text.NumberFormat去完成。

此抽象类共有三个具体实现:

  • NumberStyleFormatter:数字格式化,如小数,分组等
  • PercentStyleFormatter:百分数格式化
  • CurrencyStyleFormatter:钱币格式化

数字格式化

NumberStyleFormatter使用NumberFormat的数字样式的通用数字格式化程序。可定制化参数为:pattern。核心源码如下:

NumberStyleFormatter:@Overridepublic NumberFormat getNumberFormat(Locale locale) {NumberFormat format = NumberFormat.getInstance(locale);...// 解析时,永远返回BigDecimal类型decimalFormat.setParseBigDecimal(true);// 使用格式化模版if (this.pattern != null) {decimalFormat.applyPattern(this.pattern);}return decimalFormat;}

代码示例:

@Test
public void test2() throws ParseException {NumberStyleFormatter formatter = new NumberStyleFormatter();double myNum = 1220.0455;System.out.println(formatter.print(myNum, Locale.getDefault()));formatter.setPattern("#.##");System.out.println(formatter.print(myNum, Locale.getDefault()));// 转换// Number parsedResult = formatter.parse("1,220.045", Locale.getDefault()); // java.text.ParseException: 1,220.045Number parsedResult = formatter.parse("1220.045", Locale.getDefault());System.out.println(parsedResult.getClass() + "-->" + parsedResult);
}

运行程序,输出:

1,220.045
1220.05class java.math.BigDecimal-->1220.045
  1. 可通过setPattern()指定数字格式化的模版(一般建议显示指定)
  2. parse()方法返回的是BigDecimal类型,从而保证了数字精度

百分数格式化

PercentStyleFormatter表示使用百分比样式去格式化数字。核心源码(其实是全部源码)如下:

PercentStyleFormatter:@Overrideprotected NumberFormat getNumberFormat(Locale locale) {NumberFormat format = NumberFormat.getPercentInstance(locale);if (format instanceof DecimalFormat) {((DecimalFormat) format).setParseBigDecimal(true);}return format;}

这个就更简单啦,pattern模版都不需要指定。代码示例:

@Test
public void test3() throws ParseException {PercentStyleFormatter formatter = new PercentStyleFormatter();double myNum = 1220.0455;System.out.println(formatter.print(myNum, Locale.getDefault()));// 转换// Number parsedResult = formatter.parse("1,220.045", Locale.getDefault()); // java.text.ParseException: 1,220.045Number parsedResult = formatter.parse("122,005%", Locale.getDefault());System.out.println(parsedResult.getClass() + "-->" + parsedResult);
}

运行程序,输出:

122,005%
class java.math.BigDecimal-->1220.05

百分数的格式化不能指定pattern,差评。

钱币格式化

使用钱币样式格式化数字,使用java.util.Currency来描述货币。代码示例:

@Test
public void test3() throws ParseException {CurrencyStyleFormatter formatter = new CurrencyStyleFormatter();double myNum = 1220.0455;System.out.println(formatter.print(myNum, Locale.getDefault()));System.out.println("--------------定制化--------------");// 指定货币种类(如果你知道的话)// formatter.setCurrency(Currency.getInstance(Locale.getDefault()));// 指定所需的分数位数。默认是2formatter.setFractionDigits(1);// 舍入模式。默认是RoundingMode#UNNECESSARYformatter.setRoundingMode(RoundingMode.CEILING);// 格式化数字的模版formatter.setPattern("#.#¤¤");System.out.println(formatter.print(myNum, Locale.getDefault()));// 转换// Number parsedResult = formatter.parse("¥1220.05", Locale.getDefault());Number parsedResult = formatter.parse("1220.1CNY", Locale.getDefault());System.out.println(parsedResult.getClass() + "-->" + parsedResult);
}

运行程序,输出:

¥1,220.05
--------------定制化--------------
1220.1CNY
class java.math.BigDecimal-->1220.1

值得关注的是:这三个实现在Spring 4.2版本之前是“耦合”在一起。直到4.2才拆开,职责分离。

✍总结

本文介绍了Spring的Formatter抽象,让格式化器大一统。这就是Spring最强能力:API设计、抽象、大一统。

Converter可以从任意源类型,转换为任意目标类型。而Formatter则是从String类型转换为任务目标类型,有点类似PropertyEditor。可以感觉出Converter是Formater的超集,实际上在Spring中Formatter是被拆解成PrinterConverter和ParserConverter,然后再注册到ConverterRegistry,供后续使用。

关于格式化器的注册中心、注册员,这就是下篇文章内容喽,欢迎保持持续关注。

♨本文思考题♨

看完了不一定懂,看懂了不一定记住,记住了不一定掌握。来,文末3个思考题帮你复盘:

  1. Spring为何没有针对JSR310时间类型提供专用转换器实现?
  2. Spring内建众多Formatter实现,如何管理?
  3. 格式化器Formatter和转换器Converter是如何整合到一起的?

♚声明♚

本文所属专栏:Spring类型转换,公号后台回复专栏名即可获取全部内容。

分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字专栏有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。

本文是 A哥(YourBatman) 原创文章,未经作者允许不得转载,谢谢合作。

☀推荐阅读☀

  • 5. 穿过拥挤的人潮,Spring已为你制作好高级赛道
  • 6. 抹平差异,统一类型转换服务ConversionService
  • 7. JDK拍了拍你:字符串拼接一定记得用MessageFormat#format

8. 格式化器大一统 -- Spring的Formatter抽象相关推荐

  1. Spring boot格式化器

    格式化器 WebMvcAutoConfiguration 自动配置 dateFormatter 格式化器,格式化日期 getDateFormat 默认日期格式,采用斜线的方式 可以通过配置文件 修改为 ...

  2. 日计不足涓滴成河-自定义响应结果格式化器

    什么是响应结果 响应结果就是,在客户端向服务器发出请求后,服务器根据客户端的请求参数,给出的结果,这就是一个完整的响应结果过程. 响应的结果包含的内容非常多,主要的有 HTTP Status Code ...

  3. 云客Drupal源码分析之实体视图显示及格式化器

    在实体视图构建器中构建完实体的基本渲染数组后,会调用实体视图显示对象继续构建实体字段对象的渲染数组,然后合并到基本数组中(合并过程基本数组的优先级更高)以形成完整实体渲染数组,实体视图显示对象内部又依 ...

  4. Java学习day051 记录日志(基本日志、高级日志、修改日志管理器配置、本地化、处理器、过滤器、格式化器、日志记录说明)

    使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识. day051   记录日志(基本日志.高级日志.修改日志管理器配置.本地化.处理器.过滤器.格式化器 ...

  5. ExcelReport第三篇:扩展元素格式化器

    2019独角兽企业重金招聘Python工程师标准>>> 导航 目   录:基于NPOI的报表引擎--ExcelReport 上一篇:ExcelReport源码解析 概述 上篇中已介绍 ...

  6. 扩展springmvc组件——当页面跳转时,需要在Controller里面创建一个空方法去跳转或者是创建一个配置类  ||日期格式化说明||自定义格式化器||消息转化器扩展fastjson

    在容器中注册视图控制器 当页面跳转时,我们需要在Controller里面创建一个空方法去跳转,那么有没有别的配置方法呢 创建一个WebMvcConfig的配置类   实现WebMvcConfigure ...

  7. Log4j详细介绍(七)----日志格式化器Layout

    日志格式化器Layout负责格式化日志信息.方法log.error()的参数只包含日志信息,利用Layout可以附加其他信息,以输出更多的信息或者布局显示. PatternLayout布局 Patte ...

  8. 第四节:格式化器如何序列化类型实例

    本节要深入讨论一下格式化器如何序列化对象的字段.掌握了这些知识后,可以更容易地理解本章后面要解释的一些更高级的序列化和反序列化技术. 为了简化格式化器的操作,FCL在System.Runtime.Se ...

  9. 拦截器及 Spring MVC 整合

    一.实验介绍 1.1 实验内容 本节课程主要利用 Spring MVC 框架实现拦截器以及 Spring MVC 框架的整合. 1.2 实验知识点 Spring MVC 框架 拦截器 1.3 实验环境 ...

最新文章

  1. element中upload单图片转base64后添加进数组,请求接口
  2. Grpc+Grpc Gateway实践一 介绍与环境安装
  3. (转)CXF学习笔记一:如何创建、发布和访问基于CXF的服务
  4. c++ 多线程 垃圾回收器_7种jvm垃圾回收器,这次全部搞懂
  5. elasticsearch,elasticsearch-service安装
  6. 前端学习(3166):react-hello-react之鼠标移入效果
  7. 前端学习(2750):global全局外观设置
  8. id门禁卡复制到手机_手机NFC有哪些功能?怎么设置手机门禁卡?别浪费了手机的NFC功能...
  9. mysql基本功能+show+innodb+索引+慢sql+explain
  10. 房子值500万租金仅3500元,卖房拿利息和出租哪个合算?
  11. 剑指offer——面试题39:二叉树的深度
  12. 《深度学习》李宏毅 -- task4深度学习介绍和反向传播机制
  13. java代码生成UUID以及在线UUID生成器
  14. Win 10 桌面简单美化(+开始菜单 TileGenie)
  15. 湿空气性质计算,随笔与学习记录 (3.露点温度,绝对湿度)
  16. 世界芯中国芯RISC-V相关资源及进展
  17. [渝粤教育] 西南交通大学 土木工程试验与量测技术 参考 资料
  18. MacBook键盘锁定、按键失效问题
  19. 手把手教您搭建一个跨境电商平台
  20. mysql gtid 同步_结合案例说说5.7使用gtid同步后,mysql.gtid_executed引起的从库gtid断层...

热门文章

  1. tail 上下_鹦鹉的身体语言(下)
  2. VL170的浅应用分享
  3. 聊聊 SQL 注入那些事儿
  4. 【《Real-Time Rendering 3rd》 提炼总结】(四) 第五章 · 图形渲染与视觉外观
  5. 用户自定义文件(word、excel)保存位置
  6. Eureka服务启动定期报错:Batch update failure with HTTP status code 404; discarding XXX replication tasks
  7. steam link 闪屏_Steam Link硬件已死,这是您可以做的
  8. Python直接控制鼠标键盘模块 pyautogui
  9. iOS重磅升级:首次支持后台恢复备份,可直接进入桌面
  10. km itm监控启停命令记录