8. 格式化器大一统 -- Spring的Formatter抽象(上)

简介: 8. 格式化器大一统 -- Spring的Formatter抽象(上)

你好,我是A哥(YourBatman)。


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


本文提纲

image.png

版本约定


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


✍正文


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


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


image.png


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


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


image.png


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


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


时间日期格式化


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


image.png


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


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


-image.png


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的格式化体系做铺垫了吧,万变不离其宗。


image.png


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


image.png


因此: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类型


image.png


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为例,其它辅助类的格式化器实现其实基本一样:


image.png



那么问题来了: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) { ... }


image.png


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


  • pattern
  • iso
  • dateStyle/timeStyle


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

相关文章
|
存储 缓存 监控
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache抽象详解的核心原理探索
缓存的工作机制是先从缓存中读取数据,如果没有再从慢速设备上读取实际数据,并将数据存入缓存中。通常情况下,我们会将那些经常读取且不经常修改的数据或昂贵(CPU/IO)的且对于相同请求有相同计算结果的数据存储到缓存中。
195 1
|
前端开发 Java Spring
《Spring MVC》 第六章 MVC类型转换器、格式化器
《Spring MVC》 第六章 MVC类型转换器、格式化器
196 0
|
前端开发 Java Spring
Spring MVC-06循序渐进之Converter和Formatter
Spring MVC-06循序渐进之Converter和Formatter
100 0
|
XML 缓存 监控
Spring Cache抽象-基于XML的配置声明(基于EhCache的配置)
Spring Cache抽象-基于XML的配置声明(基于EhCache的配置)
96 0
|
XML 缓存 监控
Spring Cache抽象-基于XML的配置声明(基于ConcurrentMap的配置)
Spring Cache抽象-基于XML的配置声明(基于ConcurrentMap的配置)
94 0
|
缓存 Java Spring
Spring Cache抽象-使用SpEL表达式
Spring Cache抽象-使用SpEL表达式
275 0
|
缓存 Java Spring
Spring Cache抽象-缓存管理器
Spring Cache抽象-缓存管理器
117 0
|
存储 缓存 Java
Spring Cache抽象-使用Java类注解的方式整合EhCache
Spring Cache抽象-使用Java类注解的方式整合EhCache
82 0
|
存储 缓存 搜索推荐
Spring Cache抽象-缓存注解
Spring Cache抽象-缓存注解
121 0
|
XML 安全 Java
Spring事务专题(四)Spring中事务的使用、抽象机制及模拟Spring事务实现(1)
Spring事务专题(四)Spring中事务的使用、抽象机制及模拟Spring事务实现(1)
228 0
Spring事务专题(四)Spring中事务的使用、抽象机制及模拟Spring事务实现(1)