✍前言
你好,我是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,從而自己同時就具備這兩項能力:
- 生成DateTimeFormatter例項
- 將該例項放進IoC容器
多說一句:雖然這個工廠Bean非常簡單,但是它釋放的訊號可以作為程式設計指導:
- 一個應用內,對日期、時間的格式化儘量只存在1種模版規範。比如我們可以向IoC容器裡扔進去一個模版,需要時注入進來使用即可
- 注意:這裡指的應用內,一般不包含協議轉換層使用的模版規範。如Http協議層可以使用自己單獨的一套轉換模版機制
- 日期時間模版不要在每次使用時去臨時建立,而是集中統一建立好管理起來(比如放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
- 可通過setPattern()指定數字格式化的模版(一般建議顯示指定)
- 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個思考題幫你覆盤:
- Spring為何沒有針對JSR310時間型別提供專用轉換器實現?
- Spring內建眾多Formatter實現,如何管理?
- 格式化器Formatter和轉換器Converter是如何整合到一起的?
♚宣告♚
本文所屬專欄:Spring型別轉換,公號後臺回覆專欄名即可獲取全部內容。
分享、成長,拒絕淺藏輒止。關注公眾號【BAT的烏托邦】,回覆關鍵字
專欄
有Spring技術棧、中介軟體等小而美的原創專欄供以免費學習。本文已被 https://www.yourbatman.cn 收錄。
本文是 A哥(YourBatman) 原創文章,未經作者允許不得轉載,謝謝合作。