spring boot原理分析(六):spring boot應用啓動流程綜述
前言
在原理分析(一)已經整體概括了spring boot實現,spring boot主要是在已有Servlet容器+Servlet模板的基礎上進行整合。具體來說包括三種,tomcat + spring mvc的模式是其中的一種,另外兩種分別是Undertow+Servlet和Jetty+Servlet的模式。另外,大部分的外部模塊的加入都是使用spring bean的方式進行動態配置,在原理分析(二)(三)(四)(五)中,分別對項目內外bean自動配置進行了分析,並詳細介紹了常見的幾個例子。至此,基於註解的bean的注入已經分析完了。
本文將會從spring boot
的入口開始分析,展示spring boot在啓動過程中,涉及了哪些組件或者模塊的準備。在介紹spring mvc的DispatcherServlet的文章(tomcat + spring mvc原理(七))中,有過統覽整個流程,再詳細介紹每個局部組件原理的先例。這裏將採用類似的思路。
入口
前面spring boot的綜述已經說過,spring boot應用內部包含了tomcat和spring mvc,既可以打包成war包,部署在外部的tomcat服務下,也可以打包成jar包,直接啓動內置的tomcat,然後裝載編寫的spring mvc的應用。打包成war是spirng mvc開發過程中常用的,不是很新奇。至於如何使用內置的tomcat啓動,則需要研究一番。
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
既然要啓動進程,也就逃脫不了使用java的入口函數main。關於上面@SpringBootApplication註解,上面幾篇文章都是關於它的,有興趣可以去查閱。main函數傳入的參數,就是java啓動時的入參。這裏主要的問題是集中在SpringApplication的構造和它的run方法裏。
構造函數的準備
根據上面SpringApplication的run方法的調用方式,可以很容易推測出run是一個靜態函數。
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);
}
在run方法裏構造了一個SpringApplication,只傳入了在main函數中傳入進來的DemoApplication.class。
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//設置resourceLoader,這裏爲null
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
//設置基礎類
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//設置Web應用類型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//設置上下文Context的初始化加載器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//設置應用事件監聽器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//設置入口類
this.mainApplicationClass = deduceMainApplicationClass();
}
構造函數的調用順序就是上面SpringApplication的由上自下,需要注意的是ResourceLoader傳入時就等於null。在下面的SpringApplication的構造函數中,完成了幾項初始化,依次是:
- 設置resourceLoader
- 設置基礎類
- 設置Web應用類型
- 設置上下文Context的初始化加載器
- 設置應用事件監聽器
- 設置入口類
DemoApplication.class(這個是我自己命名的,名字可以改,都是這個類)作爲基礎類乍一聽很陌生,但是其實在前面的文章裏已經出現了“基礎類包”這個詞(《項目依賴包中容器的自動配置1》的環境上下文:基礎包配置那一段)。@SpringBootApplication是一個@Configuration、@ComponentScan等組合的註解,被這個註解註釋的類所在的包是項目中首要的基礎包,其下所有的子包都會被掃描,如果存在bean會被自動注入到容器中。
Web應用類型確實是沒出現過,構造函數使用deduceFromClasspath方法設置這個值。deduceFromClasspath方法真如其名,Web應用類型是從class中推測出來的,如果class沒有加載,還可以自行從Class默認路徑中找出來加載。源碼貼出來很多,但是內容很簡單,就是根據靜態String中定義的類有沒有出現來判斷是哪種服務類型。
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework." + "web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org." + "springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
這裏判斷了三種類型,SERVLET、REACTIVE和NONE。我們講spring mvc是Servlet容器,所以服務即SERVLET WEB類型。REACTIVE也是一種WEB服務的類型,代表着非阻塞響應式編程,正是spirng mvc不擅長的事,具體可以參考spring-webflux。NONE類型說這個服務不是WEB類型,是其他服務類型。
上下文Context的初始化加載器是用來初始化上下文Context,Context是spring boot加載過程中最重要的模塊,包含了spring boot的配置信息、bean和resource等等,tomcat實例創建工廠和spring mvc的DispatcherServlet都在裏面,可以說上下文Context即萬物。後面會細講。
應用事件監聽器ApplicationListener是spring boot自己定義的,而不是tomcat文章中(tomcat容器動態加載)提到的容器生命週期管理的容器狀態事件監聽器,ApplicationListener是針對spring boot整個應用的事件監聽,有很多種情況下可能需要使用到,擴展性還是挺好的。這個後面也會細講。
設置入口類,大家可能會一頭霧水,不是已經傳入了DemoApplication.class嗎?這裏是因爲DemoApplication中不一定要定義main,同理,也不一定在main函數中直接調用SpringApplication.run。deduceMainApplicationClass採用的方法是使用異常棧逆遞歸倒推,找到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;
}
構造函數裏面還有一個點,是關於spring的,前面也出現過。SpringFactoriesLoader.loadFactoryNames(參考項目依賴包中容器的自動配置1)在getSpringFactoriesInstance中又被用來獲取類的完整類名,然後使用反射獲取構造函數構造了實例,這個部分的細節也值得一看。因爲是技術實現問題,和本主題無關,這裏不多講。
SpringApplication run
public ConfigurableApplicationContext run(String... args) {
//spring提供的運行時間打印工具,記錄啓動時間
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//主角
ConfigurableApplicationContext context = null;
//異常報告
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//設置java headless模式
configureHeadlessProperty();
//獲取應用運行事件監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
//應用參數構造
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//環境配置準備
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//條幅打印
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
//獲取異常上報器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//準備上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//刷新上下文
refreshContext(context);
//刷新上下文後處理
afterRefresh(context, applicationArguments);
//啓動時間打印
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
//啓動後的後置處理
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;
}
上面是SpringApplication run方法的整個流程,由於是異步的,spring boot啓動時,run方法所在的線程跑完結束後,spring boot應用就算完全啓動了。run方法中,有些組件或者模塊涉及內容比較多,這裏我只會大致過下其作用,後面文章會細分析,有些就比較簡單,可以直接解釋清楚。
啓動StopWatch
StopWatch是spring提供的一個工具,用來監控和打印運行時間,使用過程包括構造、start和stop,在run方法裏都有提現。最終打印是在StartupInfoLogger方法裏,下面是打印出來的效果。
異常報告
SpringBootExceptionReporter是可以用來獲取啓動過程中的異常,並上報打印。後面使用了getSpringFactoriesInstances方法進行獲取所有異常上報器,在handleRunFailure方法中使用了這些實例。這個組件需要更加詳細的分析。
java headless模式
java的服務器開發時會使用這個模式。服務器可能缺少顯示設備、鍵盤或鼠標這些外設,但又需要使用他們提供的功能,生成相應的數據,提供給客戶端。設置這個模式,是告訴程序,不能指望硬件設備提供這些功能,需要依靠系統的計算能力模擬這些特性。設置方法就是:
System.setProperty("java.awt.headless", "true");
應用運行事件監聽器
SpringApplicationRunListener負責啓動過程中,運行事件的監聽。需要注意,和前面說到的ApplicationListener完全不同,不要混淆。在run方法運行過程中,能夠看到SpringApplicationRunListeners的構造獲取和starting、started、running。starting、started、running方法內部都是遍歷所有SpringApplicationRunListener,單獨調用每個SpringApplicationRunListener相應名字的方法實現的。這個組件也會單獨細講。
應用參數
ApplicationArguments是spring boot對應用參數進行了封裝,輸入參數是args。
環境配置
Environment是指應用程序的運行環境,包括profile和properties配置,properties配置不僅包括項目內的properties文件,還有JVM system properties、操作系統環境變量等。由此構造ConfigurableEnvironment需要傳入args也順理成章。
條幅
Banner是指spring boot啓動過程中,會在日誌中打印一個條幅,顯示本服務基於spring boot。這個應該不用多說吧,炫酷。
spring boot上下文
spring boot的上下文Context是spring boot中最重要的一個模塊,內部包含了spring boot服務運行過程中各種重要的信息,比如beanFactory、註冊的bean、environment等等。
prepareContext方法中對Context進行了初始化,設置了一些Context中的一些內容,比如environment、banner,最後還根據基礎包記錄primarySources,加載了基礎包中的bean。refreshContext方法中調用了context的refresh方法,刷新上下文,創建並啓動了tomcat實例,加載了spring mvc的Servlet,可以說服務的所有啓動工作都是在這個方法中完成的。afterRefresh是一個模板方法,用意是繼承SpringApplication的子類如果想在Context的refresh完成之後做點事,可以實現這個方法。
所有關於Context的內容,後續會用單獨篇幅來介紹。
啓動後置處理
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
callRunners(context, applicationArguments)中加載了ApplicationRunner和CommandLineRunner兩種類型的子類實例bean,並調用了它們的run方法。ApplicationRunner和CommandLineRunner是用於在spring boot啓動完成後處理一些用戶自己定義的邏輯,比如加載文件、配置數據庫等等。使用方式就是繼承這兩個類,重載裏面的run方法。至於這兩個類的區別是run方法的輸入參數不一樣,ApplicationRunner中run方法的參數爲ApplicationArguments,而CommandLineRunner接口中run方法的參數爲String數組。