昨天我們有講到,相對於「整條流水線」的基礎架構,BPP 更專注在 Bean 創建過程中的相關細節。今天我們就回過頭來,簡單認識 Spring 框架裡 Bean-Factory-Post-Processor(後續簡稱 BFPP)、和子介面 Bean-Definition-Registry-Post-Processor(後續簡稱 BDRPP)的相關內容,看看框架在執行 Bean 的創建環節之前,是怎麼把基礎環境搭建起來的。
BeanFactoryPostProcessor

BFPP 是 Spring 框架裡面的核心介面之一,內部定義了一個 post-Process-Bean-Factory 方法,同時接收一個 factory 物件作為參數。BFPP 被調用的時機點是在容器完成 Bean 工廠的初始設置之後。它的主要用途即是對 factory 組件(或 factory 管理的其他組件)進行調整,使其能在正式啟動創建 Bean 的流水線前,完成全域屬性的替換、或環境校準 ...等,確保後續生產出來的 Bean 具備正確的運行配置。
在早期以 XML 作為主體的環境下,BFPP 的主要工作是對註冊表內現有的 BD 進行參數微調。那時候「掃描並註冊 Bean」是其他組件(Day.3 提到的 XML Reader)的工作。但後來 Spring 從 XML 驅動轉向 @ 註解驅動。BD 配置從「外部檔案」的 XML 逐漸移動到「內部程式」的 Java 檔內。加上框架內的既有骨架,已經依賴了大量的 BFPP 邏輯作為管理與調度的標準。
Spring 為了在最大的程度下,完成「新配置檔案的解析邏輯」和「兼容舊有的 XML 讀取方式」,選擇在 BFPP 的介面底下,新增了專門用來註冊 BD 的子介面(也就是 BDRPP)。這也是為什麼我們 Day.3 講到的 Reader 跟 XML Reader 並沒有共同父類的原因 —— 因為底層的解析機制不同,新版的 Reader 並不依賴外部檔案做 BD 的掃描與轉化,就也不需要依賴相同的類別或介面了。
BeanFactoryPostProcessor

BDRPP 就像剛才說的,是一個專門為了「從 Java 檔案動態解析 BD 配置」而衍生出來的 BFPP 子介面。介面相較於 BFPP,只多定義了一個 post-Process-Bean-Definition-Registry 方法,實作 BDRPP 介面的抽象(或實體)類別,可以利用這個方法自定義 BD 的解析邏輯,並透過方法簽名裡的 registry 參數,將 BD 的相關資訊註冊到容器的註冊表內。
一般來說,我們會透過 Spring 框架原本就寫硬編碼的 BDRPP 組件(例如最常拿來講的 Configuration-Class-Post-Processor),配合 Day.3 提到的 Scanner, 和 XML Reader,將專案裡(和 XML ...等外部檔案)能找到的 BD 註冊進 Registry 裡。並透過後續的 BFPP 組件(例如 Property-Sources-Placeholder-Configurer),微調 BD 裡的部分參數,協力完成啟動環節中「找到 BD、註冊 BD、調整 BD」這樣的整體流程。搭建起整個流水線。
此外,CCPP 是另一個在 Spring 框架裡面的重要組件,除了動態發現並註冊 BD 這份工作以外,CCPP 還承攬了其他 1-2 項其他的內容,但因為具體的內容要先了解 Spring AOP 的相關邏輯才好說明,因此在篇幅的安排上,這邊把 CCPP 放在了 Day.17 的內容上,這裡就不多做展開,讀者先在這裡有個印象就好,其他的細節可以參考後面的篇幅。
說完了 BFPP 和 BDRPP,我們再來可以補充幾個小東西:
BD 的註冊時機
首先是在正常的流程下,BD 可以被註冊進 BD Map 的 3 個時機:最早的時機觸發點是 Day.3 提到的 Reader。這是系統內自動調用的底層方法,一般沒有太多的介入空間。主要的用途是把包含 CCPP 在內的幾個核心組件註冊進 BD Map 裡面。如果專案中需要對這一段的 BD 進行高侵入性的調整,需要透過手動啟動容器、並手動介入註冊表的方式操作,但就脫離日常 Spring Boot 的預設流程了,這裡不多做討論。
第二個可以介入的時間點就是 BDRPP 這裡。透過在 Java 類別上掛載 @Component(或其衍生註解),預設流程走到 BDRPP 的調用就會自動將這些 Java 類別轉換成 BD 資料。除此之外,我們一般使用 @Configutation(或 @Import 掛載)配置類別,並從配置類定義的額外 @Bean 也是在這邊被找到並註冊的。這也是一般最常見到的 BD 註冊方式。
第三個可以介入 BD 註冊的時機,則是 BFPP 後續被調用的時間點。雖然在職責的分工上,BFPP 更像是「負責對 BD 資料進行微調」的協作組件,但因為方法本身的輸入參數是整個 factory 物件。我們還是可以利用它取得容器的 BD 註冊表,並手動新增(或刪除)BD 資訊。
BFPP 的執行順序
因為在實際的開發上,實作 BFPP、BDRPP、或甚至 BPP 的實體組件有很多。為了正確管理這些協同組件的調用順序(避免先對 Bean 做了一次代理之後,再打算替換 Bean 物件這類的迷幻操作),Spring 提供了以 Ordered 介面(和 Priority-Ordered 介面)排序的相關操作:預設 Priority-Ordered 會比 Ordered 提前執行,且數字越小的越先執行,因此如果我們要指定一個組件的優先順序最高,讓組件實作 Priority-Ordered 介面並在排序方法上回傳 MIN_VALUE 即可。
宏觀的啟動流程
最後,我們把今天提到的 BFPP 和 BDRPP 介面。配合前面所有講過的核心組件,跑一次簡易的調用順序:先容器啟動,街的啟動 Day.3 的 Reader,註冊 CCPP 等核心組件,然後創建 Bean 工廠,並調用 BDRPP 的後置處理器,過程中就會用到 Day.3 的 Scanner(和 XML Reader),將大量掛有 @Component 註解(或配置在 XML 檔案裡面)的 BD 註冊進容器的 Registry 裡。
後續執行 BFPP 的後製處理器,對所有已經找到的 BD 資料進行參數校驗、${...} 解析、或者是 XML 資料解密 ...等,調整(或替換)BD 的相關資訊。最後開始啟動 Bean 的創建週期。依序執行 Bean 的實例化、賦值、初始化 ...等製造工作。過程中也可以配合 BPP 做細節的加工處理 ...。明天我們會把這個啟動流程講解得細緻一點、完整一點。看看在 Spring 框架裡面,最頂層的控制組件是怎麼調用這些相關方法,前後又做了哪些事情,啟動一個完整的 Spring 容器的。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。