前言
此系列是針對springboot的啟動,旨在于和大家一起來看看springboot啟動的過程中到底做了一些什麼事。如果大家對springboot的源碼有所研究,可以挑些自己感興趣或者對自己有幫助的看;但是如果大家沒有研究過springboot的源碼,不知道springboot在啟動過程中做了些什麼,那麼我建議大家從頭開始一篇一篇按順序讀該系列,不至于從中途插入,看的有些懵懂。當然,文中講的不對的地方也歡迎大家指出,有待改善的地方也希望大家不吝賜教。老規矩:一周至少一更,中途會不定期的更新一些其他的部落格,可能是springboot的源碼,也可能是其他的源碼解析,也有可能是其他的。
前情回顧
大家還記得上篇博文講了什麼嗎,或者說大家知道上篇博文講了什麼嗎。這裡幫大家做個簡單回顧,主要是兩個方法
1、getApplicationListeners
過濾出于與ApplicationStartingEvent比對的監聽器,過濾出的結果是:LoggingApplicationListener、BackgroundPreinitializer、DelegatingApplicationListener、LiquibaseServiceLocatorApplicationListener、EnableEncryptablePropertiesBeanFactoryPostProcessor五種類型的執行個體
2、invokeListener
調用getApplicationListeners過濾出的五個執行個體的onApplicationEvent方法,5個onApplicationEvent都做了啥,大體如下
LoggingApplicationListener:檢測正在使用的日志系統,預設是logback,支援3種,優先級從高到低:logback > log4j > javalog。此時日志系統還沒有初始化
BackgroundPreinitializer:另起一個背景線程觸發那些耗時的初始化,包括驗證器、消息轉換器等等,具體是哪些初始化見下代碼,有興趣的朋友可去跟下
private void performPreinitialization() {
try {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
runSafely(new ConversionServiceInitializer());
runSafely(new ValidationInitializer());
runSafely(new MessageConverterInitializer());
runSafely(new MBeanFactoryInitializer());
runSafely(new JacksonInitializer());
runSafely(new CharsetInitializer());
preinitializationComplete.countDown();
}
public void runSafely(Runnable runnable) {
try {
runnable.run();
}
catch (Throwable ex) {
// Ignore
}
}
}, "background-preinit");
thread.start();
}
catch (Exception ex) {
// This will fail on GAE where creating threads is prohibited. We can safely
// continue but startup will be slightly slower as the initialization will now
// happen on the main thread.
preinitializationComplete.countDown();
}
}
View Code
DelegatingApplicationListener:此時什麼也沒做
LiquibaseServiceLocatorApplicationListener:此時什麼也沒做
EnableEncryptablePropertiesBeanFactoryPostProcessor:此時僅僅列印了一句日志,其他什麼也沒做
簡單點來說,就是檢測正在使用的日志系統、另起一個背景線程執行耗時的初始化
prepareEnvironment
講prepareEnvironment之前,我們先來看看我們的戰績,我們對run方法完成了多少源碼解讀
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 秒表,用于記錄啟動時間;記錄每個任務的時間,最後會輸出每個任務的總費時
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// spring應用上下文,也就是我們所說的spring根容器
ConfigurableApplicationContext context = null;
// 自定義SpringApplication啟動錯誤的回調接口
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 設定jdk系統屬性java.awt.headless,預設情況為true即開啟;更多java.awt.headless資訊大家可以去查閱資料,這不是本文重點
configureHeadlessProperty();
// 擷取啟動時監聽器(EventPublishingRunListener執行個體)
SpringApplicationRunListeners listeners = getRunListeners(args)
// 觸發啟動事件,啟動監聽器會被調用,一共5個監聽器被調用
listeners.starting();
try {
// 參數封裝,也就是在指令行下啟動應用帶的參數,如--server.port=9000
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;
}
如果光從run方法的源代碼比例來看,我們已經完成了将近一半的源碼解析了,可真是這樣的嗎?我們先别急着否定,就當我們快完成一半的解析了(笑而不語)。既然我們都快完成了一半了,那麼我們加把勁,今天來完成“這一半”。
prepareEnvironment按字面意思就是準備環境,那到底準備什麼環境呢?我們一起來慢慢看,其源代碼如下
// 準備環境
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment 建立和配置環境
// 擷取或建立環境
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置環境:配置PropertySources和activeProfiles
configureEnvironment(environment, applicationArguments.getSourceArgs());
// listeners環境準備(就是廣播ApplicationEnvironmentPreparedEvent事件)。還記得這個listeners怎麼來的嗎?
listeners.environmentPrepared(environment);
// 将環境綁定到SpringApplication
bindToSpringApplication(environment);
// 如果是非web環境,将環境轉換成StandardEnvironment
if (this.webApplicationType == WebApplicationType.NONE) {
environment = new EnvironmentConverter(getClassLoader())
.convertToStandardEnvironmentIfNecessary(environment);
}
// 配置PropertySources對它自己的遞歸依賴
ConfigurationPropertySources.attach(environment);
return environment;
}
内容不多,我們就一行一行的來跟源代碼
getOrCreateEnvironment
從字面上看,這個方法的作用就是擷取或建立環境,應該就是存在就直接傳回,不存在則建立一個并傳回。
// 擷取或建立Environment,很顯然我們這裡是建立StandardServletEnvironment
private ConfigurableEnvironment getOrCreateEnvironment() {
// 存在則直接傳回
if (this.environment != null) {
return this.environment;
}
// 根據webApplicationType建立對應的Environment
// webApplicationType的值還記得在哪擷取到的嗎?不知道的請去看我的springboot源碼一
if (this.webApplicationType == WebApplicationType.SERVLET) {
return new StandardServletEnvironment(); // 标準的Servlet環境,也就是我們說的web環境
}
return new StandardEnvironment(); // 标準環境,非web環境
}
還記得this.webApplicationType的值是什麼嗎,不記得的點這裡尋找答案,在我的案例中其值就是WebApplicationType.SERVLET,那麼很顯然建立一個StandardServletEnvironment對象傳回。
StandardServletEnvironment類圖
StandardServletEnvironment繼承自StandardEnvironment,也就是web環境是特殊的非web環境,有點類似正方形是特殊的長方形一樣。AbstractEnvironment的構造方法調用了customizePropertySources方法,也就說StandardServletEnvironment在執行個體化的時候,他的customizePropertySources會被調用,customizePropertySources源代碼如下
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
super.customizePropertySources(propertySources);
}
從上圖中可以看出StandardServletEnvironment的customizePropertySources方法隻是往propertySources中添加了兩個名字叫servletConfigInitParams、servletContextInitParams的StubPropertySource對象,沒更多的操作;而StandardEnvironment的customizePropertySources方法則往propertySources中添加了兩個包含java系統屬性和作業系統環境變量的兩個對象:MapPropertySource和SystemEnvironmentPropertySource。
總結下,getOrCreateEnvironment方法建立并傳回了一個環境:StandardServletEnvironment,該環境目前包含的内容如下
configureEnvironment
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
// 配置PropertySources
configurePropertySources(environment, args);
// 配置Profiles
configureProfiles(environment, args);
}
從源碼看,将配置任務按順序委托給configurePropertySources和configureProfiles,那麼我們來看看這兩個方法
configurePropertySources
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
MutablePropertySources sources = environment.getPropertySources();
// 此時defaultProperties還是null,可能後續過程會初始化,具體詳情請期待後續的博文
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
// 存在的話将其放到最後位置
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties));
}
// 存在指令行參數,則解析它并封裝進SimpleCommandLinePropertySource對象,同時将此對象放到sources的第一位置(優先級最高)
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
"springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
// 将其放到第一位置
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
注釋說明是增加、移除或者重排序應用環境中的PropertySource。就目前而言,如果有指令行參數則新增封裝指令行參數的PropertySource,并将它放到sources的第一位置。
configureProfiles
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
// 保證environment的activeProfiles屬性被初始化了。從PropertySources中查找spring.profiles.active屬性
// 存在則将其值添加activeProfiles集合中。我們可以通過指令行參數指定該參數,但我們沒有指定
environment.getActiveProfiles(); // ensure they are initialized
// But these ones should go first (last wins in a property key clash)
// 如果存在其他的Profiles,則将這些Profiles放到第一的位置。此時沒有,後面有沒有後面再說
Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}
配置應用環境中的哪些配置檔案處于激活狀态(或預設激活)。可以通過spring.profiles.active屬性在配置檔案處理期間激活其他配置檔案。說的簡單點就是設定哪些Profiles是激活的。
這3個方法都是protected,也就說鼓勵被重寫。重寫configureEnvironment可以完全控制自定義環境,或者重寫configurePropertySources或configureProfiles,進行更細粒度控制。
listeners.environmentPrepared(environment)
public void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}
這個代碼有沒有很熟悉?,不清楚的點這裡,檢視其中的listeners.starting()。上次廣播的是ApplicationStartingEvent事件,而這次廣播的是ApplicationEnvironmentPreparedEvent事件。這裡就不和大家一起跟源代碼了,大家自行去跟哦。我在這總結下:
過濾出的與ApplicationEnvironmentPreparedEvent相比對的監聽器清單如下,他們的onApplicationEvent會被調用,大緻做了以下事情:
ConfigFileApplicationListener
1、加載EnvironmentPostProcessor清單,仍然是從META-INF/spring.factories中加載(在SpringApplication執行個體化的時候已經加載了,這次是從緩存中讀取),然後執行個體化;
2、将自己也加入EnvironmentPostProcessor清單;ConfigFileApplicationListener實作了EnvironmentPostProcessor接口,可以看它的類圖。
3、對EnvironmentPostProcessor清單進行排序;排序之後,EnvironmentPostProcessor清單圖如下:
4、周遊EnvironmentPostProcessor清單,調用每個EnvironmentPostProcessor的postProcessEnvironment方法
SystemEnvironmentPropertySourceEnvironmentPostProcessor
将propertySourceList中名為systemEnvironment的SystemEnvironmentPropertySource對象替換成OriginAwareSystemEnvironmentPropertySource對象,source未變,還是SystemEnvironmentPropertySource對象的source;OriginAwareSystemEnvironmentPropertySource是SystemEnvironmentPropertySourceEnvironmentPostProcessor的靜态内部類,且繼承自SystemEnvironmentPropertySource。具體這麼替換出于什麼目的,便于原點查找?暫時還未知。
SpringApplicationJsonEnvironmentPostProcessor
spring.application.json(或SPRING_APPLICATION_JSON)是設定在系統屬性或系統環境中;
如果spring.application.json(或SPRING_APPLICATION_JSON)有配置,那麼給environment的propertySourceList增加JsonPropertySource,并将JsonPropertySource放到名叫systemProperties的PropertySource前;目前沒有配置,那麼此環境後處理器相當于什麼也沒做。
CloudFoundryVcapEnvironmentPostProcessor
雲平台是否激活,激活了則給environment的propertySourceList增加名為vcap的PropertiesPropertySource對象,并将此對象放到指令行參數PropertySource(名叫commandLineArgs)後。很顯然,我們沒有激活雲平台,那麼此環境後處理器相當于什麼也沒做。
ConfigFileApplicationListener
添加名叫random的RandomValuePropertySource到名叫systemEnvironment的PropertySource後;
并初始化Profiles;初始化PropertiesPropertySourceLoader和YamlPropertySourceLoader這兩個加載器從file:./config/,file:./,classpath:/config/,classpath:/路徑下加載配置檔案,PropertiesPropertySourceLoader加載配置檔案application.xml和application.properties,YamlPropertySourceLoader加載配置檔案application.yml和application.yaml。目前我們之後classpath:/路徑下有個application.yml配置檔案,将其屬性配置封裝進了一個名叫applicationConfig:[classpath:/application.yml]的OriginTrackedMapPropertySource中,并将此對象放到了propertySourceList的最後。
AnsiOutputApplicationListener
設定ansi輸出,将AnsiOutput的屬性enabled設定成ALWAYS,即允許ANSI-colored輸出
LoggingApplicationListener
初始化日志系統
ClasspathLoggingApplicationListener:沒開啟調試,是以什麼也沒做
BackgroundPreinitializer:此時什麼也沒做
DelegatingApplicationListener:此時什麼也沒做,因為環境中沒有配置context.listener.classes屬性
FileEncodingApplicationListener:此時什麼也沒做,環境中沒有spring.mandatory-file-encoding屬性
EnableEncryptablePropertiesBeanFactoryPostProcessor:此時什麼也沒有做
environmentPrepared方法會觸發所有監聽了ApplicationEnvironmentPreparedEvent事件的監聽器,這些監聽器目前主要新增了兩個PropertySource:RandomValuePropertySource和OriginTrackedMapPropertySource,這個OriginTrackedMapPropertySource一般就是我們應用的配置檔案application.yml(application.properties)。
bindToSpringApplication(environment)
/**
* Bind the environment to the {@link SpringApplication}.
* @param environment the environment to bind
*/
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}
代碼比較簡單,應該就是将environment綁定到SpringApplication,可我跟進去發現沒有将environment綁定到SpringApplication,執行完bindToSpringApplication方法後,SpringApplication的屬性environment仍是null,這我就有點懵圈了,那這個方法到底有什麼用,有知道的朋友嗎?
ConfigurationPropertySources.attach(environment)
public static void attach(Environment environment) {
// 判斷environment是否是ConfigurableEnvironment的執行個體
Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
// 從environment擷取PropertySources
MutablePropertySources sources = ((ConfigurableEnvironment) environment)
.getPropertySources();
PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
if (attached != null && attached.getSource() != sources) {
sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
attached = null;
}
if (attached == null) {
// 将sources封裝成ConfigurationPropertySourcesPropertySource對象,并把這個對象放到sources的第一位置
sources.addFirst(new ConfigurationPropertySourcesPropertySource(
ATTACHED_PROPERTY_SOURCE_NAME,
new SpringConfigurationPropertySources(sources)));
}
}
将sources封裝成了一個名叫configurationProperties的ConfigurationPropertySourcesPropertySource對象,并把這個對象放到了sources的第一個位置。SpringConfigurationPropertySources是一個将MutablePropertySources轉換成ConfigurationPropertySources的擴充卡。這就相當于sources的第一個元素是它自己,形成了一個自己對自己的遞歸依賴,這麼做的目的是什麼,暫時還不得而知,也許後面會有所展現,這裡先當做一個疑問留着。
prepareEnvironment執行完後,此時environment中的内容如下:(重點看下propertySourceList)
總結
1、profile
直譯的意思總感覺不對(其作用就是指定激活的配置檔案,可以區分環境來加載不同的配置),是以文中沒有對其進行翻譯,直接采用的原單詞。有更好了解的小夥伴可以在評論區提供翻譯。
2、資源檔案
加載外部化配置的資源到environment,Spring Boot設計了一個非常特别的PropertySource順序,以允許對屬性值進行合理的覆寫。具體有哪些外部化配置,以及他們的優先級情況可以參考《Spring Boot Reference Guide》的第24章節
3、prepareEnvironment方法到底做了什麼
加載外部化配置資源到environment,包括指令行參數、servletConfigInitParams、servletContextInitParams、systemProperties、sytemEnvironment、random、application.yml(.yaml/.xml/.properties)等;
初始化日志系統。
參考
Spring Boot Reference Guide