概述
Spring歷來一直是JAVA研發中不可或缺的框架,它提供了完美的控制反轉功能,使應用能夠達到低耦合的設計規范。在微服務時代,Spring Boot的免配置設計更是丟棄了原來復雜的xml聲明模式,使程序員能更加專心業務代碼的編寫。
最近在面試時,我問過很多同學關于Spring生命周期的問題,可能完整的回答上來的人寥寥無幾。Spring其實并不難,反而是很經典。其架構設計巧妙,代碼邏輯清晰,即便對編程不太了解的人也能看得懂,不信你繼續往下看。
本文主要關注Spring Boot是如何創建啟動類和如何管理bean的生命周期這兩點內容,我會深入到Spring Boot框架的源碼,從神秘的 SpringApplication
類開始,一步步揭開Spring Boot美麗的面紗。如果你是初學者,這篇文章可能會帶給你更多迷茫,請移步搜索Spring Boot基礎使用教程。
問題
首先來看一個最簡單的Spring Boot應用,代碼如下。
@SpringBootApplication
public class PeopleBatchApplication {
public static void main(String[] args) {
SpringApplication.run(PeopleBatchApplication.class, args);
}
}
public static void main
多簡單熟悉的語句,就差一個System.out.println("Hello World")就是我們幾年前初次接觸java時的第一個程序。這段代碼中的 SpringApplication.run(PeopleBatchApplication.class, args);
語句就是Spring Boot的 Hello World,這一句代碼就完整的創建了一個Spring的運行環境。
我們的問題就此開始,這句代碼是如何完成Spring上下文創建以及相關bean的聲明呢?那個在主類上標記的奇怪的@SpringBootApplication
注解是什么?Spring Boot如何識別web環境,并創建Servlet上下文環境?Spring Boot是如何識別
Spring Boot 如何創建上下文環境
創建 SpringApplication
SpringApplication
類是Spring Boot應用的標配,它可以啟動Spring應用并加載配置文件,并創建Spring上下文環境。
源碼展示
SpringApplication類部分源碼如下。
public class SpringApplication {
/**
*這個是在入門示例中所調用的方法
**/
public static ConfigurableApplicationContext run(Object source, String... args) {
return run(new Object[] { source }, args);
}
/**
* 創建可配置的應用上下文
* @param sources 要作為配置類的對象
* @param args 程序啟動參數
*/
public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
return new SpringApplication(sources).run(args);
}
/**
* 構造方法
*/
public SpringApplication(Object... sources) {
initialize(sources);
}
/**
* 初始化 SpringApplication 的方法
**/
private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
this.webEnvironment = deduceWebEnvironment();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
private boolean deduceWebEnvironment() {
for (String className : WEB_ENVIRONMENT_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return false;
}
}
return true;
}
}
初始化SpringApplication對象流程
SpringApplication對象的初始化經過了以下步驟:
- 記錄下用戶指定的Spring Boot配置類信息,并將配置類對象存入到全局變量
Set<Object> sources
。 - 驗證運行環境是否為web環境,并將驗證結果存入
boolean webEnvironment
全局變量。 - 設置需要新初始化的上下文配置對象(需要在
META_INFO/spring.factories
文件中指定的bean工廠創建類的實例,默認實現類有org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer
,org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer
,org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer
,org.springframework.boot.context.ContextIdApplicationContextInitializer
,org.springframework.boot.context.config.DelegatingApplicationContextInitializer
,org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer
這幾個類)。并將初始化類放入到List<ApplicationContextInitializer<?> initializers
全局變量。 - 設置默認bean監聽器(默認加載spring-boot-autoconfigure.jar包中
META_INFO/spring.factories
文件所指定的監聽器實現org.springframework.boot.autoconfigure.BackgroundPreinitializer
)。并將初始化監聽器放入到List<ApplicationListener<?>> listeners
。 - 設置main方法所在的類到全局變量
Class mainApplicationClass
。
小結
SpringApplication對象在初始化時主要將應用的各個配置都存入全部變量,并沒有進行邏輯操作。如果我們需要自定義bean工廠或者監聽器的話,可以選擇在classpath下建立自己的META_INF/spring.factories
文件分別指定初始化bean工廠和bean監聽器。注意自建的初始化工廠或監聽器并沒有覆蓋spring原有的bean工廠。
從上面的代碼我們可以看出,只要classpath中存在javax.servlet.Servlet
或org.springframework.web.context.ConfigurableWebApplicationContext
類,Spring Boot都自動構建Servlet運行環境。
SpringApplication 的 run 方法
run
方法主要用于創建或刷新一個應用上下文,是 Spring Boot的核心。
run 方法源碼
比起 SpringApplication 的創建的代碼,run
方法的邏輯復雜了許多。不但有加載配置文件,創建上下文環境的邏輯,還有對創建Spring上下文的計時信息,另外我們啟用Spring Boot應用時所打印的那個字符串圖像標識,也是在這個方法上控制的(如果你很不喜歡這個標識,可以考慮在這里去掉)。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
run 方法執行流程
- 創建計時器,用于記錄SpringBoot應用上下文的創建所耗費的時間。
- 開啟所有的
SpringApplicationRunListener
監聽器,用于監聽Sring Boot應用加載與啟動信息。 - 創建應用配置對象(main方法的參數配置)
ConfigurableEnvironment
- 創建要打印的Spring Boot啟動標記
Banner
- 創建
ApplicationContext
應用上下文對象,web環境和普通環境使用不同的應用上下文。 - 創建應用上下文啟動失敗原因分析對象
FailureAnalyzers
- 刷新應用上下文,并從xml、properties、yml配置文件或數據庫中加載配置信息,并創建已配置的相關的單例bean。到這一步,所有的非延遲加載的Spring bean都應該被創建成功。
- 調用實現了
*Runner
類型的bean的run方法,開始應用啟動。 - 完成Spring Boot啟動監聽
- 打印Spring Boot上下文啟動耗時
- 如果在上述步驟中有異常發生則日志記錄下才創建上下文失敗的原因并拋出
IllegalStateException
異常。
運行事件
事件就是Spring Boot啟動過程的狀態描述,在啟動Spring Boot時所發生的事件一般指:
- 開始啟動事件
- 環境準備完成事件
- 上下文準備完成事件
- 上下文加載完成
- 應用啟動完成事件
Spring啟動時的事件都是繼承自 SpringApplicationEvent
抽象類,每一個事件都包含了應用的 SpringApplication
對象和應用程序啟動時的參數。
SpringApplicationRunListener 運行監聽器
顧名思意,運行監聽器的作用就是為了監聽 SpringApplication
的run方法的運行情況。在設計上監聽器使用觀察者模式,以總信息發布器 SpringApplicationRunListeners
為基礎平臺,將Spring啟動時的事件分別發布到各個用戶或系統在 META_INF/spring.factories
文件中指定的應用初始化監聽器中。使用觀察者模式,在Spring應用啟動時無需對啟動時的其它業務bean的配置關心,只需要正常啟動創建Spring應用上下文環境。各個業務'監聽觀察者'在監聽到spring開始啟動,或環境準備完成等事件后,會按照自己的邏輯創建所需的bean或者進行相應的配置。觀察者模式使run方法的結構變得清晰,同時與外部耦合降到最低。
運行時監聽器繼承自 SpringApplicationRunListener
接口,其代碼如下:
package org.springframework.boot;
public interface SpringApplicationRunListener {
/**
* 在run方法業務邏輯執行、應用上下文初始化前調用此方法
*/
void starting();
/**
* 當環境準備完成,應用上下文被創建之前調用此方法
*/
void environmentPrepared(ConfigurableEnvironment environment);
/**
* 在應用上下文被創建和準備完成之后,但上下文相關代碼被加載執行之前調用。因為上下文準備事件和上下文加載事件難以明確區分,所以這個方法一般沒有具體實現。
*/
void contextPrepared(ConfigurableApplicationContext context);
/**
*當上下文加載完成之后,自定義bean完全加載完成之前調用此方法。
*/
void contextLoaded(ConfigurableApplicationContext context);
/**
*當run方法執行完成,或執行過程中發現異常時調用此方法。
*/
void finished(ConfigurableApplicationContext context, Throwable exception);
}
默認情況下Spring Boot會實例化EventPublishingRunListener
作為運行監聽器的實例。在實例化運行監聽器時需要SpringApplication
對象和用戶對象作為參數。其內部維護著一個事件廣播器(被觀察者對象集合,前面所提到的在META_INF/spring.factories
中注冊的初始化監聽器的有序集合 ),當監聽到Spring啟動等事件發生后,就會將創建具體事件對象,并廣播推送給各個被觀察者。
運行事件廣播
下面的代碼來自SimpleApplicationEventMulticaster
,主要描述如何將Spring Boot啟動時的各個事件推送到被觀察者。
/**
* 將接受的事件進行廣播
*/
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
invokeListener(listener, event);
}
});
}
else {
invokeListener(listener, event);
}
}
}
/**
* 將給定的事件發送到指定的監聽器
*/
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || msg.startsWith(event.getClass().getName())) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}
細讀這段代,之所以我們的應用會啟動的很慢,很大的原因就是因為在創建應用時我們對事件的處理機制都是同步的,如果業務邏輯允許,我們將廣播方法改為異步的(通過 public void setTaskExecutor(Executor taskExecutor)
可以借助線程池實現異步),可能會大幅提高應用啟動速度。
運行業務監聽器
這里的'運行業務監聽器'指的是每個組件對Spring Boot啟動事件的監聽器,其主要作用對在Spring啟動狀態做出明確響應。如日志監聽器LoggingApplicationListener
會對啟動時的狀態做日志記錄,ConfigFileApplicationListener
會在接收到環境配置完成事件后解析加載配置文件。
下面以ConfigFileApplicationListener
為例,簡要的看看運行業務監聽器時怎么處理事件的。
package org.springframework.boot.context.config;
public class ConfigFileApplicationListener
implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
/*
*收到事件請求后執行這個方法
*配置文件監聽器只監聽環境配置完成事件和上下文加載完成事件
**/
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
/**
* 獲取所有環境配置處理器,并根據事件所給出的環境執行加載文件配置任務
**/
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
/**
* 環境配置方法
* 首先加載應用配置文件 application.properties 包括已激活的profiles的配置文件
* 其次配置是否忽略bean的信息
* 最后將配置文件的配置信息綁定到 SpringApplication中
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
configureIgnoreBeanInfo(environment);
bindToSpringApplication(environment, application);
}
}
onApplicationEnvironmentPreparedEvent()
方法相關代碼的主要作用是首先從META_INF/spring.factories
中找到所有屬性名為org.springframework.boot.env.EnvironmentPostProcessor
的環境前置處理器,并將其加入到有序的環境處理器列表中,且ConfigFileApplicationListener
類就恰恰在這個前置處理器列表里。然后逐個執行每個環境前置處理器的前置處理方法。
@Enable* 注解就是通過在
META_INF/spring.factories
中配置org.springframework.boot.autoconfigure.EnableAutoConfiguration
屬性,其值指向對注解的解析類而實現的。學會了這段內容后,我們也可以自己設計一個 spring-boot-*-start.jar 包,并完成其自動配置了。
對于ConfigFileApplicationListener
的環境前置處理方法請注意,當收到環境配置完成事件后從classpath中加載并解析application.properties/application.yml 配置文件,在以下位置的應用配置文件都會被掃描到:
<ul>
<li>file:./config/:</li>
<li>file:./</li>
<li>classpath:config/</li>
<li>classpath:</li>
</ul>
注意路徑的掃描順序,在不同路徑下的應用配置文件中如果有相同的屬性,后加載的屬性會覆蓋先加載的屬性。另外如果在應用配置文件中指定了 spring.config.location
屬性,該路徑下的配置文件也會自動被spring掃描并加載。
Spring Boot配置屬性的優先級(前面會覆蓋后面的):
- 命令行啟動參數的配置
- 后加載的配置文件中的屬性會覆蓋先加載的
- 后加載的 application-profiles.yml 中的屬性會覆蓋先加載的 application.yml
引用
本文是對Spring Boot源碼的深度分析,為為在此首先特別感謝Spring團隊無私的將經典的代碼開源,另外感謝我的同事對我寫作此文期間給予真切的鼓勵與支持。
關于
文章內容著重源碼分析與設計詳解,可能沒有對實際使用的用例,因此不太適合Spring Boot的初學者。
后記
本文主要介紹了Spring Boot啟動時的主體流程、事件解耦設計與配置加載原理,后續下篇文章將深入剖析上下文環境 ApplicationContext
的加載原理與流程。
本文內容主要是對 Spring Boot 1.5.9RELEASE的源碼解析,不過作者水平有限,有不盡然的地方敬請指出。本項目和文檔中所用的內容僅供學習和研究之用,轉載或引用時請指明出處。如果你對文檔有疑問或問題,請在項目中給我留言或發email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技術能夠改變世界 。