面試官:講一下SpringBoot的啟動流程

現在Java的專案開發中,幾乎都會引入Spring框架,甚至有人說java開發現在就是在面向Spring程式設計。基於Spring提供了很多的功能,可以使我們可以方便地構建出低耦合、易擴充套件、易維護的應用,比如:IOC、AOP、Spring Web、事務等,。但是如果我們使用原生的Spring framework來開發,我們就需要自己引入許多Spring的依賴包,如果少引入某個依賴包,就會導致我們的應用構建失敗,而且如果我們需要引入一些第三方元件,比如:mybatis、redis等,我們也需要相應的去尋找合適的依賴包,這個過程是很繁瑣而且需要消耗比較多的成本。

如果我們是使用SpringBoot來開發,我們只需要引入相應的*-starter包,這個*-starter依賴包便可以幫我們把相應需要的依賴包自動匯入到專案中,這邊便是SpringBoot提供的快速啟動功能;SpringBoot還提供了自動配置功能,幫助我們自動整合配置了很多第三應用的預設配置,使得我們只需要修改很少的配置甚至不需要修改任何配置就可以將第三方元件引入進來,而且SpringBoot還內建了Web伺服器,比如:Tomcat、Jetty等,讓我們不再需要自己將應用部署到Web伺服器中,直接就可以啟動起來。簡單來說,使用SpringBoot可以讓我們更加快速地構建和啟動一個應用程式,簡化我們的開發工作,使我們能夠更加專注於業務功能的開發。

既然使用SpringBoot有這麼多的好處,那麼SpringBoot啟動過程中都做了哪些工作呢?本篇文章就讓我們一起來看一下SpringBoot的啟動流程。

SpringBootApplication註解

我們可以透過如下的方式啟動一個SpringBoot應用,如下所示:

@SpringBootApplication //SpringBoot應用的註解public class Application { public static void main(String[] args) { //執行SpringApplication的run方法 SpringApplication。run(Application。class, args); }}

我們看到的是在Application上面有一個@SpringBootApplication註解,這個註解有什麼用呢?下面我們就一起來看一下SpringBootApplication註解的定義。

。。省略其他註解@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanpublic @interface SpringBootApplication { 。。。省略無關程式碼}

在上面的SpringBootApplication註解的定義中,我們會發現SpringBootApplication註解其實是一個複合註解,也就是組合了其它註解的註解,如果我們不想使用SpringBootApplication註解,其實也可以單獨使用上面的三個註解來標識我們的Application啟動類,效果也是一樣的,如下所示:

@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanpublic class Application {。。。省略無關程式碼}

下面我們就來看一下SpringBootApplication中這三個核心註解分別起什麼作用。

SpringBootConfiguration註解

該註解定義如下:

@Configurationpublic @interface SpringBootConfiguration {}

由SpringBootConfiguration註解的定義中可知,該註解當中包含了@Configuration註解,作用是標識我們的啟動類Application是一個配置類,有了這個註解,我們的啟動類Application就可以作為一個配置類,從而被Spring並納入到Spring的管理當中。

EnableAutoConfigration註解

EnableAutoConfigration註解是SpringBoot自動配置的核心註解,那麼這個註解是怎麼實現自動裝配的呢?

該註解定義如下:

。。。省略無關程式碼@AutoConfigurationPackage@Import(AutoConfigurationImportSelector。class)public @interface EnableAutoConfiguration { 。。。省略無關程式碼}

由EnableAutoConfiguration的定義可知,該註解包含了兩個子註解@AutoConfigurationPackage和@Import註解,這兩個註解的作用如下:

AutoConfigurationPackage註解,該註解會向Spring中註冊了一個BasePackages物件,該物件持有一個包路徑,這個包路徑就是標註了AutoConfigurationPackage註解的類的包路徑,也就是我們的啟動類的路徑,這個包路徑會被Spring作為自動配置的包路徑進行管理,可以透過List packages = AutoConfigurationPackages。get(this。beanFactory)獲取到該包路徑;

Import註解,該註解指定了一個AutoConfigurationImportSelector類,Spring在啟動時會呼叫這個類中的selectImports方法,這個方法中會呼叫SpringFactoriesLoader的loadFactoryNames方法從

META-INF/spring。factories檔案中載入key為

org。springframework。boot。autoconfigure。EnableAutoConfiguration

指定的自動配置類,然後將返回的自動配置類交給Spring進行註冊載入;

ComponentScan註解

該註解在SpringBootApplication的完整形式如下所示:

@ComponentScan(excludeFilters = { @Filter(type = FilterType。CUSTOM, classes = TypeExcludeFilter。class), @Filter(type = FilterType。CUSTOM, classes = AutoConfigurationExcludeFilter。class) })

該註解的作用的將我們的啟動類Application的包名作為根路徑,將該包路徑及其子包下符合條件的類掃描載入到Spring容器當中。

這裡需要注意的是AutoConfigurationExcludeFilter,這個Filter會排除掉在

META-INF/spring。factories中指定的自動配置類,因為自動配置類會使用

EnableAutoConfigration註解進行載入,為了避免重複載入,在這裡需要將自動配置類給排除掉,不再透過ComponentScan註解進行掃描載入。

SpringApplicatin。run方法

下面我再將啟動SpringBoot應用程式的程式碼貼到下面,方便檢視:

@SpringBootApplication //SpringBoot應用的註解public class Application { public static void main(String[] args) { //執行SpringApplication的run方法 SpringApplication。run(Application。class, args); }}

我們注意到,在main主方法中呼叫了SpringApplication的run方法,該方法完成了整個SpringBoot的啟動流程,下面讓我們一起來看一下這個方法執行過程中都做了哪些工作。

讓我們看一下SpringApplication的run方法的定義:

//SpringApplication的run方法public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources)。run(args);}

由上面的程式碼可以指定,run方法在SpringApplication是一個靜態方法,該方法會將傳入的啟動類(Application。class)作為SpringApplication的構造引數來建立一個SpringApplication的例項,並呼叫該例項物件的run方法,下面來看一下SpringApplication構造過程中做了哪些操作。

建立SpringApplication例項物件

首先看一下SpringApplication建構函式中做了哪些操作。

public SpringApplication(Class<?>。。。 primarySources) { 。。。省略無關程式碼 //設定啟動類為primarySources this。primarySources = new LinkedHashSet<>(Arrays。asList(primarySources)); //決策當前web環境的型別 this。webApplicationType = deduceWebApplicationType(); //透過SPI載入ApplicationContextInitializer setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer。class)); //透過SPI載入ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener。class)); //決策當前啟動main方法的是哪個類 this。mainApplicationClass = deduceMainApplicationClass();}

SpringApplication的建構函式比較簡單,主要是做了一些簡單的初始化操作:

設定當前的primarySources為當前的啟動類;

呼叫deductWebApplicatoinType方法判斷當前web環境的型別,一般我們使用的都是Servlet環境;

透過SPI機制載入ApplicationContextInitializer例項物件,並設定到SpringApplication的成員變數中;

透過SPI機制載入ApplicationListener例項物件,並設定到SpringApplication的成員變數中;

設定mainAppllicationClass為執行main方法的類;

建立完SpringApplication例項物件後,接下來就是呼叫該物件的run方法,下面我們一起來看一下run方法的執行流程。

run方法的執行流程

下面我們看一下run方法的程式碼實現,注意該run方法是SpringApplicatoin的例項方法,而不是之前提到的靜態方法,該方法實現如下所示:

//SpringApplication的run方法public ConfigurableApplicationContext run(String。。。 args) { //建立StopWatch物件用於統計run方法的執行耗時 StopWatch stopWatch = new StopWatch(); //呼叫start方法表示開始啟動計時 stopWatch。start(); ConfigurableApplicationContext context = null; Collection exceptionReporters = new ArrayList<>(); //透過SPI機制載入所有的SpringApplicatoinRunListener監聽器 //SpringBoot啟動過程的不同階段會回撥該監聽器的不同方法 SpringApplicationRunListeners listeners = getRunListeners(args); //呼叫SpringApplicatoinRunListener的starting方法 listeners。starting(); try { //將SpringApplication的啟動引數封裝為ApplicationArguments ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); //建立SpringBoot應用使用的環境變數物件,內部會根據webApplicationType建立不同的環境物件, //這裡會建立StandardServletEnvironment物件 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); //列印banner,預設是在管控臺列印SpringBoot Banner printedBanner = printBanner(environment); //建立使用的ApplicationContext上下文物件,這裡會根據webApplicationType建立不同的物件上下文物件 //這裡建立的是AnnotationConfigServletWebServerApplicationContext物件 context = createApplicationContext(); //透過SPI機制載入SpringBoot的異常報告物件SpringBootExceptionReporter //當SpringBoot啟動過程中丟擲異常時,會透過該物件列印錯誤日誌 exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter。class, new Class[] { ConfigurableApplicationContext。class }, context); //ApplicationContext上下文物件建立完畢後,會呼叫prepareContext為ApplicationContext做一些準備工作 //比如為ApplicationContext設定環境變數,回撥ApplicationContextInitializer物件的initialize方法等 prepareContext(context, environment, listeners, applicationArguments, printedBanner); //呼叫ApplicationContext的refresh方法,啟動整個Spring應用程式 refreshContext(context); //停止StopWatch計時器 stopWatch。stop(); //呼叫SpringApplcationListener物件的started監聽方法 listeners。started(context); //回撥Spring中的的ApplicationRunner物件和CommandLineRunner物件 callRunners(context, applicationArguments); } catch (Throwable ex) { //丟擲異常,則使用SpringExceptionReporter列印異常報告 handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { //啟動成功,則呼叫SpringApplicationListener物件的running監聽方法 listeners。running(context); } catch (Throwable ex) { //丟擲異常,則使用SpringExceptionReporter列印異常報告 handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } //返回建立的ApplicationContext應用上下文 return context;}

SpringApplication的run方法中定義了整個SpringBoot的啟動流程,具體的流程可以參考上面的註釋,這裡就不再重複贅述,下面只對其中幾個關鍵點進行介紹。

在run方法中,透過SPI機制載入了SpringApplicationRunListener物件,該物件定義了許多SpringBoot應用啟動過程中的一些回撥方法,比如:starting方法,在SpringBoot剛啟動時回撥;environmentPrepared方法,在SpringBoot的環境Environment物件準備完成時回撥;其它方法感興趣的小夥伴可以自行查閱原始碼。

在run方法中,我們還注意到建立Environment物件和ApplicationContext物件時,會根據webApplicationType的值來建立具體的物件,webApplicationType的值在SpringApplication的建構函式中會進行賦值,用於表示當前的web環境型別,常用的是Servlet環境,因此這裡建立的環境物件和上下文物件分別是StandardServletEnvironment和AnnotationConfigServletWebServerApplicationContext。

建立完ApplicationContext物件後,會呼叫refreshContext方法,方法中會呼叫ApplicationContext的refresh方法重新整理Spring上下文,該方法是Spring應用啟動過程中的重要方法,Spring應用中使用到的bean,都是透過這個方法進行掃描載入,最終注入到Spring上下文中,關於refresh方法的具體實現邏輯小編將在後續的文章中進行分享,有興趣的小夥伴可以關注小編。

總結

本篇文章,主要對現在主流的SpringBoot的一些啟動流程進行了簡單介紹,關於Spring更多詳細的內容,可以關注小編,小編將在後續的文章中進行介紹。