Spring的Registrar倒排思想送給你
本文轉載自微信公眾號「BAT的烏托邦」,作者YourBatman 。轉載本文請聯系BAT的烏托邦公眾號。
請人吃飯不如請人出汗,請人出汗不如送人以漁。A哥春節繼續營業,這個時候還能看得下去這種技術文章的同學我猜有三類:
- 要么孤獨了
- 要么喝醉了
- 要么喝醉后覺得孤獨了
現實情況往往挺扎心,所以牢記使命,砥礪前行是個好辦法。
上篇文章 把@DateTimeFormat和@NumberFormat注解的實現原理搞清楚了,通過面向元數據編程屏蔽了理解層面、實施層面上的差異化。同時,通過手敲代碼案例,扎扎實實、徹徹底底搞明白了@DateTimeFormat等注解有何用以及如何用,從此不再虛。
像AnnotationFormatterFactory、xxxConverter這種均屬于low-level底層API,上手起來一般頗具難度。一個良好的、流行的框架最起碼應該是上手簡單的,所以開發者應該是最多關心到FormattingConversionService/ConversionService層面即止。本文帶你看看Spring是如何做到醬紫的~
本文提綱
版本約定
- Spring Framework:5.3.x
- Spring Boot:2.4.x
正文
上文是通過手動調用API的方式實現元數據的解析從而達到數據格式化(轉換)的目的,而在實際應用場景中,作為業務開發者是不可能去直接去操縱API的,畢竟說到底那對開發者太不友好,使用門檻過高。
因此,本文將介紹的是一種更為“高級”的使用方案,看看Spring是如何做到兼具高擴展性的整合,從而對開發者十分友好,相信這便也是Spring最有魅力的地方,一起來學習學習吧。
FormatterRegistry:注冊中心
對于多組件的管理,注冊中心是個很好的解決方案。
FormatterRegistry其實在:9. 細節見真章,Formatter注冊中心的設計很討巧 這篇文章已經有過很詳細的分析,學到了它那非常巧妙的設計,這里也順道推薦你花幾分鐘前往看看。在這篇文章的末尾,A哥故意留下了一個小尾巴沒講:注冊中心對注解工廠AnnotationFormatterFactory的支持,也就是這個接口方法:
- FormatterRegistry:
- void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
現在時機成熟,本文就來重點關照它。
該接口方法的唯一實現在FormattingConversionService里:
①:從AnnotationFormatterFactory的泛型類型中提取到注解類型。注意:若沒有指定泛型(沒有指定注解類型)就拋出異常②:該工廠類支持的類型們③:對于支持的每個類型,均注冊一個Printer/Parser
重點在于步驟③,AnnotationPrinterConverter和AnnotationParserConverter均是一個ConditionalGenericConverter轉換器,底層實現實際委托給AnnotationFormatterFactory去完成,所以說對AnnotationFormatterFactory的理解格外的重要,還好上篇文章對它已經做了詳盡分析,點擊這里電梯直達。
下面以AnnotationPrinterConverter為例觀其源碼:
①:該轉換器只負責將fieldType類型轉換為String類型②:只有fieldType上標注有指定的這個注解,此轉換器才會生效③:轉換邏輯。這種緩存式處理邏輯很是常見,其實最核心的代碼往往只有一句,本處就是它:this.annotationFormatterFactory.getPrinter(...)。獲取到合適的Printer,然后適配為PrinterConverter從而完成最終的convert轉換動作
❝說明:PrinterConverter和ParserConverter在本系列前面文章已介紹,相關內容可出門左拐在本系列內很容易找到❞AnnotationParserConverter的實現邏輯如出一轍,這里就不再啰嗦了。
FormattingConversionService它實現了FormatterRegistry接口的所有接口方法,但是它并未提供一些默認行為。換句話講:實現了所有的組件注冊/管理的能力,但并沒有“幫你”注冊任何組件,所以還不具備能夠直接提供服務的條件,若要使用還需“人工干預”放些組件進去才行。
一般來講,對于這種情況一般在外部再包一層 DefaultXXX來提供默認服務是一種對開發者十分友好的解決方案,Spring也是這么干的,下面來看看DefaultFormattingConversionService為我們默認注冊了哪些基礎組件,提供了哪些能力呢。
DefaultFormattingConversionService
默認的格式化器轉換服務,該默認行為適用于大多數應用程序對格式化器、轉換器的需求。
繼承自FormattingConversionService,這個默認行為是為該實例而設計的,但為了方便使用,它對外暴露了其static靜態方法addDefaultFormatters(),這個設計方式同DefaultConversionService暴露了靜態方法addDefaultConverters()如出一轍。
默認注冊了哪些組件?
對于一個默認的Service服務,最關心的當屬它提供了哪些能力。換句話講:它默認幫我們注冊了哪些組件呢?
要回答這個問題可不能靠“背答案”,方式方法其實非常的簡單,爬進去它的源碼處一看便知:
①:雖然說本類(其實是父類)實現了EmbeddedValueResolverAware接口,但構造時依舊可以指定占位符處理器StringValueResolver,當然一般情況下傳入null即可②:調用DefaultConversionService的靜態方法,把默認的轉換器們都注冊進來。那么,默認到底注冊了哪些轉換器呢?DefaultConversionService.addDefaultConverters(this)該靜態方法其實是本系列前面文章所講的內容,這里A哥順道也貼在這吧:
③:若registerDefaultFormatters為true就添加默認的格式化器們,一般來講,此值都為true。那么,默認到底注冊了哪些格式化器呢?
①:對@NumberFormat注解提供支持,格式化數字(Currency、數字、百分數等)
②:對JSR 354錢幣類型javax.money.CurrencyUnit、Monetary等類型提供支持。一般情況下,用不著,所以此part不會被真的注冊
③:對JSR-310日期時間的格式化提供支持。這里使用到了其專用的注冊器DateTimeFormatterRegistrar統一操作
④、⑤:第4、5步是互斥操作,若有Jota-Time就提供對它的支持而不觸發java.util.Date的注冊器,否則使用后者注冊器。
注意:你以為④、⑤是真的互斥嗎?難道導入了joda-time的包后java.util.Date相關模塊就失效了?很明顯不是這樣的,讓你“放心”的地方在于JodaTimeFormatterRegistrar注冊器內部包含了java.util.Date格式化器的注冊關系,因此一切都還得到xxxRegistrar里去看才能揭曉。
總之,DefaultFormattingConversionService作為默認的格式化轉換服務,它是DefaultConversionService的超集,在其基礎上擴展了格式化器,格式化注解支持等相關能力。在Spring環境下,大多數情況使用都是它而非DefaultConversionService。
現在,對FormatterRegistry類一個籠統的認識,知道它默認給注冊了哪些組件,支持哪些功能,但是細節部分還不清晰。比如說:支持哪些數據類型?支持哪些格式?這些都藏在相應的xxxRegistrar里~
FormatterRegistrar:注冊員
registrar:登記員;注冊主任。
xxxRegistrar它是一種“倒排”思想的設計體現,能達到高內聚的效果。Spring、Spring Boot慣用的“伎倆”,譬如你隨便一搜就能看能看到很多很多:
FormatterRegistrar代表的是格式化器注冊員接口,接口定義:
- public interface FormatterRegistrar {
- void registerFormatters(FormatterRegistry registry);
- }
接口方法含義:將Converter和Formatter注冊進FormatterRegistry注冊中心里,至于注冊哪些組件由各子類自行管理和負責,而非Registry注冊中心主動去編排。這是一種倒排設計思想,能夠很好的達到高內聚的目的。
❝注意:雖然存在ConverterRegistry和FormatterRegistry兩個接口,但只有FormatterRegistrar而 沒有 ConverterRegistrar哦❞該接口有三個實現類:
見名之意,每個實現子類都維護著自己分內之事,邊界十分清晰。
DateFormatterRegistrar:Date注冊員
提供對java.util.Date、java.util.Calendar、long類型的日期時間的注冊支持。
接口方法實現如下:
①:添加常規轉換器,支持DateToLong、DateToCalendar、LongToCalendar等基礎轉換能力②:若有個性化指定格式化器,那就給Calendar專門使用。當然,大多數情況下并不會這么做,這步邏輯是為了向后兼容性而考慮而已,一般可忽略③:添加@DateTimeFormat注解的解析支持
代碼示例
下面介紹DateFormatterRegistrar注冊員的使用示例。
普通使用方式
最常規的轉換,Date、Long、Calendar等日期時間類型似乎是可以互轉的。
- @Test
- public void test1() {
- FormattingConversionService conversionService = new FormattingConversionService();
- // 注冊員負責添加格式化器以支持Date系列的轉換
- new DateFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);
- // 1、普通使用
- long currMills = System.currentTimeMillis();
- System.out.println("當前時間戳:" + currMills);
- // Date -> Calendar
- System.out.println(conversionService.convert(new Date(currMills), Calendar.class));
- // Long -> Date
- System.out.println(conversionService.convert(currMills, Date.class));
- // Calendar -> Long
- Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
- calendar.setTimeInMillis(currMills);
- System.out.println(conversionService.convert(calendar, Long.class));
- }
運行程序,輸出:
- 當前時間戳:1612741385457
- java.util.GregorianCalendar[time=1612741385457 ...
- Mon Feb 08 07:43:05 CST 2021
- 1612741385457
完美。
注解使用方式
使用更高級的注解方式,如@DateTimeFormat
- // 準備一個Java Bean:
- @Data
- @AllArgsConstructor
- class Son {
- @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
- private Date birthday;
- }
測試代碼:
- @Test
- public void test1() {
- FormattingConversionService conversionService = new FormattingConversionService();
- // 重要:重要:重要:注冊基礎的轉換能力
- DefaultConversionService.addDefaultConverters((ConverterRegistry) conversionService);
- // 注冊員負責添加格式化器以支持Date系列的轉換
- new DateFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);
- // 1、注解使用
- Son son = new Son(new Date());
- // 輸出:將Date類型輸出為Long類型
- System.out.println(conversionService.convert(son.getBirthday(), Long.class));
- // 輸出:將String烈性輸入為Date類型
- // System.out.println(conversionService.convert("2021-02-12", Date.class)); // 報錯
- System.out.println(conversionService.convert(1613034123709L, Date.class));
- }
運行程序,輸出:
- 1613034230018
- Thu Feb 11 17:02:03 CST 2021
完美。實現了Long類型 <-> Date類型的互轉。
可能有同學會問了,為毛"2021-02-12"就不能convert到Date類型呢?這個原因,額,嗯,哼,若你看了上篇文章 的話,這將不會是個問題。
當然,在實際使用中,更多的情況是String -> Date的轉換case,怎么破?有兩個辦法:
回味本系列前面文章,因為前面有講了不止一次
關注后面文章。因為此case過于常見,后面(特別是在Spring MVC下使用)依舊會重點提及
總結
本文重點是想經由FormatterRegistry注冊中心,引述出Spring常用的Registrar注冊員設計思想,它是一種面向對象編程思想的體現,是不是比面向過程優雅很多呢?本文以DateTimeFormatterRegistrar為示例進行了打樣,可以看到Spring在API抽象這塊著實是非常優秀的,擴展性和方便性兼具,這個度把握得絕佳,或許這也算是設計美學吧。