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