在 Spring 的框架裡面,如果我們想要將一份 Java 檔案轉變成 Spring 裡一筆可被使用的 BD 元資料,以宏觀的角度來看,我們大抵需要 3 個 Spring 組件的分工與協作。除了標題寫到的 Bean-Definition-Registry(後續簡稱 Registry)以外,還會需要 Annotated-Bean-Definition-Reader(後續簡稱 Reader)和 Class-Path-Bean-Definition-Scanner(後續簡稱 Scanner)。
AnnotatedBeanDefinitionReader
Reader 是用來「將一個類別主動地轉換成 BD」的 Spring 組件。具體來說,Reader 內部定義了一系列的 registerBean 方法多型,同時將 Class<?> 作為參數。使調用方能透過這個入口,直接將指定的類別註冊進 Spring 容器,使其成為被容器管裡的 BD 之一。
Reader 在這裡的功能相對單純:當我們使用 SpringApplication.run() 啟動整個系統,Spring 首先會建立一個空白的容器,接著 Reader 就會被核心程式調用,將我們的啟動類別打包成一個 BD,同時登記到 Spring 的空白容器當中。而當 Reader 將主類別註冊為 BD 之後,後續的解析組件就能以所在的 package 作為基準路徑掃描,找到 package 底下的所有 Bean。
ClassPathBeanDefinitionScanner
Scanner 是 BD 的掃描器。它主要負責「掃描檔案」和「將檔案轉變為 BD」這兩份工作。跟主動調用的 Reader 不同,Scanner 內部的驅動方法是被動式的 doScan。該方法接收一個(或多個)base-packages 作為參數,接著在拿到這些 packages 之後,就會遍歷 package 底下的所有檔案,並將符合 BD 條件的類別轉換為可被容器管理的 BD。
承襲上一段說的,在 Reader 將主類別註冊成 BD 之後,核心程式就會調用 Scanner,並將主類別的 package 作為參數交給 Scanner,使 Scanner 可以正常檢索核心包底下的所有業務邏輯檔案,並將符合 BD 條件的相關檔案(像是帶有 @Component ...等註解的檔案)整理成 BD。這就是「為什麼我們一般寫的業務程式,都要放在跟 main 那包同級、或 main 那包其他資料夾底下」的主要原因。
這裡可以多提一個小東西:為了處理大型專案可能有上百(甚至上千個)檔案的情況,Scanner 並不會使用傳統的 loadClass 方法進行緩慢的反射載入。Scanner 會借助一個叫做 MetadataReader 的組件,透過更加高效的 ASM 技術檢索檔案 bytecode。這樣除了有速度上的提升以外,因為沒有使用到 loadClass 的反射調用,也可以避免提前觸發類別的靜態初始化區塊,導致邏輯上的相關錯誤。
BeanDefinitionRegistry
當 Reader 設定好 main Class 的主路徑、且 Scanner 透過 main Class 開始進行 base-packages 的掃描之後,就輪到最後的 Registry 登場了。Registry 負責的是 BD 的紀錄與保存,在現代的環境下,通常 IoC 容器會直接充當 Registry 這個角色(IoC 容器除了充當 Registry 以外,還會充當以後介紹到的其他東西)。容器內部有一個簡單的 Map 表,基本上就是負責用來存儲 BD(和 BD 名稱)映射結構。
細節一點來說,Registry 的創建時間其實比 Reader 還要來得早。還記得我們在 Reader 那段說的嗎?「當我們使用 SpringApplication.run() 啟動整個系統,Spring 首先會建立一個空白的容器。」在說這句話的同時,Resistry(其實就是容器本身)就已經被建立起來了。這也是為什麼 Reader 後續可以把啟動類別打包成 BD,並註冊到容器裡面的完整原因。
這裡也可以多提及一個東西:在 Spring 裡面,BD Map 跟 Bean Map 是兩個不一樣的資料結構。清楚一點的說,因為 Prototype Bean、@ConditionOnXxx 或系統相關邏輯的緣故,在 BD Map 裡面的 BD 不一定會全數被轉換成 Bean 物件。如果我們只用一個 Map 同時記錄 BD 和 Bean,是很容易在後續的執行期間發生問題的,因此將「靜態規格」與「最終成品」進行拆分,才是 Spring 能保持系統穩固(和高可讀性)的一種作法。
XmlBeanDefinitionReader
最後補充一個小東西:XML Reader 是 Spring 最早時期用來處理 XML 設定檔的組件,在功能跟定位上比較像是現今 Reader 的早期版本,可以藉由讀取(並解析)中的配置資訊,將裡面的標籤轉換成 BD 並註冊進容器。但 XML 設定檔但跟 Reader 不同的地方在於:XML Reader 接收的參數是 Resource,對應的資料是 classpath 或檔案系統上的 XML 檔案路徑。但因為現在幾乎沒在使用,加上我自己也沒有太多用它的經驗,單純在查找資料的過程中,這個組件一直有被寫出來,所以就簡單提了一下。
簡單來說:Spring 在啟動的環節裡,大致是遵循以下流程找到 BD 的
到這裡為止,大概就是今天要講的所有內容了。今天我們解答了 Spring 是怎麼掃描和儲存 BD,明天我們就來解決昨天提到的第二個問題(Spring 在拿到 BD 之後又會怎麼做)。介紹一下 Spring 當中的 BeanFactory,看看框架裡面的 Bean 工廠是怎麼把 Bean 的定義檔逐步轉換成真正可使用的 Bean 物件的。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。