前幾天我們講完了 Spring AOP 的核心代理組件,今天把視角重新放在 IoC 容器本身,介紹一下 Configuration-Class-Post-Processor(後續簡稱 CCPP),看看 Spring 是怎麼把專案中散落的組件配置、註解、與其他的業務相關程式,全面解析並匯入 IoC 容器裡面。
CCPP 是一個 BDRPP(Bean-Definition-Registry-Post-Processor),在預設的執行順序上(包含其他 BDRPP 在內),CCPP 是所有 BDRPP 裡第一個會被執行的後置處理器。CCPP 的主要工作有 2 個:第一個是解析配置並註冊 IoC 容器裡的所有 BeanDefinition;第二個主要的工作是為部分類別的 CGLIB 增強:當 CCPP 解析完所有的 BD,會接著檢查配置類別(@Configuration)的相關標記,並對部分的配置類別進行 CGLIB 代理,使 @Bean 註解對應的實體物件可以保持單例。
CCPP 的外部呼叫入口有兩個:第一個是 BDRPP 的公共介面 postProcessBeanDefinitionRegistry。CCPP 會用它進行全專案的多輪遞迴解析與掃描,並從中找出所有專案中的 BeanDefinition。具體來說,在 CCPP 開始運作之前,IoC 容器內就會先持有幾個核心組件的 BD 資料在 BD 註冊表內。CCPP 在這一階段會先遍歷容器內已經註冊的 BeanDefinition,並嘗試從中找出標有 @ComponentScan, @Configuration, 或 @Import ...等註解的候選類別。
接著逐一處理,將配置類別裡面定義的 @Component(或其他普通 Bean)封裝成 BD 元資料儲存在註冊表內。此外,如果在解析的過程中,CCPP 有找到更多的配置類別(例如 @Import 將一個 @Configuration 掛入專案、或在一個 @ComponentScan 底下的掃描包裡,發現另一個標有 @Configuration 的類別 ...等)就會觸發新一輪的解析和掃描。這一過程會持續到沒有新的候選類別被找到為止。
這裡有一個值得補充的細節:CCPP 在找出候選類別之後,後續實際會委託兩個其餘組件協同處理,Parser(Configuration-Class-Parser)會負責逐一閱讀候選類別的元資料細節,理解並展開其背後的衍生資訊,Reader(Configuration-Class-Bean-Definition-Reader)則負責接收 Parser 收集整理好的資訊,把那些方法與額外類別轉換成標準的 BD,寫入註冊表。
而這裡也可以再分類成 2 種情況討論:如果遇到的候選類別內部含有 @Bean 方法,或身上帶有 @Import ...等非 @ComponentScan 的註解,CCPP 會先呼叫 Parser 將新類別的 Metadata 進行暫存,待當輪的配置類解析全數完成之後,再由 Reader 做後續的統一註冊;但如果掃描遇到的註解是 @ComponentScan,Parser 就會暫時停止手邊的解析工作,先進行 @ComponentScan 的包掃描,直到包掃描(包含裡面的 BD 解析和註冊)全部完成,才會回頭繼續進行原本的候選類別解析工作。
此外,從最剛開始的第一輪起,CCPP 在找到候選類別之後,就會為這些類別標記 "full" 標籤或 "lite" 標籤,它們的用途等等再說明,這裡先來講一下這兩個標籤的標記條件:打上 full 標籤的條件很單純,如果一個類別被標記了 @Configuration 註解,且註解中的 proxy-Bean-Methods 屬性為 true(預設就是),那麼這個配置類別就會被 CCPP 打上 full 標籤。
但如果一個 @Configuration 類別的 proxyBeanMethods 為 false,或者類別持有 @Component, @ComponentScan, @Import, @ImportResource 等註解,或者類別內部含有 @Bean 方法,這三個條件任一成立,就會觸發 CCPP 進行 lite 標籤的標記。而如果一個類別沒有辦法符合 full 或 lite 的條件,在紀錄上就會維持原本的 null。簡單來說,null 可以理解成不是一個候選類別、lite 標籤可以理解為一個輕量的候選類別、而 full 則代表需要額外處理的候選類別。
CCPP 的第二個入口是 BFPP 的公共介面 postProcessBeanFactory。在這裡,CCPP 會對所有標記 full 的候選類別(其實就是一部分的 @Configuration 配置類別)進行一次 CGLIB 代理的邏輯增強。配置類別可以用來定義新的 Bean 物件,這點應該沒有問題。
但如果我們先在 @Configuration 裡面寫了 AService 的 @Bean 方法,再寫一個 BService 的 @Bean 方法,並且在 bService() 裡面呼叫 aService() 方法作為建構子參數(例如 return new BService(aService());),這時候如果沒有做 CGLIB 代理的動態增強,系統內就會出現 2 個 AService(一個是 IoC 容器裡的 A、另一個是 new 在 B 裡面的 A),進而破壞 AService 的單例性。
為了解決這個問題,Spring 才會對 full 標籤的配置類別做一次 CGLIB 代理:透過 CGLIB 增強,攔截了對 aService() 的方法調用,可以讓底層從容器中取出已存在的 A 單例(而不是額外創建),進而確保單例的語義不受破壞。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。