什么是 Spring Boot 自動裝配?
@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
AutoConfigurationImportSelector
Spring Boot 中的 SPI 機制
Spring Boot 作為現在 Java 微服務開發中的中流砥柱,其開箱即用、免去 xml 配置等特點,高效便捷的使 Java 程序員進行業務開發。每次面試 Spring Boot 相關技術點時,其自動裝配原理也是重點關注對象。下面我將結合源碼針對 Spring Boot 實現自動配置做一個詳細的介紹。
閱讀完本文你能知道:
Spring Boot 誕生背景
什么是 Spring Boot 自動裝配?
Spring Boot 啟動時的自動配置的原理知識
Spring Boot 啟動時的自動配置的流程
對于 Spring Boot 一些常用注解的了解
一步一步 debug 從淺到深。
注意:本文的 Spring Boot 版本為 2.6.3。
Spring Boot 誕生背景
使用過 Spring 的小伙伴,一定有被 XML 配置統治的恐懼。
我們回顧下原來搭建一個 Spring MVC 的 helloword 的 web 項目( xml 配置的)我們是不是要在 pom 中導入各種依賴,然后各個依賴有可能還會存在版本沖突需要各種排除。當你歷盡千辛萬苦的把依賴解決了,然后還需要編寫 web.xml 、 springmvc.xml 配置文件等。
我們只想寫個 helloword 項目而已,確把一大把的時間都花在了配置文件和 jar 包的依賴上面。大大的影響了我們開發的效率,以及加大了 web 開發的難度。
為了簡化這復雜的配置、以及各個版本的沖突依賴關系,Spring Boot 就應運而生。
我們現在通過 idea 創建一個 Spring Boot 項目只要分分鐘就解決了,你不需要關心各種配置(基本實現零配置)。讓你真正的實現了開箱即用。它的出現不僅可以讓你把更多的時間都花在你的業務邏輯開發上,而且還大大的降低了 web 開發的門檻。所以 Spring Boot 還是比較善解人意,知道開發人員的痛點在哪。
為什么 Spring Boot 使用起來這么酸爽呢? 這得益于其自動裝配。自動裝配可以說是 Spring Boot 的核心,那究竟什么是自動裝配呢?
什么是 Spring Boot 自動裝配?
Spring Boot 有一個全局配置文件: application.properties 或 application.yml 。在這個全局文件里面可以配置各種各樣的參數比如:你想改個端口啦 server.port 或者想調整下日志的級別都可以配置。
更多其他可以配置的屬性可以參照 官網 。
這么多屬性,這些屬性在項目是怎么起作用的呢?
Spring Boot 項目看下來啥配置也沒有( application.properties 或 application.yml 除外),既然從配置上面找不到突破口,那么我們就只能從啟動類上面找入口了。啟動類也就一個光禿禿的一個 main 方法,類上面僅有一個注解 SpringBootApplication 這個注解是 Spring Boot 項目必不可少的注解。那么自動配置原理一定和這個注解有著千絲萬縷的聯系!
我們下面來一起看看這個注解吧。
探究自動裝配原理
@SpringBootApplication
@SpringBootApplication 標注在某個類上說明這個類是 Spring Boot 的主配置類, Spring Boot 就應該運行這個類的 main 方法來啟動 Spring Boot 應用;它的本質是一個 組合注解 ,我們點進去查看。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
這里最上面四個注解的話沒啥好說的,基本上自己實現過自定義注解的話,都知道分別是什么意思。關鍵就在于后面三個注解:
@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
我們逐個分析下。
@SpringBootConfiguration
這個注解我們點進去就可以發現,它實際上就是一個 @Configuration 注解,這個注解大家應該很熟悉了,加上這個注解就是為了讓當前類作為一個配置類交由 Spring 的 IOC 容器進行管理,因為 Spring Boot 本質上還是 Spring,所以原屬于 Spring 的注解 @Configuration 在 Spring Boot 中也可以直接應用。
由此可見, @SpringBootConfiguration 注解的作用與 @Configuration 注解相同,都是標識一個可以被組件掃描器掃描的配置類,只不過 @SpringBootConfiguration 是被 Spring Boot 進行了重新封裝命名而已。
@ComponentScan
這個注解也很熟悉,用于定義 Spring 的掃描路徑,等價于在 xml 文件中配置 context:component-scan ,假如不配置掃描路徑,那么 Spring 就會默認掃描當前類所在的包及其子包中的所有標注了 @Component , @Service , @Controller 等注解的類。
@EnableAutoConfiguration
這個注解才是實現自動裝配的關鍵,點進去之后發現,它是一個由 @AutoConfigurationPackage 和 @Import 注解組成的復合注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
看起來很多注解,實際上關鍵在 @Import 注解,它會加載
AutoConfigurationImportSelector 類,然后就會觸發這個類的 selectImports() 方法。根據返回的 String 數組(配置類的 Class 的名稱)加載配置類。
我們重點看下
AutoConfigurationImportSelector 。
AutoConfigurationImportSelector
AutoConfigurationImportSelector中的selectImport是自動裝配的核心實現,它主要是讀取META-INF/spring.factories文件,經過去重、過濾,返回需要裝配的配置類集合。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
我們點進 getAutoConfigurationEntry() 方法:
getAttributes 獲取 @EnableAutoConfiguration 中的 exclude 、 excludeName 等。
getCandidateConfigurations 獲取所有自動裝配的配置類,也就是讀取 spring.factories 文件,后面會再次說明。
removeDuplicates 去除重復的配置項。
getExclusions 根據 @EnableAutoConfiguration 中的 exclude 、 excludeName 移除不需要的配置類。
fireAutoConfigurationImportEvents 廣播事件。
最后根據多次過濾、判重返回配置類合集。
現在我們結合 getAutoConfigurationEntry() 的源碼來詳細分析一下:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
第 1 步:判斷自動裝配開關是否打開。
默認
spring.boot.enableautoconfiguration=true ,可在 application.properties 或 application.yml 中設置。
第 2 步 :
用于獲取 EnableAutoConfiguration 注解中的 exclude 和 excludeName 。
第 3 步:
獲取需要自動裝配的所有配置類,讀取 META-INF/spring.factories 。
我們點進getCandidateConfigurations() 方法:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
獲取候選配置了使用了 Spring Framework 自定義的 SPI 機制,使用 SpringFactoriesLoader#loadFactoryNames 加載了類路徑下
/META-INF/spring.factories 文件中的配置類,里面是以 key/value 形式存儲,其中一個 key 是 EnableAutoConfiguration 類的全類名,而它的 value 是一個以 AutoConfiguration 結尾的類名的列表。以 spring-boot-autoconfigure 模塊為例,其 spring.factories 內容如下。
不光是這個依賴下的 META-INF/spring.factories 被讀取到,所有 Spring Boot Starter 下的 META-INF/spring.factories 都會被讀取到。
如果,我們自定義一個 Spring Boot Starter,就需要創建 META-INF/spring.factories 文件。
第 4 步 :
到這里可能面試官會問你:“ spring.factories 中這么多配置,每次啟動都要全部加載么?”。
很明顯,這是不現實的。我們 debug 到后面你會發現, configurations 的值變小了。
雖然 133 個全場景配置項的自動配置啟動的時候默認全部加載。但實際經過后續處理后只剩下 25 個配置項真正加載進來。很明顯,Spring Boot 只會加載實際你要用到的場景中的配置類。這是如何做到的了?
按需加載
這里我們分析剩下的 25 個自動配置類,觀察到每一個自動配置類都有著 @Conditional 或者其派生條件注解。
- @ConditionalOnBean:當容器里有指定 Bean 的條件下
- @ConditionalOnMissingBean:當容器里沒有指定 Bean 的情況下
- @ConditionalOnSingleCandidate:當指定 Bean 在容器中只有一個,或者雖然有多個但是指定首選 Bean
- @ConditionalOnClass:當類路徑下有指定類的條件下
- @ConditionalOnMissingClass:當類路徑下沒有指定類的條件下
- @ConditionalOnProperty:指定的屬性是否有指定的值
- @ConditionalOnResource:類路徑是否有指定的值
- @ConditionalOnExpression:基于 SpEL 表達式作為判斷條件
- @ConditionalOnJava:基于 Java 版本作為判斷條件
- @ConditionalOnJndi:在 JNDI 存在的條件下差在指定的位置
- @ConditionalOnNotWebApplication:當前項目不是 Web 項目的條件下
- @ConditionalOnWebApplication:當前項目是 Web 項 目的條件下
@Configuration(
proxyBeanMethods = false
)
// 檢查是否有該類才會進行加載
@ConditionalOnClass({
RedisOperations.class})
// 綁定默認配置信息
@EnableConfigurationProperties({
RedisProperties.class})
@Import({
LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
...
}
所以當 classpath 下存在某一個 Class 時,某個配置才會生效。
上面所有的注解都在做一件事:注冊 bean 到 Spring 容器。他們通過不同的條件不同的方式來完成:
- @SpringBootConfiguration 通過與 @Bean 結合完成 Bean 的 JavaConfig 配置;
- @ComponentScan 通過范圍掃描的方式,掃描特定注解注釋的類,將其注冊到 Spring 容器;
- @EnableAutoConfiguration 通過 spring.factories 的配置,并結合 @Condition 條件,完成bean的注冊;
- @Import 通過導入的方式,將指定的 class 注冊解析到 Spring 容器;
我們在這里畫張圖把 @SpringBootApplication 注解包含的幾個注解分別解釋一下。
我們現在提到自動裝配的時候,一般會和 Spring Boot 聯系在一起。但是,實際上 Spring Framework 早就實現了這個功能。Spring Boot 只是在其基礎上,通過 SPI 的方式,做了進一步優化。
SPI 全稱為 Service Provider Interface,是一種服務發現機制。為了被第三方實現或擴展的 API,它可以用于實現框架擴展或組件替換
SPI 機制本質是 將接口實現類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載文件中的實現類,這樣可以在運行時,動態為接口替換實現類 。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。
用生活中的例子說就是,你買了一臺小米的手機。但是你用的充電器并不一定非要是小米充電器,你可以拿其他廠商的充電器來進行充電,只要滿足協議、端口等要求,那么就是可以充電的。這也是一種熱拔插的思想,并不是固定死的。
換成代碼來說也是一樣的,我定義了一個接口,但是不想固定死具體的實現類,因為那樣如果要更換實現類就要改動源代碼,這往往是不合適的。
那么我也可以定義一個規范,在之后需要更換實現類或增加其他實現類時,遵守這個規范,我也可以動態的去發現這些實現類。
一個接口可以有很多實現,比如數據庫驅動,有 oracle,mysql,postgress 等等,他們都遵循JDBC 規范,為了解耦,我們可以抽象出一個高層的 Driver 接口,讓各個數據庫服務商去實現各自的驅動,在使用的時候我們可以選擇加載具體的實現方式,這時候我們就可以使用 SPI 這種技術。
JDK 中的 SPI
講到 JDK 中的 SPI ,我們不得不說 java.util.ServiceLoader 這個類,我們先跑起來。
1、創建一個接口,Message
public interface Message{
void send()
}
2、在 resources 資源目錄下創建 META-INF/services 文件夾
3、在 services 文件夾中創建文件,以接口全名命名
4、創建接口實現類
public class SmsMessage implements Message{
public void send(){
System.out.println("send sms message");
}
}
public class EmailMessage implements Message{
public void send(){
System.out.println("send email message");
}
}
5、測試
public class TestServiceLoader{
public static void main(String[] args){
ServiceLoader<Message> messages = ServiceLoader.load(Message.class);
for(Message msg : messages){
msg.send();
}
}
}
打印結果:
send email message
send sms message
簡單一點來說,就是在 META-INF/services 下面定義個文件,然后通過一個特殊的類加載器,啟動的時候加載你定義文件中的類,這樣就能擴展原有框架的功能。
Spring Boot 中的 SPI 機制
在 Spring Boot 的自動裝配過程中,最終會加載 META-INF/spring.factories 文件,Spring Boot 是通過 SpringFactoriesLoader#loadFactoryNames 方法加載的。
Spring Boot 定義了一套接口規范,這套規范規定:Spring Boot 在啟動時會掃描外部引用 jar 包中的 META-INF/spring.factories 文件,將文件中配置的類型信息加載到 Spring 容器(此處涉及到 JVM 類加載機制與 Spring 的容器知識),并執行類中定義的各種操作。對于外部 jar 來說,只需要按照 Spring Boot 定義的標準,就能將自己的功能裝置進 Spring Boot。
本文我們主要介紹了 Spring Boot 自動裝配原理。
簡單來說, Spring Boot 通過 @EnableAutoConfiguration 開啟自動裝配,通過 SpringFactoriesLoader 最終加載 META-INF/spring.factories 中的自動配置類實現自動裝配,自動配置類其實就是通過 @Conditional 按需加載的配置類,想要其生效必須引入 spring-boot-starter-xxx 包實現起步依賴。