你好,我是A哥(YourBatman)。前端
上篇文章 介绍了java.text.Format
格式化体系,做为JDK 1.0就提供的格式化器,除了设计上存在必定缺陷,过于底层没法标准化对使用者不够友好,这都是对格式化器提出的更高要求。Spring做为Java开发的标准基建,本文就来看看它作了哪些补充。java
在应用中(特别是web应用),咱们常常须要将前端/Client端传入的字符串转换成指定格式/指定数据类型,一样的服务端也但愿能把指定类型的数据按照指定格式 返回给前端/Client端,这种状况下Converter
已经没法知足咱们的需求了。为此,Spring提供了格式化模块专门用于解决此类问题。git
首先能够从宏观上先看看spring-context对format模块的目录结构安排:web
public interface Formatter<T> extends Printer<T>, Parser<T> { }
能够看到,该接口自己没有任何方法,而是聚合了另外两个接口Printer和Parser。spring
这两个接口是相反功能的接口。编程
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; }
格式化器接口,它的继承树以下:安全
由图可见,格式化动做只需关心到两个领域:app
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中对时间日期的格式化还需分为这两大阵营来处理:
虽然已经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日期时间类型是Java8引入的一套全新的时间日期API。新的时间及日期API位于java.time中,此包中的是类是不可变且线程安全的。下面是一些关键类
同时还有一些辅助类,如: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融入到Spring本身的Formatter体系内,Spring准备了多个API用于衔接。
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) { ... }
优先级关系两者是一致的:
说明:一致的设计,能够给与开发者近乎一致的编程体验,毕竟JSR 310和Date表示的都是时间日期,尽可能保持一致性是一种很人性化的设计考量。
顾名思义,DateTimeFormatterFactory用于生成一个DateTimeFormatter实例,而本类用于把生成的Bean放进IoC容器内,完成和Spring容器的整合。客气的是,它直接继承自DateTimeFormatterFactory,从而本身同时就具有这两项能力:
多说一句:虽然这个工厂Bean很是简单,可是它释放的信号能够做为编程指导:
说明:
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
使用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
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个思考题帮你复盘:
本文所属专栏:Spring类型转换,公号后台回复专栏名便可获取所有内容。
分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字
专栏
有Spring技术栈、中间件等小而美的原创专栏供以避免费学习。本文已被 https://www.yourbatman.cn 收录。
本文是 A哥(YourBatman) 原创文章,未经做者容许不得转载,谢谢合做。