Spring Boot的自動配置原理及啟動流程源碼分析


概述

Spring Boot 應用目前應該是 Java 中用得最多的框架了吧。其中 Spring Boot 最具特點之一就是自動配置,基於Spring Boot 的自動配置,我們可以很快集成某個模塊,不用加 xml 之類的配置文件,大部分情況下甚至什么配置都不用寫,直接引起 maven 包即可使用。

之前我也僅僅會用的,但是它怎么實現的是沒有詳細了解,都是通過看別人的文章大概知道了流程,但是這樣好像總是記得不是很清楚,所以就打算也自己也輸出一份,方便自己將來記憶另外也方便自己查看。因為之前搜索的這個知識點的時候,感覺其他文章總有一些講的不是很全面。

思考一下?如果讓要自己實現自動配置要怎么實現呢?🧐僅僅通過引入 maven 依賴包,即可盡量達到最大限度的默認配置?

自己猜想:有個機制自動掃描引入來的依賴的包,自動將包里的某些類進行實例化並注入到Spring Boot中並初始化好設置好的一些參數。

實際上 Spring Boot 的自動配置也是這樣,不過它自定義了自己的一套的可拓展,具備通用性模塊去掃描並進行初始化。

Spring Boot 啟動源碼中看自動配置

Spring Boot 自動配置是在應用啟動的時候就會完成的,所以在項目啟動的源碼中包含着如何實現自動配置的原理。

一個簡單的Spring Boot 應用的啟動入口類可以這樣定義,主要的依賴於@SpringBootApplication這個注解,標記它是Spring Boot 的啟動類。

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

我們跟蹤進入這個注解可以看到它主要包含三個元注解 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan,實際上也可以通過在啟動類中只加上這三個注解就可以實現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 : 標記該類可以用來作為 Spring Boot 啟動類的配置類,內部是 @Configuration 注解,可以用來加載 bean 到 Spring 上下文中
@ComponentScan :主要為配置組件掃描加載@Configuration類的包路徑,默認為當前目錄下的所有包
@EnableAutoConfiguration :設置自動配置,會自動加載某些特定的配置類並進行初始化處理,自動配置的核心就在這個注解里

@Enabe* 這類的注解是都是啟用什么功能的注解,里面包含了 @Import注解,通過該注解定義某些類繼承 ImportSelectorImportBeanDefinitionRegistrar 接口,在實現類中將某些 bean 完整類名以列表返回即可將這些類注冊到Spring 容器中和進行一些配置的初始化行為。詳細可以參考:Spring Boot @Enable注解源碼解析及自定義@Enable 這篇我自己之前寫過的文章

@Import 注解支持導入普通 java 類,並將其聲明成一個bean。主要用於將多個分散的 java config 配置類融合成一個更大的 config 類。在這里的作用可以將其他配置類導入到Spring Boot Application的自動配置類中。

而這里的@EnableAutoConfiguration@Import 的value 為:AutoConfigurationImportSelector 類,從該類的類名(如果實現了接口,接口名在類名的后面),我們可以知道該類實現了 ImportSelector 接口。該類中最主要的方法是:getAutoConfigurationEntry,主要通過該方法來返回類路徑讓Spring 實例化注入到容器中。

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		// 獲取到注解的屬性,標記是哪些注解進入的
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		// 獲取配置類列表的核心方法,跟蹤進入可以發現是從 SpringFactoriesLoader#loadFactoryNames 中獲取自動配置類的列表,傳遞的參數為:`EnableAutoConfiguration.class`信息和 ClassLoader 
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		// 去除重復的配置類
		configurations = removeDuplicates(configurations);
		// 獲取要排除的配置類,然后排除掉不進行加載
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = filter(configurations, autoConfigurationMetadata);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

該方法的核心邏輯是通過 SpringFactoriesLoader#loadFactoryNames 從 各個jar 包中的META-INF/spring.factories 配置文件中獲取key 為org.springframework.boot.autoconfigure.EnableAutoConfiguratio 的配置類路徑列表,然后進行去重等邏輯判斷返回實際應該加載的配置類列表信息。(SpringFactoriesLoader為Spring 定義的工具類,主要作用就是加載所有jar包中的META-INF/spring.factories配置文件中定義的類信息)

基本每個定義的 **boot-starter 包都會包含 META-INF/spring.factories文件,里面定義了自動配置應該加載配置類的路徑。

META-INF/spring.factories 配置文件

META-INF/spring.factories 文件為key-value 文件格式的配置文件,配置了應該加載的類信息,在自動配置模板中會把要加載的自動配置類定義在該文件中。

打開spring-boot-autoconfigure包下的 spring.factories文件我們可以清晰地看到該文件的結構
autoConfig包下的spring.factories

自動配置類定義在org.springframework.boot.autoconfigure.EnableAutoConfiguration 的key下面,Spring Boot在加載的時候會取這個 key 下面的類去加載到Spring Boot 中的自動配置模塊,並自動實例化。

自動配置必須通過將自動配置類定義在spring.factories 中由 spring boot 去加載。
參考:Creating your own auto-configuration,https://docs.spring.io/spring-boot/docs/2.0.0.M3/reference/html/boot-features-developing-auto-configuration.html

但是如果我定義了的類就加載,那樣我要加載的類不就很多?或者我想某些類在滿足某些條件下才進行加載?

Spring 為此定義了@Conditional 注解,該注解定義在自動配置類上,可以實現在某些條件下才加載該類,例如:@ConditionalOnPropert 注解可以根據你配置文件中的定義的配置來決定是否加載該類。

Spring Boot 啟動流程

經過上面,我們知道了自動配置模塊是在Spring Boot 啟動的時候去加載配置類文件進行配置的,但是具體在什么時候會去加載呢?是在tomcat 啟動前,還是啟動后?

我們需要先知道Spring Boot 的啟動流程,這樣也方便我們更加了解Spring Boot 的自動配置流程。

  1. 首先創建 一個SpringApplication對象,在創建的過程中對資源進行獲取:判斷該應用應該是什么類型;使用SpringFactoriesLoader查找並加載注冊所有有用的ApplicationContextInitializerApplicationListener到Spring容器中;獲取main 方法的對象類
  2. 然后由創建出來的對象SpringApplication執行run方法
  3. run方法的開始會啟動一個時間監視器,統計項目啟動所用的時間
  4. 初始化 ConfigurableApplicationContext 上下文和Spring Boot 啟動異常收集類集合
  5. 通過SpringFactoriesLoaderMETA-INF/Spring.factories 中獲取並實例化SpringApplicationRunListener類和調用他們的starting方法,用於通知他們“Spring Boot開始啟動了” (SpringApplicationRunListener是只在Spring Boot 啟動過程中接受不同時間點的事件的監聽者,用於在Spring Boot 的run方法執行不同過程中監聽執行不同的方法)
  6. 創建並配置Spring Boot的環境配置 (注意這里會重新執行一次run方法,如果是debug的時候,需要留意這次run 方法不同於第一次的run)
  7. 打印Banner
  8. 創建Spring ApplicationContent 上下文類
  9. 創建 SpringBootExceptionReporter 類,用於存放啟動的時候錯誤信息
  10. 遍歷調用 SpringApplicationRunListenercontextLoaded() 通知 所有SpringApplicationRunListener,告訴它們ApringContext 加載完成。並加載ConfigurableEnvironmentConfiguration 類 到Springcontext上下文中
  11. 調用ApplicationContextrefresh()方法,進行自動配置模塊的加載,啟動Tomcat容器,加載並初始化數據源,kafka 等中間件組件,執行 @Scheduled 注解 等
  12. 計時器停止計時;通知 SpringApplicationRunListener Spring Boot 的上下文刷新完成了
  13. 查找實現了ApplicationRunnerCommandLineRunner 接口的類,並執行 它們的 run 方法
  14. 最后再遍歷執行 SpringApplicationRunListenerfinished() 方法,通知 Spring Boot 啟動完成。如果有報錯會拋出報錯信息。

基本一個Spring Boot 應用就啟動完成了。

查看其執行步驟會發現比較復雜,但是有很多步驟是進行事件通知和進行監控的,如果事件監聽和監控簡化一下,可以得到如下圖的簡單的Spring Boot 啟動流程圖:
SpringBoot 應用啟動流程圖

run的源碼:

	public ConfigurableApplicationContext run(String... args) {
	    // 啟動時間監視器,統計執行時間
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		// 從META-INF/Spring.factories 配置中獲取並通過BeanUtils 實例化 SpringApplicationRunListeners 和調用 starting方法
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
		    // 封裝傳遞過來的參數
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 創建並配置SpringBoot 的環境配置(包含PropertySource和Profile),里面會再創建一個SpringApplication 並執行run方法
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			// 打印banner
			Banner printedBanner = printBanner(environment);
			// 創建Spring ApplicationContent 上下文
			context = createApplicationContext();
			// 創建SpringBootExceptionReporter類 用於捕捉報告失敗的原因給用戶
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			// 在加載應用程序上下文但在刷新它  之前遍歷調用 SpringApplicationRunListener 的 contextLoaded() 通知 所有SpringApplicationRunListener,告訴它們ApringContext 加載完成。並加載ConfigurableEnvironment和Configuration 類 進入到Springcontext上下文中
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 實際調用的是AbstractApplicationContext#refresh() 方法,這里刷新上下文(加載或更新 持久化的配置),並且進行自動配置模塊的加載,啟動Tomcat容器,加載並初始化數據源,kafka 等中間件組件,執行 @Scheduled 注解 等
			refreshContext(context);
			// 在SpringContext 刷新后執行的操作,目前該方法沒有執行任何操作,估計是為了后期擴展
			afterRefresh(context, applicationArguments);
			// 計時器停止計時
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 通知所有應用啟動監聽器 注冊在應用啟動事件里,注冊進Spring Boot 的上下文中,並通知SpringBoot 已經刷新完上下文信息
			listeners.started(context);
			// 初始化實現了ApplicationRunner或CommandLineRunner 的接口並執行  run 方法(一般都是用於SpringBoot 啟動后執行的方法,兩者的區別為,后者接受的參數為原始的字符串格式,前者為 ApplicationArguments 類形式的參數)
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

org.springframework.context.support.AbstractApplicationContext#refresh() 源碼 (源碼注釋寫得很清楚😂,基本不用怎么看,我主要標注一下一些重要的)

    @Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();
			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);
			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);
				// Invoke factory processors registered as beans in the context.  這里會進行自動配置類的加載和實例化,跟着源碼debug就可以看到
				invokeBeanFactoryPostProcessors(beanFactory);
				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				// Initialize message source for this context.
				initMessageSource();
				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();
				// Initialize other special beans in specific context subclasses.  主要創建啟動  webService  一般為Tomcat 容器
				onRefresh();
				// Check for listener beans and register them.
				registerListeners();
				// Instantiate all remaining (non-lazy-init) singletons.  初始化所有剩余的單例bean。
				finishBeanFactoryInitialization(beanFactory);
				// Last step: publish corresponding event.  發布相應的事件
				finishRefresh();
			}
			catch (BeansException ex) {
                   。。。    
            }

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

對着這些解析來看,可以回答開頭小節的問題了,自動加載模塊是在Tomcat啟動前執行的。

注意,根據源碼的時候看起來run 方法執行了兩次,但是是以第二次執行的流程為最終版的,第一次執行的只是加載環境等相關信息的時候執行
基於SpringBoot 版本 2.2.5.RELEASE。剛開始的時候,自己追源碼的時候,發現比較難看懂的,看了好幾次,每次都跟蹤進入,只能大概看懂,后面我結合網上博客以及相關書籍的講解來結合源碼來分析就快很多了,也明白了很多。不過有些博客沒有寫上是參考那里來寫的,當時也想着找最原始(權威一點)的出處,以免原來博客就解釋錯了,這個時候也耗費了挺多時間,最終找到了《SpringBoot揭秘 快速構建微服務體系》 這本書里面有比較詳細和原始的Spring Boot 啟動解析,挺多博客是參考了這本書的。可以在關注 公總號 “CurdBoys” 后回復“SpringBoot揭秘”獲取到該本書的地址。

總結

工作兩年多了,發現自己還沒怎么看過SpringBoot 啟動源碼(只記得之前秋招的時候看過相關博客,背過相關面試題,沒有自己手動debug過🙃),最近偶爾有時間來研究一下,還是學到了很多東西,如果自己沒有跟着源碼去debug估計對於SpringBoot的理解只能理解表面。自己去跟源碼運行的時候,可以發現很多了信息是自己之前沒有留意過的,也可以幫忙自己的深刻理解某些Spring框架組件的用法,以防踩坑😅。例如:繼承了ApplicationRunner或CommandLineRunner接口的類的會在項目啟動的時候運行,之前有些方法需要在項目啟動后就立即運行的,網上搜索到是繼承 CommandLineRunner 接口的就可以了,但是當時並沒有在意它具體是怎么實現。看了源碼才知道,原來是它是在run方法中的最后一步去查找實現類並去執行的,就是說如果實現類執行的方法出錯的話會導致SpringBoot 無法正常啟動(這個當時並沒有留意到這個問題☹,也會看了別人的文章聯想到這個問題,感覺挺有意思的)。

另外,最后發現自己寫這篇文章那么久,結果越寫發現不懂的越多🙄,Spring Boot 啟動過程涉及的東西太多了,感覺自己的只是簡單寫了些皮毛,如果自己要去的看的話,還是建議找一下《SpringBoot揭秘 快速構建微服務體系》書全面地看一下(這里面講了很多知識點)和對照着源碼手動debug一下會更加清楚點。

參考:

  1. SpringBoot自動配置的原理詳解 , https://zhuanlan.zhihu.com/p/136469945
  2. Spring boot(二):啟動原理解析 , https://www.cnblogs.com/xiaoxi/p/7999885.html
  3. 為什么我要寫spring.factories文件? , https://blog.csdn.net/SkyeBeFreeman/article/details/96291283
  4. Creating your own auto-configuration ,https://docs.spring.io/spring-boot/docs/2.0.0.M3/reference/html/boot-features-developing-auto-configuration.html
  5. 源碼分析之Spring Boot如何利用Spring Factories機制進行自動注入 , https://blog.csdn.net/evasnowind/article/details/108647194?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242
  6. SpringApplication https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-spring-application.html
  7. Standard and Custom Events https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#context-functionality-events
  8. 《SpringBoot揭秘 快速構建微服務體系 》
  9. SpringBoot內置Tomcat啟動時間 https://blog.csdn.net/luxiaoruo/article/details/106637335
  10. Spring筆記——通過源碼理解Spring 中事件發布 https://blog.csdn.net/qq_19865749/article/details/70186889

文章雜談

好像很久沒更新文章了,上次說要以快速過一個知識點方式寫文章的,后面實際寫的時候的發現的,自己還是陷入了想了解清楚的每一處的代碼的含義的誤區。導致寫這篇文章的時候中間有個地方卡住了很久,也就拖更了很久,再加上最近心態不太好(有暫時不方便說的不可控因素😢),陷入了低迷期,心態爆炸啊。今天是5,1假期的最后一天了,這個5,1假期也沒有大部分時間都在家躺平了🤐,最近兩天才調整回來狀態,好像最近的天氣都讓人很想睡覺。就這樣吧,下篇文章再見!

CrudBoys


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM