目錄
- 一、過程簡介
- 二、過程流程圖
- 三、源碼分析
- 1、運行 SpringApplication.run() 方法
- 2、確定應用程序類型
- 3、加載所有的初始化器
- 4、加載所有的監聽器
- 5、設置程序運行的主類
- 6、開啟計時器
- 7、將 java.awt.headless 設置為 true
- 8、獲取并啟用監聽器
- 9、設置應用程序參數
- 10、準備環境變量
- 11、忽略 Bean 信息
- 12、打印 banner 信息
- 13、創建應用程序的上下文
- 14、實例化異常報告器
- 15、準備上下文環境
- 15.1、實例化單例的 beanName 生成器
- 15.2、執行初始化方法
- 15.3、將啟動參數注冊到容器中
- 16、refresh 刷新上下文(實例化 Bean)
- 16.1、refresh() 方法內容
- 16.2、Bean 對象的創建
- 17、刷新上下文后置處理
- 18、結束計時器
- 19、發布上下文準備就緒事件
- 20、執行自定義的 run 方法
最好的學習方式就是帶著問題學習,在分析 SpringBoot 的啟動過程前,先問大家兩個問題:
-
在啟動過程中,SpringBoot 是在哪一步實例化 Bean 的?
答案:在本文的第 16 步 refresh() 刷新上下文的時候實例化的。
-
ApplicationContext 作為一個 IOC 容器,底層是通過什么方式來存儲實例化好的 Bean 呢?
答案:ApplicationContext 是先使用 Set 集合將 BeanDefinition 存儲起來,然后再將不是抽象的、單例的、非懶加載的類進行實例化,然后存放到 Map 集合中統一管理。
文章中使用的源碼版本:
- spring-boot:
2.2.x
- spring-framework:
5.2.x
話不多說,下面就讓我們開始了解 SpringBoot 的啟動過程吧。
一、過程簡介
首先,SpringBoot 啟動的時候,會構造一個 SpringApplication 的實例,構造時會進行初始化的工作。初始化的時候會做以下幾件事情:
- 把參數 sources 設置到 SpringApplication 屬性中,這個 sources 可以是任何類型的參數;
- 判斷是否是 web 程序,并設置到 webEnvironment 的 boolean 屬性中;
- 創建并初始化 ApplicationInitializer,設置到 initializers 屬性中;
- 創建并初始化 ApplicationListener,設置到 listeners 屬性中;
- 初始化主類 mainApplicationClass。
其次,SpringApplication 構造完成之后調用 run 方法,啟動 SpringApplication。run 方法執行的時候會做以下幾件事:
- 構造一個 StopWatch 計時器,用來記錄 SpringBoot 的啟動時間;
- 初始化監聽器,獲取 SpringApplicationRunListeners 并啟動監聽,用于監聽 run 方法的執行。
- 創建并初始化 ApplicationArguments,獲取 run 方法傳遞的 args 參數。
- 創建并初始化 ConfigurableEnvironment(環境配置)。封裝 main 方法的參數,初始化參數,寫入到 Environment 中,發布 ApplicationEnvironmentPreparedEvent(環境事件),做一些綁定后返回 Environment。
- 打印 banner 和版本。
- 構造 Spring 容器(ApplicationContext)上下文。先填充 Environment 環境和設置的參數,如果 application 有設置 beanNameGenerator(bean)、resourceLoader(加載器)就將其注入到上下文中,調用初始化的切面,發布 ApplicationContextInitializedEvent(上下文初始化)時間。
- SpringApplicationRunListeners 發布 finish 事件。
- StopWatch 計時器停止計時,日志打印總共啟動的時間。
- 發布 SpringBoot 程序已啟動事件(started())。
- 調用 ApplicationRunner 和 CommandLineRunner。
- 最后發布就緒事件 ApplicationReadyEvent,標志著 SpringBoot 可以處理接收的請求了(running())。
二、過程流程圖
由此看來,SpringBoot 的啟動過程還是挺多的,下面我們結合源碼,詳細分析講解啟動過程中的步驟。
三、源碼分析
1、運行 SpringApplication.run() 方法
可以肯定的是,所有的標準 SpringBoot 應用都是從 run 方法開始的。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class SpringbootDemoApplication {public static void main(String[] args) {// 啟動應用SpringApplication.run(SpringbootDemoApplication.class, args);}}
進入 run 方法后,會 new 一個 SpringApplication 上下文對象,創建這個對象的構造方法做了一些準備工作,第 2 ~ 5 步就是構造函數里面做的事情。
/*** Static helper that can be used to run a {@link SpringApplication} from the* specified source using default settings.* @param primarySource the primary source to load* @param args the application arguments (usually passed from a Java main method)* @return the running {@link ApplicationContext}*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[] { primarySource }, args);
}
另外補充一下,SpringBoot 除了 SpringApplication.run() 方法啟動之外,還可以通過 AnnotationConfigApplicationContext
指定配置類啟動,這里就不展開說明了。
2、確定應用程序類型
在 SpringApplication 的構造方法內,首先會通過 WebApplicationType.deduceFromClasspath(); 方法判斷當前應用程序的容器,默認使用的是 Servlet 容器,除了 Servlet 之外,還有 NONE 和 REACTIVE(響應式編程)。
/*** Create a new {@link SpringApplication} instance. The application context will load* beans from the specified primary sources (see {@link SpringApplication class-level}* documentation for details. The instance can be customized before calling* {@link #run(String...)}.* @param resourceLoader the resource loader to use* @param primarySources the primary bean sources* @see #run(Class, String[])* @see #setSources(Set)*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));this.webApplicationType = WebApplicationType.deduceFromClasspath();setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));this.mainApplicationClass = deduceMainApplicationClass();
}
3、加載所有的初始化器
這里加載的初始化器是 SpringBoot 自帶的初始化器,從 META-INFO/spring.factories 配置文件中加載的,那么這個文件在哪呢?自帶的有2個,分別在源碼的 jar 包的 spring-boot-autoconfigure 項目和 spring-boot 項目里面各有一個:
spring.factories 文件里面,可以看到開頭是 org.springframework.context.ApplicationContextInitializer 接口就是初始化器了:
當然,我們也可以自己實現一個自定義的初始化器:實現 ApplicationContextInitializer 接口即可。
MyApplicationContextInitializer.java
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;/*** 自定義初始化器*/
public class MyApplicationContextInitializer implements ApplicationContextInitializer {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {System.out.println("MyApplicationContextInitializer.initialize()");}
}
在 resources 目錄下添加 META-INF/spring.factories 配置文件,目錄如下,將自定義的初始化器注冊進去:
org.springframework.context.ApplicationContextInitializer=\
com.demo.application.MyApplicationContextInitializer
啟動 SpringBoot 后,就可以看到控制臺打印的內容了,在這里我們可以很直觀地看到它地執行順序,是在打印 banner 的后面執行的:
4、加載所有的監聽器
加載監聽器也是從 META-INF/spring.factories 配置文件中加載的,與初始化不同的是,監聽器的加載是為了實現 ApplicationListener 接口的類。
自定義監聽器也跟自定義初始化器一樣,這里不再舉例。
5、設置程序運行的主類
deduceMainApplicationClass(); 這個方法僅僅是找到 main 方法所在的類,為后面的掃包做準備,deduce 是推斷的意思,所以準確的說,這個方法作用是推斷出主方法所在的類。
6、開啟計時器
程序運行到這里,就已經進入了 run 方法的主體了,第一步調用的 run 方法是靜態方法,那個時候還沒實例化 SpringApplication 對象,現在調用的 run 方法是非靜態的,是需要實例化后才可以調用的,進來后首先會開啟計時器,這個計時器有什么作用呢?顧名思義,就是用來記錄 SpringBoot 啟動時長的,核心代碼如下:
// 實例化計時器
StopWatch stopWatch = new StopWatch();
// 開始計時
stopWatch.start();
run 方法代碼段截圖:
7、將 java.awt.headless 設置為 true
這里將 java.awt.headless 設置為 true,表示運行在服務器端,在沒有顯示和鼠標鍵盤的模式下照樣可以工作,模擬輸入輸出設備功能。
做了這樣的操作后,SpringBoot 想干什么呢?其實是像設置該應用程序,即使沒有檢測到顯示器,也允許其啟動,對于服務器來說,是不需要顯示器的,所以要這樣設置。
方法主體如下:
private void configureHeadlessProperty() {System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
通過方法可以看到,setProperty() 方法里面有有個 getProperty(); 這不是多此一舉嗎?其實 getProperty() 方法里面有2個參數,第一個 key 值,第二個默認值,意思是通過 key 值查找屬性值,如果屬性值為空,則返回默認值 true;保證了一定有值的情況。
8、獲取并啟用監聽器
這一步,通過監聽器來實現初始化的基本操作,這一步做了2件事:
1)創建所有 Spring 運行監聽器,并發布應用啟動事件。
2)啟用監聽器。
9、設置應用程序參數
將執行 run 方法時傳入的參數封裝成一個對象。
這里只是將參數封裝成對象,沒啥好說的,對象的構造函數如下:
public DefaultApplicationArguments(String... args) {Assert.notNull(args, "Args must not be null");this.source = new Source(args);this.args = args;
}
這里的 args 參數其實就是 main 方法里面執行靜態 run 方法時傳入的參數。
10、準備環境變量
準備環境變量,包括系統屬性和用戶配置的屬性,執行的代碼塊在 preparedEnvironment 方法內。
打了斷點之后可以看到,它將 Maven 和系統的環境變量都加載進來了。
11、忽略 Bean 信息
configureIgnoreBeanInfo() 這個方法是將 spring.beaninfo.ignore 的默認值設置為 true,意思是忽略 Java Bean 的信息解析:
private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());}
}
當然也可以在配置文件中添加以下配置來設為 false。
spring.beaninfo.ignore=false
當 spring.beaninfo.ignore 配置被設置為 false 時,Spring 框架會解析 Java Bean 的信息,包括屬性、方法、事件等,以便在運行時進行操作。
需要注意的是,在現在的 Java 環境中,Java Bean 的信息解析通常不再需要,而且會對性能產生負面影響。因此,大多數形況下,無需關注或更改該配置。
12、打印 banner 信息
顯而易見,這個流程就是用來打印控制臺那個很大的 Spring 的 banner 圖案,就是下面這個東西:
那他在哪里打印的呢?是在 SpringBootBanner.java 里面打印的,這個類實現了 Banner 接口,而且 banner 信息時直接在代碼里面寫死的。
13、創建應用程序的上下文
實例化 ApplicationContext(應用程序的上下文),調用 createApplicationContext() 方法,這里就使用反射創建對象,沒什么好說的。
14、實例化異常報告器
異常報告器時用來捕獲全局異常使用的,當 SpringBoot 應用程序在發生異常時,異常報告器會將其捕捉并作響應處理,在 spring.factories 文件里配置了默認的異常報告器:
需要注意的是,這個異常報告器只會捕獲啟動過程拋出的異常,如果是在啟動完成后,在用戶請求時報錯,異常捕獲器不會捕獲請求中出現的異常。
了解了遠離了,接下來我們自己配置一個異常報告器試試。
創建 MyExceptionReporter.java 類,繼承 SpringBootExceptionReporter 接口。
import org.springframework.boot.SpringBootExceptionReporter;
import org.springframework.context.ConfigurableApplicationContext;/*** 自定義異常報告器*/
public class MyExceptionReporter implements SpringBootExceptionReporter {private ConfigurableApplicationContext context;// 必須要有一個有參構造函數,否則啟動會報錯MyExceptionReporter(ConfigurableApplicationContext context) {this.context = context;}@Overridepublic boolean reportException(Throwable failure) {System.out.println("MyExceptionReporter.reportException() is called.");failure.printStackTrace();// 返回false會打印詳細 SpringBoot 報錯信息,返回true則紙打印異常信息。return false;}
}
在 spring.factories 文件中注冊異常報告器。
# Error Reporters 異常報告器
org.springframework.boot.SpringBootExceptionReporter=\
com.demo.application.MyExceptionReporter
然后我們在 application.yml 中把端口設置為一個很大的值(端口的最大值為65535),我們設置為5個8:
server:port: 88888
啟動后,控制臺打印截圖如下:
15、準備上下文環境
這里準備的上下文環境是為了下一步刷新做準備的, 里面還做了一些額外的事情:
15.1、實例化單例的 beanName 生成器
在 postProcessApplicationContext(context); 方法里面。使用單例模式創建了 BeanNameGenerator 對象,其實就是 beanName 生成器,用來生成 bean 對象的名稱。
15.2、執行初始化方法
初始化方法有哪些呢?還記得第3步里面加載的初始化器嗎?其實是執行第3步加載出來的所有初始化器,實現了 ApplicationContextInitializer 接口的類。
15.3、將啟動參數注冊到容器中
這里將啟動參數以單例的模式注冊到容器中,是為了以后方便拿來使用,參數的 beanName 為:springApplicationArguments。
16、refresh 刷新上下文(實例化 Bean)
刷新上下文就到了 Spring 的范疇了,這里進行了自動裝配和啟動 tomcat,以及其他 Spring 自帶的機制。這里我們主要看一下 refresh() 方法包含了哪些內容,以及 Bean 對象的創建具體是如何進行的?
16.1、refresh() 方法內容
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {//為容器初始化做準備prepareRefresh();// 解析xml和注解ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 給BeanFacory設置屬性值以及添加一些處理器,即準備Spring的上下文環境prepareBeanFactory(beanFactory);try {// 由子類實現對BeanFacoty的一些后置處理postProcessBeanFactory(beanFactory);/** BeanDefinitionRegistryPostProcessor* BeanFactoryPostProcessor* 完成對這兩個接口的調用*/invokeBeanFactoryPostProcessors(beanFactory);/** 把實現了BeanPostProcessor接口的類實例化,并且加入到BeanFactory中*/registerBeanPostProcessors(beanFactory);/** 國際化*/initMessageSource();//初始化事件管理類initApplicationEventMulticaster();//這個方法著重理解模板設計模式,因為在springboot中,這個方法是用來做內嵌tomcat啟動的onRefresh();/** 往事件管理類中注冊事件類*/registerListeners();/** 1、bean實例化過程* 2、依賴注入* 3、注解支持* 4、BeanPostProcessor的執行* 5、Aop的入口*/finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();} finally {resetCommonCaches();}}
}
16.2、Bean 對象的創建
當前面的準備工作做好后,就開始初始化 Bean 實例了,也就是 finishBeanFactoryInitialization 方法所作的事。不過這里可不是根據 BeanDefinition 去 new 一個對象就完了,它包含了以下幾個工作:
- 初始化實例。
- 解析 @PostConstruct、@PreDestroy、@Resource、@Autowired、@Value 等注解。
- 依賴注入。
- 調用 BeanPostProcessor 方法。
- AOP 入口。
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {......//重點看這個方法// Instantiate all remaining (non-lazy-init) singletons.beanFactory.preInstantiateSingletons();
}public void preInstantiateSingletons() throws BeansException {if (logger.isTraceEnabled()) {logger.trace("Pre-instantiating singletons in " + this);}// Iterate over a copy to allow for init methods which in turn register new bean definitions.// While this may not be part of the regular factory bootstrap, it does otherwise work fine.// xml解析時,講過,把所有beanName都緩存到beanDefinitionNames了List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);// Trigger initialization of all non-lazy singleton beans...for (String beanName : beanNames) {// 把父BeanDefinition里面的屬性拿到子BeanDefinition中RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);//如果不是抽象的,單例的,非懶加載的就實例化if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {//判斷bean是否實現了FactoryBean接口,這里可以不看if (isFactoryBean(beanName)) {Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);if (bean instanceof FactoryBean) {final FactoryBean<?> factory = (FactoryBean<?>) bean;boolean isEagerInit;if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)((SmartFactoryBean<?>) factory)::isEagerInit,getAccessControlContext());}else {isEagerInit = (factory instanceof SmartFactoryBean &&((SmartFactoryBean<?>) factory).isEagerInit());}if (isEagerInit) {getBean(beanName);}}}else {//主要從這里進入,看看實例化過程getBean(beanName);}}}
}
其他詳細內容,可以參考下這位大佬的文章:Spring的Bean實例化原理,這一次徹底搞懂了!
17、刷新上下文后置處理
afterRefresh 方法是啟動后的一些處理,留給用戶擴展使用,目前這個方法里面是空的。
/*** Called after the context has been refreshed.* @param context the application context* @param args the application arguments*/
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
18、結束計時器
到這一步,SpringBoot 其實就已經完成了,計時器會打印 SpringBoot 的啟動時長。
在控制臺看到啟動還是挺快的,2秒多就啟動完成了。
19、發布上下文準備就緒事件
告訴應用程序,我已經準備好了,可以開始工作了。
20、執行自定義的 run 方法
這是一個擴展功能,callRunners(context, applicationArguments) 可以在啟動完成后執行自定義的 run 方法。有 2 種實現方式:
- 實現 ApplicationRunner 接口;
- 實現 CommandLineRunner 接口。
接下來我們驗證一把,為了一次性驗證全,我們把這2種方式都放在同一個類里面。
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;/*** 自定義啟動后執行*/
@Component
public class MyRunner implements ApplicationRunner, CommandLineRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {System.out.println(" 我是自定義的 run 方法1,實現 ApplicationRunner 接口即可運行");}@Overridepublic void run(String... args) throws Exception {System.out.println(" 我是自定義的 run 方法2,實現 CommandLineRunner 接口即可運行");}
}
啟動 SpringBoot 后就可以看到控制臺打印的信息了。
整理完畢,完結撒花~ 🌻
參考地址:
1.SpringBoot啟動過程,https://blog.csdn.net/qq_42259971/article/details/127151316
2.Spring的Bean實例化原理,這一次徹底搞懂了!https://zhuanlan.zhihu.com/p/198087901