✍前言

你好,我是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:

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

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

DateFormatter:

	@Override
public String print(Date date, Locale locale) {
return getDateFormat(locale).format(date);
} @Override
public 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> {

	@Override
public Month parse(String text, Locale locale) throws ParseException {
return Month.valueOf(text.toUpperCase());
}
@Override
public 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:

	@Nullable
private String pattern;
@Nullable
private ISO iso;
@Nullable
private FormatStyle dateStyle;
@Nullable
private FormatStyle timeStyle;
@Nullable
private 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> {
...
@Override
public String print(Number number, Locale locale) {
return getNumberFormat(locale).format(number);
} @Override
public 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:

	@Override
public 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.045
Number parsedResult = formatter.parse("1220.045", Locale.getDefault());
System.out.println(parsedResult.getClass() + "-->" + parsedResult);
}

运行程序,输出:

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

百分数格式化

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

PercentStyleFormatter:

	@Override
protected 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.045
Number 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()));
// 指定所需的分数位数。默认是2
formatter.setFractionDigits(1);
// 舍入模式。默认是RoundingMode#UNNECESSARY
formatter.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) 原创文章,未经作者允许不得转载,谢谢合作。

推荐阅读

8. 格式化器大一统 -- Spring的Formatter抽象的更多相关文章

  1. 消息队列写入内容后,读出来的自动包裹了<string>标签,自定义格式化器解决该issue

    /// <summary> /// 该格式化器使输入即输出 /// </summary> public class StringFormatter : IMessageForm ...

  2. Asp.Net Web API 2第十二课——Media Formatters媒体格式化器

    前言 阅读本文之前,您也可以到Asp.Net Web API 2 系列导航进行查看 http://www.cnblogs.com/aehyok/p/3446289.html 本教程演示如何在ASP.N ...

  3. 【ASP.NET Web API教程】6.1 媒体格式化器

    http://www.cnblogs.com/r01cn/archive/2013/05/17/3083400.html 6.1 Media Formatters6.1 媒体格式化器 本文引自:htt ...

  4. Media Formatters媒体格式化器

    Media Formatters媒体格式化器 前言 阅读本文之前,您也可以到Asp.Net Web API 2 系列导航进行查看 http://www.cnblogs.com/aehyok/p/344 ...

  5. Media Formatters(媒体格式化器)

    6.1.1 Internet的媒体类型 媒体类型,也叫做MIME类型,标识了数据的格式.在HTTP中,媒体类型描述了消息体的格式.一个媒体类型由两个字符串组成:类型和子类型.例如: text/html ...

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

    导航 目   录:基于NPOI的报表引擎——ExcelReport 上一篇:ExcelReport源码解析 概述 上篇中已介绍了ExcelRepor的架构,本篇将通过例子讲述如何扩展元素格式化器以满足 ...

  7. Ⅷ.spring的点点滴滴--抽象对象和子对象

    承接上文 抽象对象和子对象 .net篇(环境为vs2012+Spring.Core.dll v1.31) public class parent { public string Name { get; ...

  8. Log4j扩展使用--日志格式化器Layout

    Layout:格式化输出日志信息 OK,前面我已经知道了.Appender必须使用一个与之相关联的Layout,这样才能知道怎样格式化输出日志信息. 日志格式化器Layout负责格式化日志信息,方法l ...

  9. 拦截器及 Spring MVC 整合

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

随机推荐

  1. 稀疏矩阵三元组表快速转置(C语言实现)

    本来准备昨天下午写的,但是因为去参加360众测靶场的考核耽搁了,靶场的题目还是挺基础的. 继续学习吧. 使用黑色墨水在白纸上签名就像由像素点构成的稀疏矩阵.如图4所示. 图4 手写体签名 [问题]请将 ...

  2. scrapy爬虫爬取小姐姐图片(不羞涩)

    这个爬虫主要学习scrapy的item Pipeline 是时候搬出这张图了: 当我们要使用item Pipeline的时候,要现在settings里面取消这几行的注释 我们可以自定义Item Pip ...

  3. Symbol类型是不可枚举的

    const info = { [Symbol('a')]: 'b' } console.log(info)//{Symbol('a'): 'b'} console.log(Object.keys(in ...

  4. 项目实战:Qt多通道数据采集系统(通道配置、电压转换、采样频率、通道补偿值、定时采集、导出exel和图表、自动XY轴、隐藏XY轴、实时隐藏显示通道)

    需求   1.通道使能.选择.更改通道名称.设置显示颜色  2.采样率可设置(Sa/s/chj)  3.单位换算,按照给定的进行换算  4.对通道可进行设置补偿值  5.通道取消可动态显示和隐藏,并可 ...

  5. ORCHARD WOODEN GATE

    狗: 代码小盒子 爆零秘籍 备忘录 任务计划 核心算法: 搜索/枚举/贪心 dp 分治 数据结构: 并查集 ST表 堆 线段树 树状数组 分块 树套树 平衡树 LCT 莫队 字符串: 哈希 Trie ...

  6. MariaDB的安装及相关配置

    MariaDB的安装及相关配置 安装 yum -y install mariadb mariadb-server 安装完成MariaDB,首先启动MariaDB systemctl start mar ...

  7. STL——容器(List)list 数据的存取

    list.front(); //返回第一个元素 list.back(); //返回最后一个元素 1 #include <iostream> 2 #include <list> ...

  8. Jmeter对数据库做压力测试

    一.环境:apache-jmeter-5.0,Oracle11g.windows7.jdk1.8.ojdbc14-10.2.0.2.0.jar二.操作配置:2.1.启动Jmeter Jmeter初始化 ...

  9. vue+axois 封装请求+拦截器(请求锁+统一错误)

     需求 封装常用请求 拦截器-请求锁 统一处理错误码 一.封装常用的请求 解决痛点:不要每一个模块的api都还要写get,post,patch请求方法.直接将这些常用的方法封装好. 解决方案:写一个类 ...

  10. Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!

    Spring Cloud 2020.0.0 没错,Spring Cloud 2020.0.0 正式发布了: 感谢Java技术栈群友通知,想入群的在公众号Java技术栈后台回复:wx,正在使用 Spri ...