SpringBoot啟動流程分析(二):SpringApplication的run方法
SpringBoot系列文章簡介
SpringBoot原始碼閱讀輔助篇:
Spring IoC容器與應用上下文的設計與實現
SpringBoot啟動流程原始碼分析:
- SpringBoot啟動流程分析(一):SpringApplication類初始化過程
- SpringBoot啟動流程分析(二):SpringApplication的run方法
- SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
- SpringBoot啟動流程分析(四):IoC容器的初始化過程
- SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
- SpringBoot啟動流程分析(六):IoC容器依賴注入
筆者註釋版Spring Framework與SpringBoot原始碼git傳送門:請不要吝嗇小星星
- spring-framework-5.0.8.RELEASE
- SpringBoot-2.0.4.RELEASE
一、前言
前一篇部落格介紹了 SpringApplication 類的例項化過程,本章總結SpringBoot啟動流程最重要的部分run方法。通過rrun方法梳理出SpringBoot啟動的流程,然後後面的部落格再一步步的分析啟動流程中各個步驟所做的具體的工作。深入分析後會發現SpringBoot也就是給Spring包了一層皮,事先替我們準備好Spring所需要的環境及一些基礎,具體通過原始碼一步步深入分析後會發現Spring是真的很偉大。當然跟程式碼的時候越深入越容易陷進去進而發現有些東西沒法通過部落格詳細的梳理出來。當然在這個過程中還是立足於我們對SpringBoot的使用來說明原始碼所做的工作。知其然才能知其所以然。加油
二、SpringBoot啟動流程梳理
首先擺上run方法的原始碼
1 /** 2 * Run the Spring application, creating and refreshing a new 3 * {@link ApplicationContext}. 4 * 5 * @param args the application arguments (usually passed from a Java main method) 6 * @return a running {@link ApplicationContext} 7 * 8 * 執行spring應用,並重新整理一個新的 ApplicationContext(Spring的上下文) 9 * ConfigurableApplicationContext 是 ApplicationContext 介面的子介面。在 ApplicationContext 10 * 基礎上增加了配置上下文的工具。 ConfigurableApplicationContext是容器的高階介面 11 */ 12 public ConfigurableApplicationContext run(String... args) { 13 //記錄程式執行時間 14 StopWatch stopWatch = new StopWatch(); 15 stopWatch.start(); 16 // ConfigurableApplicationContext Spring 的上下文 17 ConfigurableApplicationContext context = null; 18 Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); 19 configureHeadlessProperty(); 20 //從META-INF/spring.factories中獲取監聽器 21 //1、獲取並啟動監聽器 22 SpringApplicationRunListeners listeners = getRunListeners(args); 23 listeners.starting(); 24 try { 25 ApplicationArguments applicationArguments = new DefaultApplicationArguments( 26 args); 27 //2、構造應用上下文環境 28 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); 29 //處理需要忽略的Bean 30 configureIgnoreBeanInfo(environment); 31 //列印banner 32 Banner printedBanner = printBanner(environment); 33 ///3、初始化應用上下文 34 context = createApplicationContext(); 35 //例項化SpringBootExceptionReporter.class,用來支援報告關於啟動的錯誤 36 exceptionReporters = getSpringFactoriesInstances( 37 SpringBootExceptionReporter.class, 38 new Class[]{ConfigurableApplicationContext.class}, context); 39 //4、重新整理應用上下文前的準備階段 40 prepareContext(context, environment, listeners, applicationArguments, printedBanner); 41 //5、重新整理應用上下文 42 refreshContext(context); 43 //重新整理應用上下文後的擴充套件介面 44 afterRefresh(context, applicationArguments); 45 //時間記錄停止 46 stopWatch.stop(); 47 if (this.logStartupInfo) { 48 new StartupInfoLogger(this.mainApplicationClass) 49 .logStarted(getApplicationLog(), stopWatch); 50 } 51 //釋出容器啟動完成事件 52 listeners.started(context); 53 callRunners(context, applicationArguments); 54 } catch (Throwable ex) { 55 handleRunFailure(context, ex, exceptionReporters, listeners); 56 throw new IllegalStateException(ex); 57 } 58 59 try { 60 listeners.running(context); 61 } catch (Throwable ex) { 62 handleRunFailure(context, ex, exceptionReporters, null); 63 throw new IllegalStateException(ex); 64 } 65 return context; 66 }
具體的每一行程式碼的含義請看註釋,我們在這先總結一下啟動過程中的重要步驟:(筆者傾向於將應用上下文同容器區分開來)
第一步:獲取並啟動監聽器 第二步:構造應用上下文環境 第三步:初始化應用上下文 第四步:重新整理應用上下文前的準備階段 第五步:重新整理應用上下文 第六步:重新整理應用上下文後的擴充套件介面
OK,下面SpringBoot的啟動流程分析,我們就根據這6大步驟進行詳細解讀。最總要的是第四,五步。我們會著重的分析。
三、第一步:獲取並啟動監聽器
事件機制在Spring是很重要的一部分內容,通過事件機制我們可以監聽Spring容器中正在發生的一些事件,同樣也可以自定義監聽事件。Spring的事件為Bean和Bean之間的訊息傳遞提供支援。當一個物件處理完某種任務後,通知另外的物件進行某些處理,常用的場景有進行某些操作後傳送通知,訊息、郵件等情況。
1 private SpringApplicationRunListeners getRunListeners(String[] args) { 2 Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class}; 3 return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances( 4 SpringApplicationRunListener.class, types, this, args)); 5 }
在這裡面是不是看到一個熟悉的方法:getSpringFactoriesInstances(),可以看下下面的註釋,前面的博文我們已經詳細介紹過該方法是怎麼一步步的獲取到META-INF/spring.factories中的指定的key的value,獲取到以後怎麼例項化類的。
1 /** 2 * 通過指定的classloader 從META-INF/spring.factories獲取指定的Spring的工廠例項 3 * @param type 4 * @param parameterTypes 5 * @param args 6 * @param <T> 7 * @return 8 */ 9 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, 10 Class<?>[] parameterTypes, Object... args) { 11 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 12 // Use names and ensure unique to protect against duplicates 13 //通過指定的classLoader從 META-INF/spring.factories 的資原始檔中, 14 //讀取 key 為 type.getName() 的 value 15 Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); 16 //建立Spring工廠例項 17 List<T> instances = createSpringFactoriesInstances(type, parameterTypes, 18 classLoader, args, names); 19 //對Spring工廠例項排序(org.springframework.core.annotation.Order註解指定的順序) 20 AnnotationAwareOrderComparator.sort(instances); 21 return instances; 22 }
回到refresh方法,debug這個程式碼 SpringApplicationRunListeners listeners = getRunListeners(args); 看一下獲取的是哪個監聽器:
EventPublishingRunListener監聽器是Spring容器的啟動監聽器。
listeners.starting(); 開啟了監聽事件。
四、第二步:構造應用上下文環境
應用上下文環境包括什麼呢?包括計算機的環境,Java環境,Spring的執行環境,Spring專案的配置(在SpringBoot中就是那個熟悉的application.properties/yml)等等。
首先看一下prepareEnvironment()方法。
1 private ConfigurableEnvironment prepareEnvironment( 2 SpringApplicationRunListeners listeners, 3 ApplicationArguments applicationArguments) { 4 // Create and configure the environment 5 //建立並配置相應的環境 6 ConfigurableEnvironment environment = getOrCreateEnvironment(); 7 //根據使用者配置,配置 environment系統環境 8 configureEnvironment(environment, applicationArguments.getSourceArgs()); 9 // 啟動相應的監聽器,其中一個重要的監聽器 ConfigFileApplicationListener 就是載入專案配置檔案的監聽器。 10 listeners.environmentPrepared(environment); 11 bindToSpringApplication(environment); 12 if (this.webApplicationType == WebApplicationType.NONE) { 13 environment = new EnvironmentConverter(getClassLoader()) 14 .convertToStandardEnvironmentIfNecessary(environment); 15 } 16 ConfigurationPropertySources.attach(environment); 17 return environment; 18 }
看上面的註釋,方法中主要完成的工作,首先是建立並按照相應的應用型別配置相應的環境,然後根據使用者的配置,配置系統環境,然後啟動監聽器,並載入系統配置檔案。
4.1、 ConfigurableEnvironment environment = getOrCreateEnvironment();
看看getOrCreateEnvironment()幹了些什麼。
1 private ConfigurableEnvironment getOrCreateEnvironment() { 2 if (this.environment != null) { 3 return this.environment; 4 } 5 //如果應用型別是 SERVLET 則例項化 StandardServletEnvironment 6 if (this.webApplicationType == WebApplicationType.SERVLET) { 7 return new StandardServletEnvironment(); 8 } 9 return new StandardEnvironment(); 10 }
通過程式碼可以看到根據不同的應用型別初始化不同的系統環境例項。前面咱們已經說過應用型別是怎麼判斷的了,這裡就不在贅述了。
從上面的繼承關係可以看出,StandardServletEnvironment是StandardEnvironment的子類。這兩個物件也沒什麼好講的,當是web專案的時候,環境上會多一些關於web環境的配置。
4.2、 configureEnvironment(environment, applicationArguments.getSourceArgs());
1 protected void configureEnvironment(ConfigurableEnvironment environment, 2 String[] args) { 3 // 將main 函式的args封裝成 SimpleCommandLinePropertySource 加入環境中。 4 configurePropertySources(environment, args); 5 // 啟用相應的配置檔案 6 configureProfiles(environment, args); 7 }
在執行完方法中的兩行程式碼後,debug的截圖如下
如下圖所示,我在spring的啟動引數中指定了引數:--spring.profiles.active=prod(關於這個引數的用法,點我,其實就是啟動多個例項用的)
在configurePropertySources(environment, args);中將args封裝成了SimpleCommandLinePropertySource並加入到了environment中。
configureProfiles(environment, args);根據啟動引數激活了相應的配置檔案。
話不多說,debug一遍就明白了。
4.3、 listeners.environmentPrepared(environment);
進入到方法一路跟下去就到了SimpleApplicationEventMulticaster類的multicastEvent()方法。
檢視getApplicationListeners(event, type)執行結果,發現一個重要的監聽器ConfigFileApplicationListener。
先看看這個類的註釋
1 /** 2 * {@link EnvironmentPostProcessor} that configures the context environment by loading 3 * properties from well known file locations. By default properties will be loaded from 4 * 'application.properties' and/or 'application.yml' files in the following locations: 5 * <ul> 6 * <li>classpath:</li> 7 * <li>file:./</li> 8 * <li>classpath:config/</li> 9 * <li>file:./config/:</li> 10 * </ul> 11 * <p> 12 * Alternative search locations and names can be specified using 13 * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}. 14 * <p> 15 * Additional files will also be loaded based on active profiles. For example if a 'web' 16 * profile is active 'application-web.properties' and 'application-web.yml' will be 17 * considered. 18 * <p> 19 * The 'spring.config.name' property can be used to specify an alternative name to load 20 * and the 'spring.config.location' property can be used to specify alternative search 21 * locations or specific files. 22 * <p> 23 * 從預設的位置載入配置檔案,並將其加入 上下文的 environment變數中 24 */
這個監聽器預設的從註釋中<ul>標籤所示的幾個位置載入配置檔案,並將其加入 上下文的 environment變數中。當然也可以通過配置指定。
debug跳過 listeners.environmentPrepared(environment); 這一行,檢視environment屬性,果真如上面所說的,配置檔案的配置資訊已經新增上來了。
五、第三步:初始化應用上下文
在SpringBoot工程中,應用型別分為三種,如下程式碼所示。
1 public enum WebApplicationType { 2 /** 3 * 應用程式不是web應用,也不應該用web伺服器去啟動 4 */ 5 NONE, 6 /** 7 * 應用程式應作為基於servlet的web應用程式執行,並應啟動嵌入式servlet web(tomcat)伺服器。 8 */ 9 SERVLET, 10 /** 11 * 應用程式應作為 reactive web應用程式執行,並應啟動嵌入式 reactive web伺服器。 12 */ 13 REACTIVE 14 }
對應三種應用型別,SpringBoot專案有三種對應的應用上下文,我們以web工程為例,即其上下文為AnnotationConfigServletWebServerApplicationContext。
1 public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot." 2 + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext"; 3 public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework." 4 + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext"; 5 public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context." 6 + "annotation.AnnotationConfigApplicationContext"; 7 8 protected ConfigurableApplicationContext createApplicationContext() { 9 Class<?> contextClass = this.applicationContextClass; 10 if (contextClass == null) { 11 try { 12 switch (this.webApplicationType) { 13 case SERVLET: 14 contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS); 15 break; 16 case REACTIVE: 17 contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); 18 break; 19 default: 20 contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); 21 } 22 } catch (ClassNotFoundException ex) { 23 throw new IllegalStateException( 24 "Unable create a default ApplicationContext, " 25 + "please specify an ApplicationContextClass", 26 ex); 27 } 28 } 29 return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); 30 }
我們先看一下AnnotationConfigServletWebServerApplicationContext的設計。
關於他的繼承體系,我們在前面的部落格中<Spring IoC容器與應用上下文的設計與實現>已經詳細介紹了,在此不再贅述。
應用上下文可以理解成IoC容器的高階表現形式,應用上下文確實是在IoC容器的基礎上豐富了一些高階功能。
應用上下文對IoC容器是持有的關係。他的一個屬性beanFactory就是IoC容器(DefaultListableBeanFactory)。所以他們之間是持有,和擴充套件的關係。
接下來看GenericApplicationContext類
1 public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry { 2 private final DefaultListableBeanFactory beanFactory; 3 ... 4 public GenericApplicationContext() { 5 this.beanFactory = new DefaultListableBeanFactory(); 6 } 7 ... 8 }
beanFactory正是在AnnotationConfigServletWebServerApplicationContext實現的介面GenericApplicationContext中定義的。在上面createApplicationContext()方法中的, BeanUtils.instantiateClass(contextClass) 這個方法中,不但初始化了AnnotationConfigServletWebServerApplicationContext類,也就是我們的上下文context,同樣也觸發了GenericApplicationContext類的建構函式,從而IoC容器也建立了。仔細看他的建構函式,有沒有發現一個很熟悉的類DefaultListableBeanFactory,沒錯,DefaultListableBeanFactory就是IoC容器真實面目了。在後面的refresh()方法分析中,DefaultListableBeanFactory是無處不在的存在感。
debug跳過createApplicationContext()方法。
如上圖所示,context就是我們熟悉的上下文(也有人稱之為容器,都可以,看個人愛好和理解),beanFactory就是我們所說的IoC容器的真實面孔了。細細感受下上下文和容器的聯絡和區別,對於我們理解原始碼有很大的幫助。在系列文章中,我們也是將上下文和容器嚴格區分開來的。
原創不易,轉載請註明出處。
如有錯誤的地方還請留言指