✍前言

你好,我是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. Autofac 动态获取对象静态类获取对象

    Autofac 从容器中获取对象 静态类或Service场景可以动态,可以直接动态获取对象 /// <summary> /// 从容器中获取对象 /// </summary> ...

  2. CSS基础-边框

    border border-top设置上边界 border-bottom / border-left / border-right 同理 可以为每一条边设置 : border-top-width宽度 ...

  3. 题解 CF1426E - Rock, Paper, Scissors

    一眼题. 第一问很简单吧,就是每个 \(\tt Alice\) 能赢的都尽量让他赢. 第二问很简单吧,就是让 \(\tt Alice\) 输的或平局的尽量多,于是跑个网络最大流.\(1 - 3\) 的 ...

  4. P5838 [USACO19DEC]Milk Visits G

    发现是一道比较裸的树上莫队,于是就开始刚,然后发现好像是最难的一道题--(本题解用于作者加深算法理解,也欢迎各位的阅读) 题意 给你一棵树,树有点权,询问一条路径上是否有点权为 \(c\) 的点. 题 ...

  5. fabric智能合约模板

    以创建用户为例,我觉得基本都是这个框架,用特殊功能直接再往上加,欢迎留言交流 func createUser(stub shim.ChaincodeStubInterface, args []stri ...

  6. Typora快捷使用方式

    快捷使用: 1.一级标题 # + 空格 + 内容 2.六级标题 # + 空格 + 内容 3.有序序号 1. + 空格 + 内容 4.无序序号 -+ 空格.*+空格.++空格 5.代码块 ```pyth ...

  7. 电脑获取手机app内的scheme

    做app开发,有时需要跳转打开外部的app应用,来促成引流或者分享等,这个时候就需要通过scheme跳转协议来完成. 使用scheme跳转外部app,就需要配置对应app的scheme,那这个sche ...

  8. react项目中的一些配置

    react中事件优化使用babel插件 npm install babel-plugin-react-scope-binding --save-dev react中绝对路径引入文件:在根目录下增加js ...

  9. Vue3源码解析(computed-计算属性)

    作者:秦志英 前言 上一篇文章中我们分析了Vue3响应式的整个流程,本篇文章我们将分析Vue3中的computed计算属性是如何实现的. 在Vue2中我们已经对计算属性了解的很清楚了,在Vue3中提供 ...

  10. 02-Dockerfile的基本使用

    1. FROM 作用:指定基础镜像 使用:FROM 镜像名 demo: FROM mysql FROM mysql:5.6 2. RUN 作用:指令是用来执行命令行命令的 使用: shell格式:RU ...