每一個 Spring boot 專案的 main 方法裡,都只有一行 SpringApplication.run,但就是這一行程式組裝了一個可以接收 HTTP 請求的 Java 後端服務。今天我們就沿著原始程式,把 run 方法從頭到尾走一遍,看看每個組件分別是在什麼時間點登場的。
run 方法的骨架大致如下:

容器誕生之前
Spring-Application 的建構子主要做了 2 件事:首先,它會呼叫 Web-Application-Type 的deduce-From-Classpath,依照 classpath 上的類別推斷應用類型:如果只有 WebFlux 的 Dispatcher-Handler、沒有 Spring MVC 的 Dispatcher-Servlet(Day.19),就判定為 REACTIVE,否則只要 Servlet 相關的類別存在,就判定為 SERVLET,後續要創建哪一種 Application-context(Day.08),就是由這個結果決定的。接著,它會透過 ResourceLoader(Day.18) 讀取所有 jar 裡的 spring.factories,把 Application-Context-Initializer 與 Application-Listener 全部實例化。
進到 run 方法之後,首先會取得 Event-Publishing-Run-Listener,負責在啟動的每個階段發布對應的事件:Spring 正常的 Publisher 是 context 本身,但這時候 context 因為還沒被創建,因此 Event-Publishing-Run-Listener 內部會持有一個 Simple-Application-Event-Multicaster,先用這個臨時的廣播器撐過容器誕生前的空窗期,等 refresh 完成之後,才改用 context 的 publish-Event 發布事件。
接著是 prepareEnvironment,這裡就是 environment 開始被記錄的地方:Spring-Application 會建立一個 Environment 的實作類別,把命令列參數加成優先級最高的屬性源,再透過 Application-Environment-Prepared-Event 觸發另一個組件(Config-Data-Environment-Post-Processor)讀取 application.yml。
IoC 容器的創建與準備
下一步是 create-Application-Context:以 SERVLET 類型來說,被創建出來的 Application-Context 在建構子裡會直接 new 一個 Default 工廠作為底層的 BeanFactory(Day.04),這個工廠同時實作了 Dean-Definition-Registry(Day.03) 用來存放 BeanDefinition(Day.02),也繼承了 Default-Singleton-Bean-Registry(Day.05) 負責三層快取。另外,因為 Application-Context 的共同父類本身就繼承 Default-Resource-Loader,所以,不再需要額外的 Loader 替身。Application-Context 的建構子還會建立 Reader(Day.03) 和 Scanner(Day.03),其中 Reader 會先把 CCPP(Day.17) ...等核心處理器註冊成 BD,為後續的註解解析預先鋪路。
再來是 prepare-Context,它會把剛才準備好的 environment 正式記錄進 Application-Context,同時也一併交給 Reader 和 Scanner,讓它們在註冊 BD 的時候,可以依照 Environment 判斷 @Profile 之類的條件;如果我們在建構子有傳入自訂的 Resource-Loader,也會在這裡被設定進 Application-Context。最後,prepare-Context 會透過 Bean-Definition-Loader 呼叫 Reader,把 main 類別註冊成 BD,到這個階段為止,Registry 裡除了框架的核心處理器以外,就只有 main 類別這一個 BD,我們寫的 Controller 都還沒有被發現。
refresh
refresh 的 12 個步驟這裡就不再逐一展開,只挑跟 Spring boot、以及這 3 條線索有關的時間點來看。首先,refresh 在準備 Default 工廠的時候,會把 Environment 註冊成一個同名的單例 Bean,並把 Application-Context 登記成 Resource-Loader 與 Publisher 的注入來源,這也是為什麼我們可以直接 @Autowired 注入 environment 或 ResourceLoader 的原因。
接著在調用 BFPP(Day.07) 的階段,CCPP 會從 main 類別開始解析:@SpringBootApplication 裡的 @ComponentScan 會另外建立一個 Scanner,掃描 main 類別所在的 package,我們寫的 Controller,就是在這裡第一次被記錄成 BD、存進 Registry。
@EnableAutoConfiguration 則透過 @Import 匯入了 ACIS(Day.28),它實作了 ImportSelector(Day.18) 的子介面 DeferredImportSelector,會等使用者的配置都解析完之後,才從 .imports 讀出候選的自動配置類別,再交由 SpringBootCondition(Day.27) 的子類別逐一判斷 @ConditionalOnXxx 的條件。此外,Web-Server-Factory(Day.26)、Dispatcher-Servlet、Handler-Mapping(Day.21)、RMHA(Day.23)、Exception-Resolver(Day.25)、Message-Converter(Day.24)、預設採用 CGLIB 代理的 Proxy-Creator(Day.16)、以及 Message-Source(Day.11),都是在這裡被決定要不要註冊成 BD 的。
後面的流程就跟一般的 refresh 一樣:BPP(Day.06) 會被提前創建,Message-Source 與 Multicaster(Day.12) 依序完成初始化,容器也從這裡開始正式接下 Publisher 的角色;onRefresh 會透過 Web-Server-Factory 創建並啟動 Tomcat,把 Dispatcher-Servlet 與所有的 Filter(Day.20) 註冊進 Servlet-Context,但還不會開始監聽 port。
接著,Default 工廠會把所有非 lazy 的單例 BD 轉換成 Bean,在這個過程中,Request-Mapping-Handler-Mapping 會在自己的初始化階段找出所有標記 @Controller 的類別,把每一個 @RequestMapping 方法記錄進 URL 映射表,同時帶上從 Web-Mvc-Configurer 收集來的 Interceptor(Day.22);RMHA 則把 Message-Converter 交給 Resolver(Day.23) 與 Return-Handler(Day.23) 使用。
Proxy-Creator 會在每個 Bean 初始化的最後比對 Advisor(Day.15),把需要切面的 Bean 替換成透過 Aop-Proxy(Day.14) 生成的代理物件;如果專案有引入 MyBatis,Mapper 也是在這時候透過 Mapper-Factory-Bean 這個 Factory-Bean(Day.13) 生產出來的。最後,finish-Refresh 才會讓 Tomcat 開始監聽 port,容器的啟動至此正式完成。
啟動完成之後
refresh 結束之後,SpringApplication 會打印出那行熟悉的 Started XxxApplication in x.xxx seconds,接著執行容器裡所有的 Application-Runner 與 Command-Line-Runner,並在前後分別發布 Application-Started-Event 與 Application-Ready-Event,最後把 Application-Context 回傳給 main 方法。並結束 run 的流程,開始 Spring 框架的後端服務。也就是到這裡為止,這個系列講過的所有組件,在這條路徑上全部串了起來。