掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
你好,我是A哥(YourBatman)。

本系列(Spring類型轉(zhuǎn)換)到現(xiàn)在,大部分的理論基礎(chǔ)已經(jīng)搞定了,很抽象甚至很枯燥有木有。還好終于快到頭了,此處應(yīng)給跟著“學(xué)”過來的自己1秒鐘掌聲。接下來的內(nèi)容會更多的偏向于應(yīng)用,比如在Spring MVC中的應(yīng)用、在IoC容器里的應(yīng)用、在JPA里的應(yīng)用等。
后續(xù)內(nèi)容相較于前面基礎(chǔ)孰輕孰重姑且不能一概而論,但相信大部分同學(xué)會更感興趣些。畢竟具象化的東西更易接受,更順應(yīng)人性,并且很多都是些工作中會用、考試中會考、面試中會問的知識點,自然積極性也會高上不少。
本文作為“二者”的承上啟下,將介紹自定義ConversionService類型轉(zhuǎn)換服務(wù)的集大成者FormattingConversionServiceFactoryBean,以及較少人會關(guān)注但設(shè)計思路卻很重要的DateTimeContext和DateTimeContextHolder內(nèi)容,很值得你看它一看。
ConversionService是Spring自3.0提出的一個全新的、統(tǒng)一的類型轉(zhuǎn)換服務(wù),在Spring Framework下它有兩大實現(xiàn)可用于生產(chǎn):
雖說Spring內(nèi)置的轉(zhuǎn)換器/格式化器能“應(yīng)付”絕大部分場景,但不免有時候我們依舊需要DIY。通過前面的學(xué)習(xí)我們知道了,向注冊中心注冊格式化器/轉(zhuǎn)換器的方式多種多樣,能否降低使用者門檻提供一種較為統(tǒng)一的編程體驗?zāi)?有,它就是今天的主角:FormattingConversionServiceFactoryBean。
一個工廠類,用于產(chǎn)生FormattingConversionService實例,設(shè)計它的目的是方便的集中化配置它。
在這之前,小復(fù)習(xí)一下:FormattingConversionService實現(xiàn)了FormatterRegistry接口,并且繼承自GenericConversionService,所以功能上它是DefaultConversionService的超集。一般來講,我們常說的ConversionService轉(zhuǎn)換服務(wù)底層實現(xiàn)使用的就是它(的子類),區(qū)分如下case:
另外請切記,ConversionService作為基礎(chǔ)組件,并非全局只有一個。在Spring Framework和Spring Boot環(huán)境下有著不同表現(xiàn),在本系列后半部分對此會再做詳細(xì)的使用分析。
根據(jù)本系列前面文章所講,雖然格式化器/轉(zhuǎn)換器的底層表現(xiàn)形式均為xxxConverter,但其“上層”的注冊方式卻不單一,提供了多種多樣的方式,表現(xiàn)出了極大的靈活性,便于使用和擴(kuò)展。就拿FormatterRegistry(繼承自ConverterRegistry)注冊中心來說,它提供了很多方法讓你可以向注冊中心注冊格式化器/轉(zhuǎn)換器,如下API:
- // ==========1、直接注冊Converter轉(zhuǎn)換器==========
- void addConverter(Converter, ?> converter);
void addConverter(ClasssourceType, ClasstargetType, Converter super S, ? extends T> converter); - void addConverter(GenericConverter converter);
- void addConverterFactory(ConverterFactory, ?> factory);
- // ==========2、注冊Formatter格式化器(底層適配為Converter轉(zhuǎn)換器)==========
- void addPrinter(Printer> printer);
- void addParser(Parser> parser);
- void addFormatter(Formatter> formatter);
- void addFormatterForFieldType(Class> fieldType, Formatter> formatter);
- // ==========3、通過注解工廠方式為某些標(biāo)有制定注解的格式注冊格式化器/轉(zhuǎn)換器==========
- void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory);
除了這些直接用于注冊的接口API能夠完成注冊外,Spring還提供了一些批量注冊方式。雖然底層依舊依賴于這些API接口,但這種聚合手段大大提高了其可治理性,簡化了注冊流程。譬如前面用專門文章重點介紹過的FormatterRegistrar注冊員就是典型代表。
關(guān)于格式化器/轉(zhuǎn)換器的注冊方式,A哥嘗試畫張圖來表示:
由此清晰可見,注冊格式化器/轉(zhuǎn)換器的方式有很多很多。因此為了方便起見,Spring設(shè)計了FormattingConversionServiceFactoryBean來集中化的向容器提供一個ConversionService實例,盡量提供統(tǒng)一化編程體驗來屏蔽更多細(xì)節(jié),對使用者友好。
知曉了此FactoryBean的功能定位,實現(xiàn)其實就比較簡單嘍,無非就是把各種“手段”整合到一起,可集中化定制和管理罷了。
從這些成員變量就能看到注冊轉(zhuǎn)換器的所有手段都被包含了進(jìn)來。細(xì)心的你有可能會疑問:咋沒看到通過注解工廠AnnotationFormatterFactory的方式呀???
其實它被歸類到了Set formatters(Set的泛型類型是?),如下源碼可“證明”:
①:負(fù)責(zé)注冊所有的轉(zhuǎn)換器。包括Converter、ConverterFactory、GenericConverter三種類型,覆蓋1:1、N:1、N:N所有場景②:負(fù)責(zé)注冊格式化器Formatter和注解工廠方式。這里有兩點值得你特別注意:
③:負(fù)責(zé)處理注冊員xxxRegistrar的批量注冊動作。如DateTimeFormatterRegistrar和DateFormatterRegistrar等,關(guān)于注冊員FormatterRegistrar詳細(xì)介紹可參見這篇文章:11. 春節(jié)禮物:Spring的Registrar倒排思想送給你
最后,從上面這張圖還有一點值得你關(guān)注:該工廠產(chǎn)生的ConversionService實例是固定的 DefaultFormattingConversionService,這就是我為何說在Spring Framework環(huán)境下默認(rèn)使用的ConversionService實例都是它的原因,這不管是web還是非web場景。
誠然,直接使用FormattingConversionServiceFactoryBean的場景是不多的,除非你對此機(jī)制非常了解想進(jìn)行完全替換,那么推薦你使用它。
舉個例子:在Spring Framework環(huán)境下,若要啟用Spring MVC模塊的話會使用@EnableWebMvc注解來開啟,此時Spring MVC默認(rèn)就向容器放入了一個ConversionService實例:
- WebMvcConfigurationSupport:
- @Bean
- public FormattingConversionService mvcConversionService() {
- FormattingConversionService conversionService = new DefaultFormattingConversionService();
- addFormatters(conversionService);
- return conversionService;
- }
- protected void addFormatters(FormatterRegistry registry) {
- }
暴露了addFormatters()這個擴(kuò)展點,一般來講若你想自定義格式化器/轉(zhuǎn)換器的話,通過復(fù)寫此方法添加是被推薦的方式。
另外呢,從這部分源碼可以看到這里并沒有通過FormattingConversionServiceFactoryBean來構(gòu)建類型轉(zhuǎn)換服務(wù)實例,而是通過直接new的方式。其實來講,這里若使用FormattingConversionServiceFactoryBean來構(gòu)建我認(rèn)為是能夠更方便的,而且也更方便留下擴(kuò)展點,你覺得呢?
Spring自4.0起提供了DateTimeContextHolder,其用于線程綁定DateTimeContext。而DateTimeContext提供了:Chronology(Java中的日歷系統(tǒng))、ZoneId(JSR 310中的時區(qū))、DateTimeFormatter(JSR 310格式化器)等上下文數(shù)據(jù),如果需要這種上下文信息的話,可以使用這個API進(jìn)行綁定。
- public class DateTimeContext {
- @Nullable
- private Chronology chronology;
- @Nullable
- private ZoneId timeZone;
- ... // 省略get/set
- }
若有定制需要,可以向該上下文實例設(shè)置這兩個值(日歷和時區(qū)),當(dāng)然最重要的當(dāng)屬從上下文中獲取到一個格式化器,這也是最終目的:
①:若設(shè)置了timeZone時區(qū),就以其為準(zhǔn)。否則執(zhí)行步驟②②:若沒設(shè)置時區(qū),嘗試從LocaleContext上下文里獲取時區(qū),有就有沒有就沒有
簡而言之,這個步驟就是根據(jù)上下文設(shè)置的參數(shù)(有就有沒有就沒有)得到一個DateTimeFormatter實例用于格式化,注意:此方法是實例方法 而非靜態(tài)方法,所以先得自己new一個DateTimeContext喲。
再看DateTimeContextHolder,它用ThreadLocal把DateTimeContext和線程綁定,方便使用者獲取上下文數(shù)據(jù):
- private static final ThreadLocal
dateTimeContextHolder = new NamedThreadLocal<>("DateTimeContext");
本類除了對DateTimeContext的維護(hù)外,提供了一個更直接的方法:根據(jù)當(dāng)前上下文情況,直接獲取到DateTimeFormatter格式化器實例:
①:給調(diào)用者傳入的格式化器綁定上Locale屬性,若存在的話②:獲取到當(dāng)前上下文對象DateTimeContext,進(jìn)而根據(jù)當(dāng)前上下文(若存在)得到加工后的DateTimeFormatter實例
該靜態(tài)方法可認(rèn)為是對DateTimeContext#getFormatter()的封裝并擴(kuò)展出Locale參數(shù)也可自定義,使用者可以一步到位獲取到和上下文相關(guān)的DateTimeFormatter實例,大多數(shù)時候我們直接使用此方法更為方便。
和其它xxxContext一樣,結(jié)合使用場景去了解它才能更深刻,畢竟一切的學(xué)習(xí)都是為了應(yīng)用嘛。Context上下文的概念在程序的世界里已經(jīng)非常多見了,不管是做業(yè)務(wù)開發(fā)、中間件開發(fā)、基礎(chǔ)架構(gòu)開發(fā)我認(rèn)為都有理由會應(yīng)用。
由于DateTimeFormatter是線程安全的,因此為了開發(fā)方便,通常會定一個(已經(jīng)配置好的)全局通用的實例,形如這樣:
- /**
- * 全局通用的日期-時間格式化器(當(dāng)然還可以有日期專用的、時間專用的...)
- */
- public static final DateTimeFormatter GLOBAL_DATETIME_FORMATTER = DateTimeFormatter
- .ofPattern("yyyy-MM-dd HH:mm:ss")
- .withLocale(Locale.CHINA)
- .withZone(ZoneId.of("Asia/Shanghai"))
- .withChronology(IsoChronology.INSTANCE);
這樣子項目中所有需要使用到格式化器DateTimeFormatter的地方從這里獲取即可,即便利又得到了統(tǒng)一管理,可謂一舉兩得。
但是,但是,但是,避免不了有時候會有個性化的的格式化需求,并且個性化的粒度還很細(xì)。如在Spring MVC場景下,不同的接口的返回值想自定義Locale、自定義ZoneId時區(qū)等從而返回不同的數(shù)據(jù)格式,但是又想復(fù)用全局的設(shè)置以盡量保持統(tǒng)一(畢竟個性化的參數(shù)一般僅1~2個而已)。
聽到不同接口,敏感的就能發(fā)現(xiàn)這是一個典型的可以用Context解決的場景:既不影響全局,又能實現(xiàn)線程級別的個性化定制。下面針對此場景,我用代碼示例模擬Demo。
- @Test
- public void test1() throws InterruptedException {
- // 模擬請求參數(shù)(同一個參數(shù),在不同接口里的不同表現(xiàn))
- Instant start = Instant.now();
- // 模擬Controller的接口1:zoneId不一樣
- new Thread(() -> {
- DateTimeContext context = new DateTimeContext();
- context.setTimeZone(ZoneId.of("America/New_York"));
- DateTimeContextHolder.setDateTimeContext(context);
- // 基于全局的格式化器 + 自己的上下文自定義一個本接口專用的格式化器
- DateTimeFormatter primaryFormatter = DateTimeContextHolder.getFormatter(GLOBAL_DATETIME_FORMATTER, null);
- System.out.printf("北京時間%s 接口1時間%s \n",
- GLOBAL_DATETIME_FORMATTER.format(start),
- primaryFormatter.format(start));
- }).start();
- // 模擬Controller的接口2:Locale不一樣
- new Thread(() -> {
- // 基于全局的格式化器 + 自己的上下文自定義一個本接口專用的格式化器
- DateTimeFormatter primaryFormatter = DateTimeContextHolder.getFormatter(GLOBAL_DATETIME_FORMATTER, Locale.US);
- System.out.printf("北京時間%s 接口2時間%s \n",
- GLOBAL_DATETIME_FORMATTER.format(start),
- primaryFormatter.format(start));
- }).start();
- TimeUnit.SECONDS.sleep(2);
- }
運行程序,輸出:
- 北京時間2021-03-15T07:29:37.8+08:00[Asia/Shanghai] 接口1時間2021-03-14T19:29:37.8-04:00[America/New_York]
- 北京時間2021-03-15T07:29:37.8+08:00[Asia/Shanghai] 接口2時間2021-03-15T07:29:37.8+08:00[Asia/Shanghai]
完美。通過這種操作上下文的方式達(dá)到了既復(fù)用又個性化的目的:
可能有同學(xué)會問,若想自定義Pattern怎么辦呢?答案是:做不到。Java的DateTimeFormatter和Pattern屬于強(qiáng)綁定關(guān)系,Pattern改了就得用個全新的DateTimeFormatter實例,其它屬性無法(內(nèi)部)拷貝。至于什么原因,A哥在講解JDK日期時間時有提及,具體可關(guān)注我參考JDK日期時間系列。
本文介紹了Spring兩個組件:
關(guān)于Spring轉(zhuǎn)換器/格式化器的基礎(chǔ)內(nèi)容基本就到這了,希望這打破了很多同學(xué)以為的:類型轉(zhuǎn)換就等于Spring MVC Controller自動封裝的思維定式,要知道它的應(yīng)用空間還大著哩。
本系列接下來會更偏向于應(yīng)用層面的case分析,Spring MVC場景的使用更是”首當(dāng)其沖“嘍,歡迎關(guān)注一起探討、交流和學(xué)習(xí)。
本文所屬專欄:Spring類型轉(zhuǎn)換,后臺回復(fù)專欄名即可獲取全部內(nèi)容,已被https://yourbatman.cn收錄。
看完了不一定懂,看懂了不一定會。來,文末3個思考題幫你復(fù)盤:
如何使用FormattingConversionServiceFactoryBean自定義類型轉(zhuǎn)換服務(wù)?
Spring設(shè)計出DateTimeContext和DateTimeContextHolder旨在解決什么問題?
為何DateTimeContextHolder#getFormatter方法的第二個參數(shù)Locale不放到DateTimeContext里?明明可以這么干的呀
系列推薦
12. 查漏補(bǔ)缺@DateTimeFormat到底干了些啥
11. 春節(jié)禮物:Spring的Registrar倒排思想送給你
10. 原來是這么玩的,@DateTimeFormat和@NumberFormat
分享題目:自定義Formatter格式化器?用它就對嘍
轉(zhuǎn)載源于:http://uogjgqi.cn/article/dhojjch.html

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流