8. 格式化器大一統 -- Spring的Formatter抽象

8. 格式化器大一統 -- Spring的Formatter抽象

你好,我是A哥(YourBatman)。前端

上篇文章 介紹了java.text.Format格式化體系,做爲JDK 1.0就提供的格式化器,除了設計上存在必定缺陷,過於底層沒法標準化對使用者不夠友好,這都是對格式化器提出的更高要求。Spring做爲Java開發的標準基建,本文就來看看它作了哪些補充。java

本文提綱

8. 格式化器大一統 -- Spring的Formatter抽象

版本約定

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

✍正文

在應用中(特別是web應用),咱們常常須要將前端/Client端傳入的字符串轉換成指定格式/指定數據類型,一樣的服務端也但願能把指定類型的數據按照指定格式 返回給前端/Client端,這種狀況下Converter已經沒法知足咱們的需求了。爲此,Spring提供了格式化模塊專門用於解決此類問題。git

首先能夠從宏觀上先看看spring-context對format模塊的目錄結構安排:web

8. 格式化器大一統 -- Spring的Formatter抽象

public interface Formatter<T> extends Printer<T>, Parser<T> {

}

能夠看到,該接口自己沒有任何方法,而是聚合了另外兩個接口Printer和Parser。spring

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

格式化器接口,它的繼承樹以下:安全

8. 格式化器大一統 -- Spring的Formatter抽象

由圖可見,格式化動做只需關心到兩個領域:app

  • 時間日期領域
  • 數字領域(其中包括貨幣)

時間日期格式化

Spring框架從4.0開始支持Java 8,針對JSR 310日期時間類型的格式化專門有個包org.springframework.format.datetime.standard框架

8. 格式化器大一統 -- Spring的Formatter抽象

值得一提的是:在Java 8出來以前,Joda-Time是Java日期時間處理最好的解決方案,使用普遍,甚至獲得了Spring內置的支持。如今Java 8已然成爲主流,JSR 310日期時間API 徹底能夠 代替Joda-Time(JSR 310的貢獻者其實就是Joda-Time的做者們)。所以joda庫也逐漸告別歷史舞臺,後續代碼中再也不推薦使用,本文也會選擇性忽略。編輯器

除了Joda-Time外,Java中對時間日期的格式化還需分爲這兩大陣營來處理:

8. 格式化器大一統 -- Spring的Formatter抽象

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的格式化體系作鋪墊了吧,萬變不離其宗。

8. 格式化器大一統 -- Spring的Formatter抽象

所以能夠認爲,Spring爲此作的事情的核心,只不過是寫了個根據Locale、pattern、IOS等參數生成DateFormat實例的邏輯而已,屬於應用層面的封裝。也就是須要知曉getDateFormat()方法的邏輯,此部分邏輯繪製成圖以下:

8. 格式化器大一統 -- Spring的Formatter抽象

所以: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類型

8. 格式化器大一統 -- Spring的Formatter抽象

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爲例,其它輔助類的格式化器實現其實基本同樣:

8. 格式化器大一統 -- Spring的Formatter抽象

那麼問題來了: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) { ... }

8. 格式化器大一統 -- Spring的Formatter抽象

優先級關係兩者是一致的:

  • 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去完成。

8. 格式化器大一統 -- Spring的Formatter抽象

此抽象類共有三個具體實現:

  • 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) 原創文章,未經做者容許不得轉載,謝謝合做。

☀推薦閱讀☀

相關文章
相關標籤/搜索