為啥一個 main 方法就能啟動項目
在 Spring Boot 出現(xiàn)之前,我們要運行一個 Java Web 應(yīng)用,首先需要有一個 Web 容器(例如 Tomcat 或 Jetty),然后將我們的 Web 應(yīng)用打包后放到容器的相應(yīng)目錄下,最后再啟動容器。
在 IDE 中也需要對 Web 容器進(jìn)行一些配置,才能夠運行或者 Debug。而使用 Spring Boot 我們只需要像運行普通 JavaSE 程序一樣,run 一下 main() 方法就可以啟動一個 Web 應(yīng)用了。這是怎么做到的呢?今天我們就一探究竟,分析一下 Spring Boot 的啟動流程。
概覽
回看我們寫的第一個 Spring Boot 示例,我們發(fā)現(xiàn),只需要下面幾行代碼我們就可以跑起一個 Web 服務(wù)器:
去掉類的聲明和方法定義這些樣板代碼,核心代碼就只有一個 @SpringBootApplication 注解和 SpringApplication.run(HelloApplication.class, args) 了。而我們知道注解相當(dāng)于是一種配置,那么這個 run() 方法必然就是 Spring Boot 的啟動入口了。
接下來,我們沿著 run() 方法來順藤摸瓜。進(jìn)入 SpringApplication 類,來看看 run() 方法的具體實現(xiàn):
Spring Boot 啟動時做的所有操作都這這個方法里面,當(dāng)然在調(diào)用上面這個 run() 方法之前,還創(chuàng)建了一個 SpringApplication 的實例對象。因為上面這個 run() 方法并不是一個靜態(tài)方法,所以需要一個對象實例才能被調(diào)用。
可以看到,方法的返回值類型為 ConfigurableApplicationContext,這是一個接口,我們真正得到的是 AnnotationConfigServletWebServerApplicationContext 的實例。通過類名我們可以知道,這是一個基于注解的 Servlet Web 應(yīng)用上下文(我們知道上下文(context)是 Spring 中的核心概念)。
上面對于 run() 方法中的每一個步驟都做了簡單的注釋,接下來我們選擇幾個比較有代表性的來詳細(xì)分析。
應(yīng)用啟動計時
在 Spring Boot 應(yīng)用啟動完成時,我們經(jīng)常會看到類似下面內(nèi)容的一條日志:
應(yīng)用啟動后,會將本次啟動所花費的時間打印出來,讓我們對于啟動的速度有一個大致的了解,也方便我們對其進(jìn)行優(yōu)化。記錄啟動時間的工作是 run() 方法做的第一件事,在編號 1 的位置由 stopWatch.start() 開啟時間統(tǒng)計,具體代碼如下:
然后到了 run() 方法的基本任務(wù)完成的時候,由 stopWatch.stop()(編號 12 的位置)對啟動時間做了一個計算,源碼也很簡單:
最后,在 run() 中的編號 13 的位置將啟動時間打印出來:
打印 Banner
Spring Boot 每次啟動是還會打印一個自己的 LOGO,如圖 8-6:

圖 8-6 Spring Boot Logo
這種做法很常見,像 Redis、Docker 等都會在啟動的時候?qū)⒆约旱?LOGO 打印出來。Spring Boot 默認(rèn)情況下會打印那個標(biāo)志性的“樹葉”和 “Spring” 的字樣,下面帶著當(dāng)前的版本。
在 run() 中編號 7 的位置調(diào)用打印 Banner 的邏輯,最終由 SpringBootBanner 類的 printBanner() 完成。這個圖案定義在一個常量數(shù)組中,代碼如下:
手工格式化了一下 BANNER 的字符串,輪廓已經(jīng)清晰可見了。真正打印的邏輯就是 printBanner() 方法里面的那個 for 循環(huán)。
記錄啟動時間和打印 Banner 代碼都非常的簡單,而且都有很明顯的視覺反饋,可以清晰的看到結(jié)果。拿出來咱們做個熱身,配合斷點去 Debug 會有更加直觀的感受,尤其是打印 Banner 的時候,可以看到整個內(nèi)容被一行一行打印出來,讓我想起了早些年用那些配置極低的電腦(還是 CRT 顯示器)運行著 Win98,經(jīng)常會看到屏幕內(nèi)容一行一行加載顯示。
創(chuàng)建上下文實例
下面我們來到 run() 方法中編號 8 的位置,這里調(diào)用了一個 createApplicationContext() 方法,該方法最終會調(diào)用 ApplicationContextFactory 接口的代碼:
這個方法就是根據(jù) SpringBootApplication 的 webApplicationType 屬性的值,利用反射來創(chuàng)建不同類型的應(yīng)用上下文(context)。而屬性 webApplicationType ?的值是在前面執(zhí)行構(gòu)造方法的時候由 WebApplicationType.deduceFromClasspath() 獲得的。通過方法名很容易看出來,就是根據(jù) classpath 中的類來推斷當(dāng)前的應(yīng)用類型。
我們這里是一個普通的 Web 應(yīng)用,所以最終返回的類型為 SERVLET。所以會返回一個 AnnotationConfigServletWebServerApplicationContext 實例。
構(gòu)建容器上下文
接著我們來到 run() 方法編號 9 的 prepareContext() 方法。通過方法名,我們也能猜到它是為 context 做上臺前的準(zhǔn)備工作的。
在這個方法中,會做一些準(zhǔn)備工作,包括初始化容器上下文、設(shè)置環(huán)境、加載資源等。
加載資源
上面的代碼中,又調(diào)用了一個很關(guān)鍵的方法——load()。這個 load() 方法真正的作用是去調(diào)用 BeanDefinitionLoader 類的 load() 方法。源碼如下:
可以看到,load() 方法在加載 Spring 中各種資源。其中我們最熟悉的就是 load((Class<?>) source) 和 load((Package) source) 了。一個用來加載類,一個用來加載掃描的包。
load((Class<?>) source) 中會通過調(diào)用 isComponent() 方法來判斷資源是否為 Spring 容器管理的組件。isComponent() 方法通過資源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在內(nèi))來區(qū)分是否為 Spring 容器管理的組件。
而 load((Package) source) 方法則是用來加載 @ComponentScan 注解定義的包路徑。
刷新上下文
run() 方法編號10 的 refreshContext() 方法是整個啟動過程比較核心的地方。像我們熟悉的 BeanFactory 就是在這個階段構(gòu)建的,所有非懶加載的 Spring Bean(@Controller、@Service 等)也是在這個階段被創(chuàng)建的,還有 Spring Boot 內(nèi)嵌的 Web 容器要是在這個時候啟動的。
跟蹤源碼你會發(fā)現(xiàn)內(nèi)部調(diào)用的是 ConfigurableApplicationContext.refresh(),ConfigurableApplicationContext 是一個接口,真正實現(xiàn)這個方法的有三個類:AbstractApplicationContext、ReactiveWebServerApplicationContext 和 ServletWebServerApplicationContext。
AbstractApplicationContext 為后面兩個的父類,兩個子類的實現(xiàn)比較簡單,主要是調(diào)用父類實現(xiàn),比如 ServletWebServerApplicationContext 中的實現(xiàn)是這樣的:
主要的邏輯都在 AbstractApplicationContext 中:
簡單說一下編號 9 處的 onRefresh() 方法,該方法父類未給出具體實現(xiàn),需要子類自己實現(xiàn),ServletWebServerApplicationContext 中的實現(xiàn)如下:
factory.getWebServer(getSelfInitializer()) 會根據(jù)項目配置得到一個 Web Server 實例,這里跟下一篇將要談到的自動配置有點關(guān)系。