iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

30 篇淺談 Spring 框架的核心底層組件 系列 第 18 篇

Day 18: ImportSelector —— 配置類別的動態導入橋樑與 SPI 機制

  • 分享至 

  • xImage
  •  

今天我們來講講 Spring 裡配置類別(Configuration class)的動態載入機制:一般載入配置類別的方式大致可以分成三種管道。最常見的是包掃描(也就是 @ComponentScan),透過註解指定需要掃描的 package 路徑,並在路徑下放上 @Configuration 配置類別,就可以在啟動的時候載入;

第二種是透過 @Import 註解,直接指定系統當中的某個配置類別做二次載入;最後一種則是 Spring 自己的 SPI 機制,透過約定好的清單檔案(AutoConfiguration.imports 或 spring.factories),把 Spring 掃描路徑之外的配置類別匯入進來。但 @Import 的問題是只能做單純的匯入,如果匯入條件變得複雜,需要根據系統參數、或某些特定類別的存在與否決定是否載入,單用 @Import 就會顯得很笨重。因此,Spring 框架提供了一個 ImportSelector,用來處理複雜邏輯的配置類匯入。

ImportSelector 是 Spring 框架裡面的一個核心介面,主要的功能是「決定要把哪些配置類別載入到 Spring 的 IoC 容器當中。」介面內定義了一個 selectImports 方法,AnnotationMetadata 參數指的是呼叫方的元數據,通常是另一個標注了 @Import(或 @EnableXxxYyy)的配置類別,參數內可以找到的東西很多,例如呼叫端的其餘註解、註解中的屬性值、或者判斷是否直接或間接持有某註解、是否存在指定的 @Bean ...等。ImportSelector 會根據這些給定的資料,運行內部的 selectImports 邏輯,最後整理出一份需要匯入的 imports 清單,並整理成 String[] 回傳給 IoC 容器。這裡有 3 個東西可以注意:首先回傳可以是空陣列,代表 selector 可以決定不額外增加其他的類別或 Bean;另外 String[] 裡面需要完整類別名稱,不可以只寫 XxxService 這樣的 simple name;最後,因為解耦和效能的緣故,ImportSelector 通常只決定「要匯入哪些額外的類別」,實際的載入會交由 Spring 當中的其他組件完成。

實務上,Spring 最常用到的 ImportSelector 是 AutoConfigurationImportSelector(後續簡稱 ACIS)。在 main 的 @SpringBootApplication 註解裡頭,有一個 @EnableAutoConfiguration,如果我們點進去,就會看到這個類別在 @Import 的註解裡面。ACIS 是 Spring boot 可以做到「開箱即用、約定大於配置」的最大功臣,原因後面會說,主要跟系統功能的擴充有關。它本身依賴了 ImportSelector 的子介面 DeferredImportSelector,跟普通 ImportSelector 最大的區別是,這個介面允許實作類「不用在當下立刻解析匯入的配置類」,而是先把它暫存起來,等到所有配置類都解析完之後才統一觸發,這使得 ASIC 可以先掃描完所有的候選類別,並根據呼叫端的 Metadata(或者是其他參數資訊)將清單進行過濾,最後交給 IoC 容器,實現後續的 BD 註冊與類別實例化。

將解析延後的原因有很多,除了可以對功能進行拆解耦、提升系統的運行效率,讓類別本身的功能更加內聚以外,最關鍵的一個原因是「時機」—— 因為能拖到最後才解析,所以這時候容器已經完整掌握開發者自己註冊了哪些 BD,ACIS 就不像普通的 ImportSelector 只能透過 MetaData 進行註解(或呼叫類別)相關的條件篩選,還可以用「系統現在是不是有 XxxService 或 xxx-starter」這類的條件進行更細緻的調整。當然了,也因為 ACIS 實作了 Environment 和 ResourceLoader 的相關 Aware 介面,所以除了上述說到的幾種方法以外,ACIS 也可以透過環境變數、JVM 參數、或特定的資源檔案 ...等資訊,決定要不要讓候選類別加入到最後的清單當中。此外,不同於 @ComponentScan 那種包底下的全類別掃描,這裡說到的「ACIS 掃描」指的其實就是 Spring 內部的 SPI 機制。

Spring 框架的 SPI 機制是透過 SpringFactoriesLoader(後續簡稱 Loader)進行驅動,以「2.7 以前的 Spring boot」來說,Loader 內約定了一個特殊的檔案名稱(spring.factories),內部的資料格式大致如附圖,是一個簡單的 k-v 純文字資料,用來區分 Bean 的職責和職責對應到的實體類。在 Spring 啟動的過程,系統會逐一訪問所有依賴裡面的 resources/META-INF,如果資料夾有存在 spring.factories,Loader 就會掃描裡面的 k-v,並解析為一個個候選類別。但因為 spring.factories 是全模組通用的約定清單,隨著內容變多,解析它的啟動成本就逐漸加大,因此從「2.7 版的 Spring boot」開始,框架不再使用原始的 spring.factories 收納自動配置的類別清單,改採職責更加分離的 .imports 檔案架構,並將路徑挪到 resources/META-INF/spring 底下,同時調整檔案名稱為「觸發模組功能的核心註解或介面」,進而提升可讀性、讓檔案的職責變得更加單一,也易於 SPI 的相關管理。

那這個東西要怎麼做?又是在哪時候會用到 SPI 呢?簡單來說,因為 SPI 機制是一種把「服務定義」和「服務實作」拆分的工作機制。就像之前提過的,Spring 在大多數的組件上都預留了客製化實作的相關方法或接口,如果我們要對組件做替換或擴充,最簡單的方式就是在專案內手動寫一個 @Bean(或繼承新類別)做到組件覆蓋。但這種方式只適用在「檔案在主路徑下」的場景,因為如果我們需要將這個自製組件進行封裝,變成一個獨立的 Starter 或第三方的通用庫,後續的組件調用就會出現問題(預設的包掃描路徑只會掃描 main 以下的 package,不會掃描到額外的三方套件)。這時候 SPI 的用途就得以浮現:透過在 spring.factories(或 xxx.imports)宣告針對特定組件的改良或擴充,主系統就可以在「只需要進行 maven 依賴」的情況下,正確把套件中的指定類別加到 BD 裡面,進而完成模組帶有的功能與特定實現。


上一篇
Day 17: ConfigurationClassPostProcessor —— 解析 Java Config 與驅動組件掃描的核心類別
下一篇
Day 19: DispatcherServlet —— 請求分發與 Spring MVC 核心處理流程
系列文
30 篇淺談 Spring 框架的核心底層組件 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言