Springboot自動裝配原始碼及啟動原理理解
springboot版本:2.2.2
傳統的Spring框架實現一個Web服務,需要匯入各種依賴JAR包,然後編寫對應的XML配置檔案 等,相較而言,Spring Boot顯得更加方便、快捷和高效。那麼,Spring Boot究竟如何做到這些的呢? 接下來分別針對Spring Boot框架的依賴管理、自動配置和執行流程進行深入分析
依賴管理
首先,建立一個springboot工程實現web服務,pom.xml檔案有兩個核心依賴,分別是 spring-boot-starter-parent 和 spring-boot-starter-web
-
spring-boot-starter-parent
在pom.xml中找到對應的依賴如下
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.5</version> <relativePath/> <!-- lookup parent from repository --> </parent>
上述程式碼中,將spring-boot-starter-parent依賴作為Spring Boot專案的統一父專案依賴管理,並 將專案版本號統一為2.7.5,該版本號根據實際開發需求是可以修改的
使用“Ctrl+滑鼠左鍵”進入並檢視spring-boot-starter-parent底層原始檔,發現spring-boot-starter-parent的底層有一個父依賴spring-boot-dependencies,核心程式碼具體如下
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.7.5</version> </parent>
發現還有爺爺輩,繼續檢視底層原始檔,核心程式碼如下
<properties> <activemq.version>5.16.5</activemq.version> <antlr2.version>2.7.7</antlr2.version> <appengine-sdk.version>1.9.98</appengine-sdk.version> <artemis.version>2.19.1</artemis.version> <aspectj.version>1.9.7</aspectj.version> <assertj.version>3.22.0</assertj.version> <atomikos.version>4.0.6</atomikos.version> <awaitility.version>4.2.0</awaitility.version> <build-helper-maven-plugin.version>3.3.0</build-helper-maven-plugin.version> <byte-buddy.version>1.12.18</byte-buddy.version> <cache2k.version>2.6.1.Final</cache2k.version> ... </properties>
可以發現,該檔案通過標籤對一些常用技術框架的依賴檔案 進行了統一版本號管理,說明pom.xml引入依賴檔案不需要標註依賴檔案版本號
思考一下,spring-boot-starter-parent父依賴啟動器的主要作用是進行版本統一管理,那麼專案執行依賴的JAR包是從何而來的?
-
spring-boot-starter-web
檢視spring-boot-starter-web依賴檔案原始碼,核心程式碼具體如下
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.7.5</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> <version>2.7.5</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <version>2.7.5</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.3.23</version> <scope>compile</scope> </dependency>
從上述程式碼可以發現,spring-boot-starter-web依賴啟動器的主要作用是提供Web開發場景所需的底層 所有依賴
正是如此,在pom.xml中引入spring-boot-starter-web依賴啟動器時,就可以實現Web場景開發,而 不需要額外匯入Tomcat伺服器以及其他Web依賴檔案等。當然,這些引入的依賴檔案的版本號還是由 spring-boot-starter-parent父依賴進行的統一管理。
自動配置(啟動流程)
概念:能夠在我們新增jar包依賴的時候,自動為我們配置一些元件的相關配置,我們無需配置或者只需 要少量配置就能執行編寫的專案
那Spring Boot到底是如何進行自動配置的,都把哪些元件進行了自動配置?
自問自答:Spring Boot應用的啟動入口是@SpringBootApplication註解標註類中的main()方法, @SpringBootApplication能夠掃描Spring元件並自動配置Spring Boot;自定義了一個demo,檢視@SpringBootApplication內部原始碼進行分析 ,核心程式碼具體如下:
@SpringBootApplication
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}
點進去看一下 @SpringBootApplication 有什麼
@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 :表示Spring Boot配置類。檢視@SpringBootConfiguration註解源 碼,核心程式碼具體如下。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {...}
從上述原始碼可以看出,@SpringBootConfiguration 註解內部有一個核心註解 @Configuration,該註解是Spring框架提供的,表示當前類為一個配置類(XML配置檔案的註解表現形式),並可以被元件掃描器掃描。由此可見,@SpringBootConfiguration 註解的作用與 @Configuration 註解相同,都是標識一個可以被元件掃描器掃描的配置類,只不過 @SpringBootConfiguration 是被SpringBoot進行了重新封裝命名而已
重點關注一下 @EnableAutoConfiguration:表示開啟自動配置功能,該註解是SpringBoot框架最重要的註解,也是實現自動化配置的註解。同樣,檢視該註解內部檢視原始碼資訊,核心程式碼具體如下
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //自動配置包
@Import(AutoConfigurationImportSelector.class) //藉助@Import註解來收集所有符合自動配置條件的bean定義,並載入到IoC容器
public @interface EnableAutoConfiguration {...}
可以發現它是一個組合註解,Spring 中有很多以Enable開頭的註解,其作用就是藉助@Import來 收集並註冊特定場景相關的bean,並載入到IoC容器。@EnableAutoConfiguration 就是藉助@Import 來收集所有符合自動配置條件的bean定義,並載入到IoC容器。下面,對這兩個核心註解分別講解 :
@AutoConfigurationPackage: 會把 @SpringBootApplication 註解標註的類所在的包名拿到,並且對該包及其子包進行掃描,將元件新增到容器中;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//@Import為Spring框架底層註解,它的作用就是給容器中匯入某個元件類,
@Import(AutoConfigurationPackages.Registrar.class) //預設將主配置類(@SpringbootAppication)所在的包及其子包裡面所有的元件掃描到容器中;
public @interface AutoConfigurationPackage {...}
從上述原始碼可以看出,@AutoConfigurationPackage 註解的功能是由 @Import 註解實現的,它是 spring框架的底層註解,它的作用就是給容器中匯入某個元件類;
@Import(AutoConfigurationPackages.Registrar.class):可以幫助springboot應用將所有符合條件的@Configuration配置都載入到當前Springboot建立並使用的IoC容器中。
從上述原始碼可以看出,在Registrar類中有一個 registerBeanDefinitions() 方法,使用Debug模式啟動專案,可以看到選中的部分就是com.yun。也就是說,@AutoConfigurationPackage 註解的主要作用就是將主程式類所在包及所有子包下的元件到掃描到spring容器中。因此在定義專案包結構時,要求定義的包結構非常規範,專案主程式啟動類要定義在最外層的根目錄位置,然後在根目錄位置內部建立子包和類進行業務開發,這樣才能夠保證定義的類能夠被元件掃描器掃 描;
@Import({AutoConfigurationImportSelector.class}):將 AutoConfigurationImportSelector 這個類匯入到spring容器中, AutoConfigurationImportSelector 可以幫助springboot應用將所有符合條件的 @Configuration 配置都載入到當前SpringBoot建立並使用的IoC容器(ApplicationContext)中;
繼續研究 AutoConfigurationImportSelector 這個類,通過原始碼分析這個類中是通過 selectImports 這 個方法告訴springboot都需要匯入那些元件:
進入 loadMetData() 方法
進入 getAutoConfigurationEntry() 在進入 getCandidateConfigurations() 方法
該方法中有一個重要方法loadFactoryNames,這個方法是讓SpringFactoryLoader去載入一些元件的 名字。
繼續點開loadFactory方法
會去讀取一個 spring.factories 的檔案,讀取不到會表這個錯誤,我們繼續根據會看到,最終路徑的長 這樣,而這個是spring提供的一個工具類
它其實是去載入一個外部的檔案,而這檔案是在
@EnableAutoConfiguration 就是從classpath中搜尋META-INF/spring.factories配置檔案,並將其中 org.springframework.boot.autoconfigure.EnableutoConfiguration 對應的配置項通過反射(Java Refletion)例項化為對應的標註了 @Configuration 的 JavaConfig形式的配置類,並載入到IOC容器中;
總結
springboot底層實現自動配置的步驟是:
-
springboot應用啟動;
-
@SpringBootApplication起作用;
-
@EnableAutoConfiguration;
-
@AutoConfigurationPackage:這個組合註解主要是 @Import(AutoConfigurationPackages.Registrar.class),它通過將Registrar類匯入到容器中,而 Registrar類作用是掃描主配置類同級目錄以及子包,並將相應的元件匯入到springboot建立管理 的容器中;
-
@Import(AutoConfigurationImportSelector.class):它通過將 AutoConfigurationImportSelector類匯入到容器中,AutoConfigurationImportSelector類作用 是通過selectImports方法執行的過程中,會使用內部工具類SpringFactoriesLoader,查詢 classpath上所有jar包中的META-INF/spring.factories進行載入,實現將配置類資訊交給 SpringFactory載入器進行一系列的容器建立過程
@ComponentScan:具體掃描的包的根路徑由Spring Boot專案主程式啟動類所在包位置決 定,在掃描過程中由前面介紹的 @AutoConfigurationPackage 註解進行解析,從而得到Spring Boot項 目主程式啟動類所在包的具體位置
自定義starter
-
新建maven工程 工程名為wsy-spring-boot-starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.3.12.RELEASE</version> </dependency>
-
編寫javaBean
@Data @EnableConfigurationProperties(SimpleBean.class) //開啟@ConfigurationProperties @ConfigurationProperties(prefix = "simplebean") //prefix命名全部為小寫 public class SimpleBean { private String id; private String name; }
-
編寫配置類 MyAutoConfiguration
@Configuration @ConditionalOnClass //當類路徑class下有指定的類的情況,就會進行自動配置 public class MyAutoConfiguration { static { System.out.println("MyAutoConfiguration init..."); } @Bean public SimpleBean simpleBean(){ return new SimpleBean(); } }
-
resource下建立/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.yun.config.MyAutoConfiguration
使用starter
-
在之前的springboot-demo專案的pom.xml中引入自定義starter依賴
<dependency> <groupId>com.yun</groupId> <artifactId>wsy-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
在全域性配置檔案中配置屬性值
simplebean.id=1 simplebean.name=wsy
-
編寫測試方法
@Autowired private SimpleBean simpleBean; @Test public void wsyStarterTest(){ System.out.println(simpleBean); }
-
查詢結果
MyAutoConfiguration init... 2022-12-01 16:21:54.168 INFO 13996 --- [ main] c.y.s.SpringbootDemoApplicationTests : Started SpringbootDemoApplicationTests in 4.143 seconds (JVM running for 6.581) SimpleBean{id='1', name='wsy'}
執行原理
每個Spring Boot專案都有一個主程式啟動類,在主程式啟動類中有一個啟動專案的main()方法, 在該方法中通過執行SpringApplication.run()即可啟動整個Spring Boot程式。
那麼SpringApplication.run()方法到底是如何做到啟動Spring Boot專案的呢?
帶著疑問我們debug走一遍內部原始碼檢視邏輯;
@SpringBootApplication
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class[]{primarySource}, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return (new SpringApplication(primarySources)).run(args);
}
從上述原始碼可以看出,SpringApplication.run() 方法內部執行了兩個操作,分別是 SpringApplication例項的初始化建立和呼叫run()啟動專案,這兩個階段的實現具體說明如下
-
SpringApplication例項的初始化建立
檢視SpringApplication例項物件初始化建立的原始碼資訊,核心程式碼具體如下
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.sources = new LinkedHashSet(); this.bannerMode = Mode.CONSOLE; this.logStartupInfo = true; this.addCommandLineProperties = true; this.addConversionService = true; this.headless = true; this.registerShutdownHook = true; this.additionalProfiles = Collections.emptySet(); this.isCustomEnvironment = false; this.lazyInitialization = false; this.applicationContextFactory = ApplicationContextFactory.DEFAULT; this.applicationStartup = ApplicationStartup.DEFAULT; this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); //把專案啟動類.class設定為屬性儲存起來 this.primarySources = new LinkedHashSet(Arrays.asList(primarySources)); //判斷當前webApplicationType應用的型別 this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); //設定初始化器(Initializer),最後會呼叫這些初始化器 this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 設定監聽器(Listener) this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)); //用於推斷並設定專案main()方法啟動的主程式啟動類 this.mainApplicationClass = this.deduceMainApplicationClass(); }
從上述原始碼可以看出,SpringApplication的初始化過程主要包括4部分,具體說明如下。
-
this.webApplicationType = WebApplicationType.deduceFromClasspath() 用於判斷當前webApplicationType應用的型別。deduceFromClasspath() 方法用於檢視Classpath類路 徑下是否存在某個特徵類,從而判斷當前webApplicationType型別是SERVLET應用(Spring 5之前的傳 統MVC應用)還是REACTIVE應用(Spring 5開始出現的WebFlux互動式應用)
-
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class)) 用於SpringApplication應用的初始化器設定。在初始化器設定過程中,會使用Spring類載入器 SpringFactoriesLoader從META-INF/spring.factories類路徑下的META-INF下的spring.factores檔案中 獲取所有可用的應用初始化器類 ApplicationContextInitializer。
-
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)) 用於SpringApplication應用的監聽器設定。監聽器設定的過程與上一步初始化器設定的過程基本一樣, 也是使用SpringFactoriesLoader從META-INF/spring.factories類路徑下的META-INF下的 spring.factores檔案中獲取所有可用的監聽器類 ApplicationListener。
-
this.mainApplicationClass = this.deduceMainApplicationClass() 用於推斷並設定專案main()方法啟動的主程式啟動類
-
-
專案的初始化啟動
分析完 (new SpringApplication(primarySources)).run(args) 原始碼前一部分SpringApplication例項物件的初始化建立後,檢視run(args)方法執行的專案初始化啟動過程,核心程式碼具體如下:
public ConfigurableApplicationContext run(String... args) { long startTime = System.nanoTime(); DefaultBootstrapContext bootstrapContext = createBootstrapContext(); // 初始化應用上下文和異常報告集合 ConfigurableApplicationContext context = null; // 配置headless屬性 configureHeadlessProperty(); // 第一步:獲取並啟動監聽器 SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(bootstrapContext, this.mainApplicationClass); try { // 建立 ApplicationArguments 物件 初始化預設應用引數類 // args是啟動Spring應用的命令列引數,該引數可以在Spring應用中被訪問 如 -- server.port=8080 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 第二步:根據SpringApplicationRunListeners以及引數來準備環境 // 建立並配置當前SpringBoot應用將要使用的Environment // 並遍歷呼叫所有的SpringApplicationRunListener的environmentPrepared()方法 ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment); // 準備Banner列印器 - 就是啟動Spring Boot的時候列印在console上的ASCII藝術字體 Banner printedBanner = printBanner(environment); // 第三步:建立Spring容器 context = createApplicationContext(); context.setApplicationStartup(this.applicationStartup); // 第四步:Spring容器前置處理 // 主要是在容器重新整理之前的準備動作,包含一個非常關鍵的操作;將啟動類注入容器,為後續開啟自動化配置奠定基礎。 prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // 第五步:重新整理容器 refreshContext(context); // 第六步:Spring容器後置處理 afterRefresh(context, applicationArguments); Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup); } // 第七步:發出結束執行的事件 listeners.started(context, timeTakenToStartup); // 第八步:執行Runners // Runners 執行器用於在服務啟動時進行一些業務初始化操作,這些操作只在伺服器啟動後執行一次 // SpringBoot提供了ApplicationRunner和COmmandLineRunner兩種服務介面 callRunners(context, applicationArguments); } catch (Throwable ex) { // 如果發生異常,則進行處理,並丟擲 IllegalStateException 異常 handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); } // 釋出應用上下文就緒事件 // 表示在前面一切初始化啟動都沒有問題的情況下,使用執行監聽器 SpringApplicationRunListener 持續執行配置好的應用上下文 ApplicationContext, // 這樣整個SpringBoot專案就正式啟動完成了 try { Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime); listeners.ready(context, timeTakenToReady); } catch (Throwable ex) { // 如果發生異常,則進行處理,並丟擲 IllegalStateException 異常 handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } //返回容器 return context; }
從上述原始碼可以看出,專案初始化啟動過程大致包括以下部分:
第一步:獲取並啟動監聽器
this.getRunListeners(args)和listeners.starting()方法主要用於獲取SpringApplication例項初始化過程中初始化的SpringApplicationRunListener監聽器並執行。
第二步:根據SpringApplicationRunListeners以及引數來準備環境
prepareEnvironment(listeners, bootstrapContext, applicationArguments)方法主要用於對專案執行環境進行預設定,同時通過this.configureIgnoreBeanInfo(environment)方法排除一些不需要的執行環境
第三步:建立Spring容器
根據webApplicationType進行判斷, 確定容器型別,如果該型別為SERVLET型別,會通過反射裝載對應的位元組碼,也就是AnnotationConfigServletWebServerApplicationContext,接著使用之前初始化設定的context(應用上下文環境)、environment(專案執行環境)、listeners(執行監聽器)、applicationArguments(專案引數)和printedBanner(專案圖示資訊)進行應用上下文的組裝配置,並重新整理配置
第四步:Spring容器前置處理
這一步主要是在容器重新整理之前的準備動作。設定容器環境,包括各種變數等等,其中包含一個非常關鍵的操作:將啟動類注入容器,為後續開啟自動化配置奠定基礎
第五步:重新整理容器
開啟重新整理spring容器,通過refresh方法對整個IOC容器的初始化(包括bean資源的定位,解析,註冊等等),同時向JVM執行時註冊一個關機鉤子,在JVM關機時會關閉這個上下文,除非當時它已經關閉
第六步:Spring容器後置處理
擴充套件介面,設計模式中的模板方法,預設為空實現。如果有自定義需求,可以重寫該方法。比如列印一些啟動結束log,或者一些其它後置處理。
第七步:發出結束執行的事件
獲取EventPublishingRunListener監聽器,並執行其started方法,並且將建立的Spring容器傳進去了,建立一個ApplicationStartedEvent事件,並執行ConfigurableApplicationContext 的publishEvent方法,也就是說這裡是在Spring容器中釋出事件,並不是在SpringApplication中釋出事件,和前面的starting是不同的,前面的starting是直接向SpringApplication中的監聽器釋出啟。
第八步:執行Runners
用於呼叫專案中自定義的執行器XxxRunner類,使得在專案啟動完成後立即執行一些特定程式。其中,SpringBoot提供的執行器介面有ApplicationRunner 和CommandLineRunner兩種,在使用時只需要自定義一個執行器類實現其中一個介面並重寫對應的run()方法介面,然後SpringBoot專案啟動後會立即執行這些特定程式。
最後繪製一個Spring Boot執行流程圖便於理解