ApplicationContext(後面簡稱 context)是 Spring 裡面的 IoC 容器。有時候在網路上看到「上下文、Spring 上下文、应用上下文」也是同樣的東西。從真實應用的角度來看,context 是 Spring 框架裡面最爲頂層的控制元件之一,同時也是 Spring 裡面功能最完善的核心控制元件。
Context 本身也是一個介面,它除了可以執行 BeanFactory 的相關功能(Bean 的創建週期管理)以外,也同時繼承了 4 個重要的 Spring 功能,分別是外部資源的定位與載入(Resource-Loader)、環境變數和配置參數的設定(Environment)、i18n 的國際化管理(Message-Source)、和事件監聽與廣播機制(Application-Event-Publisher)。也因為 context 本身可以做到很多事,完整說明會非常消耗文字,因此今天這裡我們只會提及裡面最重要的 refresh() 方法,介面實作在 Abstract-Application-Context 上。

雖然從字面上的意思來看,refresh 有重新整理的意思,但這其實是早期 Spring 容器的歷史遺留命名(當時的設計允許容器在運行期間進行打掉重練)。就 refresh 內部的調度邏輯與核心目的而言,它扮演的是從無到有(或從舊到新)建立並初始化整個 IoC 容器的角色。平常我們在 Main Class 呼叫 Spring Boot 的 run 方法時,底層其實就是觸發了 refresh 來完成容器的初始化。因此,我們可以把 refresh 方法視為整個 IoC 容器正式啟動的核心入口點。

步驟 1 是 prepareRefresh(準備刷新)。這時候的 context 其實還算不上是一個「容器」,頂多算是剛 new 出來的一個空殼物件,這裡的主要目的是把 Environment 準備完整,初始化(並驗證)應用程式的環境屬性,讓容器可以取得正確的環境資料,同時初始化事件監聽器與一些早期事件(early-Application-Events),準備讓容器在啟動的過程中可以發布與接收事件。
步驟 2 是 obtain-Fresh-Bean-Factory。從名稱上來看,這裡會負責準備一個 Bean Factory 物件給後面的流程使用。早期的用途是會在這裡 new 出一個 Bean Factory 物件,並啟動 XML 的解析流程,將配置在 XML(或其他外部檔案)的 BD 都註冊進容器內。但在現行的主流用法下,這已經被簡化成「單純 new 出一個 Day.4 提到的 Default 工廠」。掃描與註冊 BD 的工作,會留到後面的步驟才進行。
步驟 3 是 prepare-Bean-Factory,這一步的主要目的,是把上一步的空殼 Bean Factory 著裝成適配 Spring 環境的 Bean Factory。在這裡 Spring 會把後續流程會用到的一些底層工具(像是 Bean 的 ClassLoader、SpEL 表達式解析器、和轉型用的 ConversionService ...等)注入到剛剛 new 出來的空殼工廠裡,可以想像成是幫 BeanFactory 戴上頭盔、繫上工具腰帶的階段。
步驟 4 是 post-Process-Bean-Factory。在原生的 Spring 框架裡,它是一個空方法,主要的用途是預留給子類別(或第三方框架)的一個功能擴充點。是留給其他程式「掛載更多的工具或解析器」的地方。這裡能提的著名場景比較少,最經典的知名案例在 Spring MVC 上:Spring MVC 會在這裡動態注入 Web 專屬的作用域解析器,讓後續的 @RequestScope、@SessionScope 註解能夠被正確處理。
步驟 5 就是 BFPP 正式登場的地方了。invoke-Bean-Factory-PostProcessors 會依序調用所有的 BDRPP 和 BFPP(BDRPP 在順序上會比 BFPP 還早調用)。透過前幾天簡單帶過的 CCPP(Configuration-Class-Post-Processor)完成 @ComponentScan, @Import 的註解掃描工作,並把掃描過程中,帶有 @Component, @Bean 和 @Configuration 的專案類別註冊成單獨的 BD,完成 Bean 創建週期裡「組件掃描」的這件事。
步驟 6 的 register-Bean-Post-Processors 就是專門為 BPP 擴充的一個特殊管道:因為 BPP 跟普通 Bean 一樣,都是單純的 Bean 物件。如果讓 BPP 跟普通 Bean 走同一組創建流程,可能會導致部分 Bean 沒有辦法吃到 BPP 的業務邏輯。因此這裡的主要目的就是「提前創建所有步驟 5 找到的 BPP 檔案。」確保後面開始建立 Bean 的時候,所有的 BPP 都已經到位,不會有 Bean 因為順序問題而漏接後置處理的相關邏輯。
步驟 7-10 就是後續的小邏輯:Step.7 的 init-Message-Source 負責建立 i18n 相關的訊息元件;Step.8 的 init-Application-Event-Multicaster 建立 Spring 內部事件廣播的核心;Step.9 的 on-Refresh 跟 Step.4 一樣,預設是個空方法(擴充點);Step.10 的 register-Listeners 則把所有的監聽器(不管是框架內建的、web 相關的、實作 Application-Listener 介面的自訂監聽器 ...等)統一蒐集註冊,並順便把 prepare-Refresh 階段暫存至今的早期事件一次補發出去。
這裡可以多提一個小東西:步驟 9 雖然跟步驟 4 一樣是個擴充點,但相對於 4「只有工具箱」的情況,步驟 9 額外可以拿到所有的 BPP、i18n 內容、更完整的 Bean Factory 跟事件廣播器 ...等,因此這裡是個更成熟的擴充位置。這裏最知名的應用同樣來自 Spring Boot:Spring Boot 有自訂一個 context 的子類,它可以透過覆寫步驟 9 的 onRefresh 方法,把初始化內嵌的 WebServer,進而構築 web 框架的穩固基底。
步驟 11 就是大名鼎鼎的 finish-Bean-Factory-Initialization。這裡是整個 refresh 最耗時(流程也最長)的一個階段。主要會調用 Bean 工廠的 pre-Instantiate-Singletons 方法,對前面收集到的所有非 lazy 單例 BD 進行遍歷,呼叫 getBean,執行 doGetBean > createBean > doCreateBean 的創建流程,把 Bean 創建週期裡的實例化、賦值、和初始化一次跑完。前幾天說到的大部分內容,主要都是集中在這邊。
步驟 12 是 finish-Refresh。這裡是整個 Spring 容器在創建週期裡的收尾階段。它會負責清除暫存的快取資源、真正啟動 onRefresh 準備好的內嵌容器(讓 Tomcat 開始監聽 port)、並廣播一個 Context-Refreshed-Event,告訴所有的監聽器:容器已經完全就緒,系統可以開始運作。再之後,我們就會看到正常的 log 被打印、紀錄啟動花了多少時間,就可以開始使用了。
最後,簡單總結一下 refresh 這 12 個步驟:Step 1-4 會把空的 Context 與 BeanFactory 準備好;Step 5-6 讓 BFPP、BPP 進場,並完成 Bean 創建週期的組件掃描;Step 7-10 依序完成 i18n、事件廣播、Web 容器 ...等系統組件的初始化;最後的 11-12 步,則是把手上蒐集到的所有 BD 一口氣轉換成可用的 Bean,並對外昭告容器啟動完成。
到這邊為止。IoC 容器核心篇就算是全部結束了。我們在這裡認識了 Bean 的定義檔(BD)、了解 Bean 的創建過程不是直接 new 出來;然後認識了 BD 的 Reader 跟 Scanner,知道 @Component 是怎麼被 Spring 掃描、轉換、設置成 BD;然後知道了 Bean Factory、知道了 getBean, createBean, 也知道了依賴循環的解決方法;當然我們也認識了 BPP 和 BFPP,懂得在 Bean(或 Bean 工廠)的執行過程,塞入一些擴充邏輯,加工原始的創建流程;接著就是今天的 IoC 容器主流程了,把所有東西連在一起。
從明天開始,我們會花 4 天的時間,講講 Application Context 除了 Bean Factory 以外的其他 4 個功能。簡單了解一下 Spring 是怎麼把一份外部資料載入到 Spring 裡面、怎麼處理 JVM, env, cmd ...等不同等級的環境變數、怎麼實作 I18N 邏輯,確保不同語系的設定可以對應到不同輸出、和怎麼用設計模式實作一個 Spring 內部的事件監聽機制的。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。