前幾天,面試的時候被問到了SpringBoot的自動裝配的原理。趁着五一的假期,就來整理一下這個流程。
我這里使用的是idea創建的最簡單的SpringBoot項目。
我們都知道,main方法是java的啟動入口,我們在開發SpringBoot項目的時候,他的啟動類如下所示:
/**
* @SpringBootApplication用來標注一個主程序類,說明這是一個springboot應用
*/
@SpringBootApplication
public class SpringbootdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootdemoApplication.class, args);
}
}
從上面代碼可以看出,SpringBoot的啟動類中最主要的就是:@SpringBootApplication和SpringApplication.run()方法。但是SpringBoot是如何找到當前程序的主類,並且獲取類上的注解的呢?
1.加載main主類
private Class<?> deduceMainApplicationClass() {
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
從上面的代碼可以看出,推斷出主應用程序的類,通過跟蹤執行過程中的堆棧信息,找到main函數之后,通過Class.forName的方式反射生成對應的對象(SpringbootdemoApplication)。
2.執行run方法
從run方法開始,一直到prepareContext方法之前,都是在做准備工作
public ConfigurableApplicationContext run(String... args) {
// 創建StopWatch實例,用來記錄SpringBoot的啟動時間
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 創建為null的上下文對象
ConfigurableApplicationContext context = null;
// 創建異常報告器
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 獲取監聽器,讀取META-INF/spring.factories文件中SpringApplicationRunListeners類型存入到集合中
SpringApplicationRunListeners listeners = getRunListeners(args);
// 循環調用監聽starting的方法
listeners.starting();
try {
// 獲取啟動時傳入的參數
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 構建當前環境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 根據環境信息配置要忽略的bean
configureIgnoreBeanInfo(environment);
// 打印SpringBoot的版本和banner,如果需要修改banner,可以在resources下面新建一個banner.txt文件,將內容拷貝進去
Banner printedBanner = printBanner(environment);
// 使用策略方法創建上下文對象,分為以下三種類型的:
// 1.SERVLET:基於servlet的web應用程序運行,並啟動嵌入式的servlet web服務器
// 2.REACTIVE:基於反應式web應用程序運行,並啟動嵌入式的反應式web服務器
// 3.NONE:不以web應用程序運行,也不以嵌入式的web應用程序運行
// 這一步會創建5個bean實例,放在下圖中的beanDefinitionMap中
context = createApplicationContext();
// 獲取異常報告器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 完成整個容器的創建和啟動以及bean的注入功能,
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 最終會調用AbstractApplicationContext#refresh的方法,實際上就是SpringIOC容器的創建過程,並且會進行自動裝配,在沒有使用外部Tomcat項目中,還會在這里創建內置Tomcat WebServer 並啟動
refreshContext(context);
// 在上下文刷新后調用,定義一個空的模板方法,給其他子類實現重寫
afterRefresh(context, applicationArguments);
// StopWatch停止計時,打印springboot啟動花費的時間
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 發布應用上下文啟動完成時間,觸發所有Listener監聽器的start方法
listeners.started(context);
// 執行所有的Runner運行器
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 發布應用上下文就緒事件,觸發監聽器的running方法
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
prepareContext這個方法前面也是在給應用程序的創建做准備工作(環境變量,初始化參數,發布監聽事件,創建bean工廠);
// Load the sources;sources可以是xml文件,也可以是配置類。在這里是使用的配置類(SpringbootDemoApplication)
Set<Object> sources = getAllSources();
// 加載各種bean對象到當前應用程序上下文,即 將SpringbootdemoApplication加載到ApplicationContext中
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
如圖所示,在load方法執行前后的beanDefinitionMap的對比,load方法執行后,將SpringbootdemoApplication加載到了bean工廠中了。

3.自動裝配
到這里,SpringBoot已經將SpringbootdemoApplication這個啟動類加載進bean容器中了。但是SpringBoot是如何引入項目需要的starter呢?
接着前面run方法中的refreshContext(context);這個方法,一路往下找,會發現調用了ConfigurableApplicationContext.refresh()方法,而這個方法是一個模板方法(如下圖),因為我們這里並不是使用的SERVLET和嵌入式的方式運行程序,所以調用的是第一個類中的方法,即 AbstractApplicationContext.refresh();在這個方法中,invokeBeanFactoryPostProcessors(beanFactory);這個方法就會執行自動裝配的邏輯。

順着下面的過程:
1.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); AbstractApplicationContext#746行
2.invokeBeanDefinitionRegistryPostProcessors(); PostProcessorRegistrationDelegate#112行
3.parser.parse(candidates); ConfigurationClassPostProcessor#331行
4.因為主類是使用注解的方式注冊的bean,所有會走ConfigurationClassParser的第175行的parse方法。
5.processImports(configClass, sourceClass, getImports(sourceClass), filter, true); ConfigurationClassParser#311
在這個方法中,通過getImports方法,其內部的 collectImports(sourceClass, imports, visited) 通過遞歸的方式收集所有聲明的@Import導入的類。在這里有一下兩個結果值:
1. AutoConfigurationPackage注解導入的AutoConfigurationPackages.Registrar.class
2. EnableAutoConfiguration注解導入的AutoConfigurationImportSelector.class

這樣就找到了我們常說的AutoConfigurationImportSelector這個類了。
6.第5步解析configration完之后,回到ConfigurationClassParser#193行
this.deferredImportSelectorHandler.process();在這個方法里面實現自動導入的邏輯,點擊往下,會找到下面的這一行代碼,ConfigurationClassParser#809行,通過getImports()獲取需要自動裝配的類。
7.在第6步的getImports()方法中,點擊往下,會找到AutoConfigurationImportSelector#433行。
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
進入到getAutoConfigurationEntry()方法中找到123行的 getCandidateConfigurations():

這個方法里面的 loadFactoryNames()方法,能發現它讀取META-INF/spring.factories下的@EnableAutoConfiguration的配置類,這個方法在run方法之中多次調用,根據傳參不一樣,從META-INF/spring.factories中獲取的內容也不一樣。

從文件中得到了133個配置類,緊接着在進行去重,排除與過濾(並不是所有的都需要)之后,就得到了需要裝配的類,我這里最后還剩26個。
到這里就將需要裝配的類都已經識別到了。
參考文章:
