你好,我是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模块的目录结构安排:
1 | public interface Formatter<T> extends Printer<T>, Parser<T> { |
可以看到,该接口本身没有任何方法,而是聚合了另外两个接口Printer和Parser。
Printer&Parser
这两个接口是相反功能的接口。
Printer
:格式化显示(输出)接口。将T类型转为String形式,Locale用于控制国际化1
2
3
4
5
public interface Printer<T> {
// 将Object写为String类型
String print(T object, Locale locale);
}Parser
:解析接口。将String类型转到T类型,Locale用于控制国际化。1
2
3
4
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
1 | // @since 3.0 |
默认使用的TimeZone是UTC标准时区,ISO_PATTERNS
代表ISO标准模版,这和@DateTimeFormat
注解的iso属性是一一对应的。也就是说如果你不想指定pattern,可以快速通过指定ISO来实现。
另外,对于格式化器来说有这些属性你都可以自由去定制:
1 | DateFormatter: |
它对Formatter接口方法的实现如下:
1 | DateFormatter: |
可以看到不管输入还是输出,底层依赖的都是JDK的java.text.DateFormat
(实际为SimpleDateFormat),现在知道为毛上篇文章要先讲JDK的格式化体系做铺垫了吧,万变不离其宗。
因此可以认为,Spring为此做的事情的核心,只不过是写了个根据Locale、pattern、IOS等参数生成DateFormat
实例的逻辑而已,属于应用层面的封装。也就是需要知晓getDateFormat()
方法的逻辑,此部分逻辑绘制成图如下:
因此:pattern、iso、stylePattern它们的优先级谁先谁后,一看便知。
代码示例
1 |
|
运行程序,输出:
1 | 默认输出格式:2020-12-26 |
注意: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等,且实现方式都是趋同的:
1 | class MonthFormatter implements Formatter<Month> { |
这里以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一样,它支持如下属性方便你直接定制:
1 | DateTimeFormatterFactory: |
优先级关系二者是一致的:
- pattern
- iso
- dateStyle/timeStyle
说明:一致的设计,可以给与开发者近乎一致的编程体验,毕竟JSR 310和Date表示的都是时间日期,尽量保持一致性是一种很人性化的设计考量。
- DateTimeFormatterFactoryBean
顾名思义,DateTimeFormatterFactory用于生成一个DateTimeFormatter实例,而本类用于把生成的Bean放进IoC容器内,完成和Spring容器的整合。客气的是,它直接继承自DateTimeFormatterFactory,从而自己同时就具备这两项能力:
- 生成DateTimeFormatter实例
- 将该实例放进IoC容器
多说一句:虽然这个工厂Bean非常简单,但是它释放的信号可以作为编程指导:
- 一个应用内,对日期、时间的格式化尽量只存在1种模版规范。比如我们可以向IoC容器里扔进去一个模版,需要时注入进来使用即可
- 注意:这里指的应用内,一般不包含协议转换层使用的模版规范。如Http协议层可以使用自己单独的一套转换模版机制
- 日期时间模版不要在每次使用时去临时创建,而是集中统一创建好管理起来(比如放IoC容器内),这样维护起来方便很多
说明:
DateTimeFormatterFactoryBean
这个API在Spring内部并未使用,这是Spring专门给使用者用的,因为Spring也希望你这么去做从而把日期时间格式化模版管理起来
代码示例
1 |
|
运行程序,输出:
1 | 2020-12-26 22:44:44 |
说明:虽然你也可以直接使用DateTimeFormatter#ofPattern()
静态方法得到一个实例,但是 若在Spring环境下使用它我还是建议使用Spring提供的工厂类来创建,这样能保证统一的编程体验,B格也稍微高点。
使用建议:以后对日期时间类型(包括JSR310类型)就不要自己去写原生的SimpleDateFormat/DateTimeFormatter
了,建议可以用Spring包装过的DateFormatter/DateTimeFormatterFactory
,使用体验更佳。
数字格式化
通过了上篇文章的学习之后,对数字的格式化就一点也不陌生了,什么数字、百分数、钱币等都属于数字的范畴。Spring提供了AbstractNumberFormatter
抽象来专门处理数字格式化议题:
1 | public abstract class AbstractNumberFormatter implements Formatter<Number> { |
这和DateFormatter
的实现模式何其相似,简直一模一样:底层实现依赖于(委托给)java.text.NumberFormat
去完成。
此抽象类共有三个具体实现:
- NumberStyleFormatter:数字格式化,如小数,分组等
- PercentStyleFormatter:百分数格式化
- CurrencyStyleFormatter:钱币格式化
数字格式化
NumberStyleFormatter
使用NumberFormat的数字样式的通用数字格式化程序。可定制化参数为:pattern。核心源码如下:
1 | NumberStyleFormatter: |
代码示例:
1 |
|
运行程序,输出:
1 | 1,220.045 |
- 可通过setPattern()指定数字格式化的模版(一般建议显示指定)
- parse()方法返回的是
BigDecimal
类型,从而保证了数字精度
百分数格式化
PercentStyleFormatter
表示使用百分比样式去格式化数字。核心源码(其实是全部源码)如下:
1 | PercentStyleFormatter: |
这个就更简单啦,pattern模版都不需要指定。代码示例:
1 |
|
运行程序,输出:
1 | 122,005% |
百分数的格式化不能指定pattern,差评。
钱币格式化
使用钱币样式格式化数字,使用java.util.Currency
来描述货币。代码示例:
1 |
|
运行程序,输出:
1 | ¥1,220.05 |
值得关注的是:这三个实现在Spring 4.2版本之前是“耦合”在一起。直到4.2才拆开,职责分离。
✍总结
本文介绍了Spring的Formatter抽象,让格式化器大一统。这就是Spring最强能力:API设计、抽象、大一统。
Converter可以从任意源类型,转换为任意目标类型。而Formatter则是从String类型转换为任务目标类型,有点类似PropertyEditor。可以感觉出Converter是Formater的超集,实际上在Spring中Formatter是被拆解成PrinterConverter和ParserConverter,然后再注册到ConverterRegistry,供后续使用。
关于格式化器的注册中心、注册员,这就是下篇文章内容喽,欢迎保持持续关注。
♨本文思考题♨
看完了不一定懂,看懂了不一定记住,记住了不一定掌握。来,文末3个思考题帮你复盘:
- Spring为何没有针对JSR310时间类型提供专用转换器实现?
- Spring内建众多Formatter实现,如何管理?
- 格式化器Formatter和转换器Converter是如何整合到一起的?
♚声明♚
本文所属专栏:Spring类型转换,公号后台回复专栏名即可获取全部内容。
分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字
专栏
有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。
本文是 A哥(YourBatman) 原创文章,未经作者允许不得转载,谢谢合作。
☀推荐阅读☀
- ……
- 5. 穿过拥挤的人潮,Spring已为你制作好高级赛道
- 6. 抹平差异,统一类型转换服务ConversionService
- 7. JDK拍了拍你:字符串拼接一定记得用MessageFormat#format
- ……
关注YourBatman
Author | A哥(YourBatman) |
---|---|
个人站点 | www.yourbatman.cn |
yourbatman@qq.com | |
微 信 | fsx1056342982 |
活跃平台 |
|
公众号 | BAT的乌托邦(ID:BAT-utopia) |
知识星球 | BAT的乌托邦 |
每日文章推荐 | 每日文章推荐 |