第四節 Starter 加載時機和源碼理解

tips:每個 springBoot 的版本不同,代碼的實現存會存在不同。

上一章,我們聊到 mybatis-spring-boot-starter; 簡單分析了它的結構。 這一章我們將著重分析 Starter 的加載機制,并結合源碼進行分析理解。

一、加載實際

1.1 何時被加載

引入的 Starter 何時被加載到 Spring 容器里面。 解決了這個過程,那么 Starter 的加載機制就明白大半了。

1.2 加載時機

給出幾個步驟。(為了突出重點,時序圖中忽略后置處理器等,下一章會給出全局時序圖)

  1. 應用啟動始于主啟動類(通常使用 @SpringBootApplication 注解)中的main方法,調用SpringApplication.run(...)
  2. 在自動配置過程中,Spring Boot會使用 SpringFactoriesLoader 類來加載所有可見的 spring.factories 文件。SpringFactoriesLoader 查找類路徑上所有 META-INF/spring.factories 實例
  3. 通過后置處理器,spring.factories文件中的自動配置類(通過EnableAutoConfiguration指定)將被實例化。這些自動配置類通常使用@Configuration注解,且可能包含@Conditional注解

整個加載過程,比較關鍵的兩個類:

  • SpringFactoriesLoader 加載 spring.factories 文件
  • EnableAutoConfigurationImportSelector 導入 autoconfiguration 并進行加載

spring.factories 文件是幫助 SpringBoot 項目包以外的bean(即在pom文件中添加依賴中的bean)注冊到SpringBoot 項目的 Spring 容器中。由于 @ComponentScan注解只能掃描 spring-boot 項目包內的 bean 并注冊到 Spring 容器中,因此需要 @EnableAutoConfiguration 注解來注冊項目包外的 bean。而spring.factories文件,則是用來記錄項目包外需要注冊的 bean 類名。

1.3 SPI 機制

SPI(Service Provider Interface)服務提供接口,在Java中是一種發現和加載可插拔實現的標準機制。目的是實現對組件的動態發現和加載

通過java.util.ServiceLoader類來發現和加載META-INF/services/目錄下相應接口的實現類。

關于 Java 本身提供的 SPI 實現細節

在 SpringBoot 中,SPI 機制允許開發人員在模塊中定義一些服務接口,并且可以為這些接口提供多個可插拔的實現。實現了進一步的便利性和更強大的整合特性。 通過定義約定的 spring.factories 文件,來實現自動配置和條件裝配

接下來,我們通過 debug 的方式來探索 Starter 被加載的過程。

特別說明:本文 debug 的源碼是:mybatis-starter-apply, 如果需要跟著 debug 走步驟流程, 下載相關源碼。uzong-starter-learning: 學習 SpringBoot Starter 的工程案例

二、源碼理解

在理解整個 Starter 之前,我們先來了解一下幾個核心類。它將是整個解密 Starter 最為關鍵的幾個類

2.1 SpringFactoriesLoader 類

這個類的作用, 解析 META-INF/spring.factories 文件,并將結果方法到一個 Map 中。

spring.factories 的結果和 properties 非常相似,我們可以查看 mybatis-spring-boot-starter 文件中的 spring.factories

注意: spring.factories 中的 value 值是可以用逗號分隔。

使用的分割方法是 org.springframework.util.StringUtils#commaDelimitedListToStringArray

public static String[] commaDelimitedListToStringArray(@Nullable String str) {return delimitedListToStringArray(str, ",");
}

將 spring.factories 中的 key = value 解析后放入到 MultiValueMap<String, String>。 這是一個特殊的 map。 它的 value 值是一個 List

public interface MultiValueMap<K, V> extends Map<K, List<V>> {.....
}

繼承至 Map<K, List<V>>。

SpringFactoriesLoader 將當前類加載器下所有的 "META-INF/spring.factories" 文件進行解析,并將解析結果放到一個 Map 中進行緩存。注意:目前都是 String, 還不是 class

附上部分 SpringFactoriesLoader 源碼以及注釋。

核心邏輯:加載 META-INF/spring.factories 文件數據; 把里面的key、value值放入到一個特殊的 Map 中。


public abstract class SpringFactoriesLoader {// JAR 文件的路徑public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";// 將所有類路徑的名稱加載到這個 Map 中private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();// 根據類名獲取value值。注意返回的是 listpublic static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {String factoryClassName = factoryClass.getName();return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}// 解析 spring.factories  文件,將解析的 key=value 放入到 cache 中。private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {MultiValueMap<String, String> result = cache.get(classLoader);if (result != null)return result;try {// jar 文件路徑Enumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = new LinkedMultiValueMap<>();// 遍歷所路徑,加載文件資源while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);// 解析成 Properties 對象,即 key = valueProperties properties = PropertiesLoaderUtils.loadProperties(resource);for (Map.Entry<?, ?> entry : properties.entrySet()) {// 逗號分割值List<String> factoryClassNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));result.addAll((String) entry.getKey(), factoryClassNames);}}cache.put(classLoader, result);return result;}......}// 反射,加載對象@SuppressWarnings("unchecked")private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {try {// 反射創建對象Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);if (!factoryClass.isAssignableFrom(instanceClass)) {throw new IllegalArgumentException("Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");}return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();}.....}
}

通過 debug 斷點,查看 cache 中的數據

以 debug 方式啟動 com.uzong.instance.MyApplication 類,查看 Map 數據。

斷點地址:SpringFactoriesLoader#142 行

可以看到,mybatis-spring-boot-starter中的 auto-configuration 類被加載了。

回到 mybatis-spring-boot-starter 的 spring.factories 確認一下。

org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 確認一致。

讀取 spring.factories 抽取了幾個關鍵步驟。如下所示:

補充: idea 中 debug 窗口,為執行棧幀,如下所示。可以清楚的知道運行經過的鏈路。

總結兩個要點:

  • cache 包含多種類型,不僅僅是 Starter 中指定的 org.springframework.boot.autoconfigure.EnableAutoConfiguration類型。 后續通過指定類型獲取 list 值。
  • 此處的 Map 中的值還是 String,不是 Class,那么哪一步才能將 String 變成 Class 呢,接下來關注 AutoConfigurationImportSelector

2.2 AutoConfigurationImportSelector 類

org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports

這個方法的作用是將 org.springframework.boot.autoconfigure.EnableAutoConfiguration對應的 List 值進行加載到 application 中進行類的初始化

相關源碼,主要從 cache map 中讀取 org.springframework.boot.autoconfigure.EnableAutoConfiguration 值。并進行加載

入口:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports

	public String[] selectImports(AnnotationMetadata annotationMetadata) {.....try {AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);AnnotationAttributes attributes = getAttributes(annotationMetadata);// 加載 org.springframework.boot.autoconfigure.EnableAutoConfigurationList<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);.....return StringUtils.toStringArray(configurations);}......}

在這個類中,會讀取 spring.factories 中的 org.springframework.boot.autoconfigure.EnableAutoConfiguration 值進行加載。

getCandidateConfigurations()

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());......return configurations;}

返回 org.springframework.boot.autoconfigure.EnableAutoConfiguration

protected Class<?> getSpringFactoriesLoaderFactoryClass() {return EnableAutoConfiguration.class;
}

特別注意:不同版本源碼略有不同;在 selectImports 中, 也對返回的 list 做了過濾。

在 filter() 方法中,過濾一些不滿足的 configuration,不進一步加載。比如:ConditionalOnClass 條件注解。如果找不到依賴的那個類,則直接過濾掉。

·

下面就是基于ConditionalOnClass org.springframework.boot.autoconfigure.condition.OnClassCondition#getOutcomes 規則做過濾。

如果我們引入的 Starter 發現沒有生效,則可以通過斷點到這一行,來排查對應的文件是否被引入。

2.3 加載過程

找到符合的全路徑名稱后,就交給 org.springframework.context.annotation.ConfigurationClassParser 類進行解析。然后把整個 configuration 所在的 Bean 都一一進行加載。

關于 ConfigurationClassParser 是非常重要的,它是 springframework core 中的核心類。以遞歸性的解析加載所有類;也是非常繁瑣和重要的,后面會用單獨章節進行詳細講解。

2.4 整個加載過程

加載過程,包括 ConfigurationClassParser 以及后置處理器也添加進來的時序圖。

時序圖中的幾個關鍵方法,可以關注一下:

關鍵方法一:refreshContext

org.springframework.boot.SpringApplication#prepareContext

關鍵方法二:invokeBeanFactoryPostProcessors。 激活各種后置處理器

org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors

關鍵方法三:processDeferredImportSelectors,處理延遲導入 importSelector 。

org.springframework.context.annotation.ConfigurationClassParser#processDeferredImportSelectors

Spring 的核心擴展接口。 SpringBoot 的 AutoConfigurationImportSelector 類,實現 ImportSelector(DeferredImportSelector)方法,從而實現 Starter 的擴展裝配能力。

三、本章小結

本文只通過局部了解到 Starter 中的類被加載的時機,主要有兩個核心類

  • SpringFactoriesLoader
  • AutoConfigurationImportSelector

一個是加載 spring.factories 加載資源放入 Map ; 另外一個觸發 Map 中讀取數據并交給 ConfigurationClassParser 解析。

如果僅僅從局部理解加載過程是局限的。接下來,我們從整個 SpringBoot 的加載順序理解全貌。

已同步發布到公眾號:面湯放鹽?第四節 Starter 加載時機和源碼理解 (qq.com)

掘金賬號:第四節 Starter 加載時機和源碼理解 - 掘金 (juejin.cn)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/14027.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/14027.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/14027.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

問題與解決:element ui垂直菜單展開后顯示不全

比如我這個垂直菜單展開后&#xff0c;其實系統管理下面還有其他子菜單&#xff0c;但是顯示不出來了。 解決方法很簡單&#xff0c;只需要在菜單外面包一層el-scrollbar&#xff0c;并且將高度設置為100vh。

Laravel 11 PHP8

一直都是用laravel 7 左右的&#xff0c;現在要求將項目升級到laravel 11 和使用PHP8&#xff0c;隨手記錄一些小問題&#xff0c;laravel 11的包是領導給的&#xff0c;沒有使用composer 安裝&#xff0c;所以我也不確定和官方的是否一致 遇到這問題 可以這樣 env 中默認的數…

基于若依的旅游推薦管理系統(spring boot+vue+mybatis+Ajax)

一、項目目的 隨著社會的高速發展&#xff0c;人們生活水平的不斷提高&#xff0c;以及工作節奏的加快&#xff0c;旅游逐漸成為一個熱門的話題&#xff0c;因為其形式的多樣&#xff0c;涉及的面比較廣&#xff0c;成為人們放松壓力&#xff0c;調節情緒的首要選擇。 傳統的旅…

上位機圖像處理和嵌入式模塊部署(mcu的按鍵輸入)

【 聲明&#xff1a;版權所有&#xff0c;歡迎轉載&#xff0c;請勿用于商業用途。 聯系信箱&#xff1a;feixiaoxing 163.com】 做技術的同學&#xff0c;大部分都會把精力放在技術本身&#xff0c;卻忽視了學的東西有什么實際的用途。就拿gpio來說&#xff0c;一般我們點燈也…

正確認識IP地址和子網掩碼的聯系

IP地址和子網掩碼是計算機網絡中兩個非常重要的概念&#xff0c;它們共同確定了設備在局域網中的地址以及該地址所屬的子網&#xff0c;只要兩者結合&#xff0c;就能確定唯一地址IP66_ip歸屬地在線查詢_免費ip查詢_ip精準定位平臺。 IP地址是用于標識計算機網絡中的每臺設備的…

Ajax用法總結(包括原生Ajax、Jquery、Axois)

HTTP知識 HTTP&#xff08;hypertext transport protocol&#xff09;協議『超文本傳輸協議』&#xff0c;協議詳細規定了瀏覽器和萬維網服務器之間互相通信的規則。 請求報文 請求行: GET、POST /s?ieutf-8...&#xff08;url的一長串參數&#xff09; HTTP/1.1 請求頭…

Buzz庫網絡爬蟲實例:快速爬取百度搜索實時熱點

前言 隨著互聯網的發展&#xff0c;信息獲取已經成為了人們日常生活和工作中的重要一環。而在信息獲取的過程中&#xff0c;網絡爬蟲作為一種自動化的數據采集工具&#xff0c;為我們提供了極大的便利。本文將介紹如何利用PHP編寫一個簡單而高效的網絡爬蟲&#xff0c;實現快速…

R實驗 參數檢驗(二)

實驗目的&#xff1a;掌握正態分布和二項分布中&#xff0c;功效與樣本容量之間的關系&#xff1b;學會利用R軟件完成一個正態總體方差和兩個正態總體方差比的區間估計和檢驗。 實驗內容&#xff1a; &#xff08;習題5.28&#xff09;一種藥物可治療眼內高壓&#xff0c;目的…

Mac安裝 Intellij IDEA,親測有效M1、M2可用

引言 最近開始學習使用spring boot寫一個簡單的后端項目&#xff0c;使用Intellij IDEA軟件&#xff0c;Intellij IDEA為新用戶提供了30天的免費試用。 方案 1.官網下載Intellij IDEA IntelliJ IDEA – the Leading Java and Kotlin IDE 或者直接網盤連接下載&#xff1a;…

第一份工資

當我拿到我人生的第一份工資時&#xff0c;那是一種難以言表的激動。我記得那個下午&#xff0c;陽光透過窗戶灑在了我的辦公桌上&#xff0c;我看著那張支票&#xff0c;心中滿是欣喜和自豪。那是我獨立生活的開始&#xff0c;也是我對自己能力的一種肯定。 我記得我是如何支配…

SQL注入:pikachu靶場中的SQL注入通關

目錄 1、數字型注入&#xff08;post&#xff09; 2、字符型注入&#xff08;get&#xff09; 3、搜索型注入 4、XX型注入 5、"insert/update"注入 Insert&#xff1a; update&#xff1a; 6、"delete"注入 7、"http header"注入 8、盲…

C#實現KMP算法,在長字符串中找到第一個符合要求的子字符串

KMP&#xff08;Knuth-Morris-Pratt&#xff09;算法是一種高效的字符串搜索算法&#xff0c;它可以在一個文本字符串&#xff08;Text&#xff09;中搜索一個詞&#xff08;Pattern&#xff09;&#xff0c;時間復雜度為O(nm)&#xff0c;其中n是文本字符串的長度&#xff0c;…

vite前端UI框架使用詳解(2024-05-24)

Vite&#xff08;發音同 "veet"&#xff09;是一種新型前端構建工具&#xff0c;能夠顯著提升前端開發體驗。它主要由兩部分組成&#xff1a; 一個開發服務器&#xff0c;它基于原生的ES模塊提供了豐富的內建功能&#xff0c;如速度快到驚人的 模塊熱更新&#xff08…

【Linux安全】Firewalld防火墻

目錄 一.Firewalld概述 二.Firewalld和iptables的關系 1.firewalld和iptables的聯系 2.firewalld和iptables的區別 三.Firewalld區域 1.概念 2.九個區域 3.區域介紹 4.Firewalld數據處理流程 四.Firewalld-cmd命令行操作 1.查看 2.增加 3.刪除 4.修改 五.Firewa…

arping 一鍵檢測網絡設備連通性(KALI工具系列二)

目錄 1、KALI LINUX簡介 2、arping工具簡介 3、在KALI中使用arping 3.1 目標主機IP&#xff08;win&#xff09; 3.2 KALI的IP 4、操作示例 4.1 IP測試 4.2 ARP測試 4.3 根據存活情況返回 5、總結 1、KALI LINUX簡介 Kali Linux 是一個功能強大、多才多藝的 Linux 發…

表現層框架設計之使用XML設計表現層

使用XML設計表現層&#xff0c;統一Web Form與Windows Form的外觀。 1.XML&#xff08;可擴展標記語言&#xff09; XML&#xff08;可擴展標記語言&#xff09;與HTML類似&#xff0c;是一種標記語言。與主要用于控制數據的顯示和外觀的HTML標記不同&#xff0c;XML標記用于定…

PostgreSQL的擴展(extensions)-常用的擴展之pg_rman

PostgreSQL的擴展&#xff08;extensions&#xff09;-常用的擴展之pg_rman pg_rman 是 PostgreSQL 社區提供的一個備份和恢復管理工具。它能夠簡化和自動化 PostgreSQL 數據庫的備份和恢復過程&#xff0c;并支持全量備份、增量備份和差異備份。pg_rman 提供了方便的命令行接…

【機器學習與大模型】驅動下的電子商務應用

摘要&#xff1a; 隨著信息技術的飛速發展&#xff0c;電子商務已經成為當今商業領域中最為活躍和重要的部分之一。而機器學習和大模型的出現&#xff0c;為電子商務帶來了新的機遇和挑戰。本文深入探討了機器學習與大模型在電子商務中的應用&#xff0c;包括個性化推薦、精準營…

Java 18:開啟Java平臺的新紀元

Java 18&#xff1a;探索Java平臺的最新飛躍 隨著Java 18的發布&#xff0c;Java平臺再次證明了其不斷創新和適應現代軟件開發需求的能力。作為長期支持&#xff08;LTS&#xff09;版本&#xff0c;Java 18不僅帶來了性能上的提升&#xff0c;還引入了一系列令人興奮的新特性…

基于雙向長短期記憶 Bi-LSTM 對消費者投訴進行多類分類

前言 系列專欄:【深度學習:算法項目實戰】?? 涉及醫療健康、財經金融、商業零售、食品飲料、運動健身、交通運輸、環境科學、社交媒體以及文本和圖像處理等諸多領域,討論了各種復雜的深度神經網絡思想,如卷積神經網絡、循環神經網絡、生成對抗網絡、門控循環單元、長短期記…