本文以SpringBoot Web項目爲例子分析(只引入web包)java
如題所示,本文主要劃分兩個部分進行介紹,SpringBoot的啓動和SpringBoot的初始化。 相信你們第一次啓動SpringBoot的時候都感到很是神奇,一個簡單的java –jar xxx.jar命令就能把一個web應用啓動了,甚至不用放到Tomcat容器裏,這實在是使人歎服的優雅和簡潔!react
究其本質,SpringBoot將應用打包成了一個fat jar包,而不是咱們常見的jar包。fat jar在啓動時會作一系列隱藏複雜的準備工做,最終呈現爲如此簡單的啓動命令。fat jar技術並非SpringBoot獨創,但確實是SpringBoot將其發揚光大。下面咱們一塊兒來了解一下這個啓動過程。web
首先咱們來看一下SpringBoot fat jar的結構spring
blockchain-0.0.1-SNAPSHOT.jar
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── BlockchinaApplication.class
│ │ └── 應用程序
│ └── lib
│ └── spring-core.jar
│ └── 第三方依賴jar
└── org
└── springframework
└── boot
└── loader
└── JarLauncher
└── WarLauncher
└── springboot啓動程序
複製代碼
每一個jar包都存在一個META-INF/ MANIFEST.MF文件,可粗略理解爲jar包的配置文件。編程
一個典型的SpringBoot fat jar包含如下幾個關鍵部分springboot
Main-Class說明了該fat jar的入口啓動類JarLauncher,執行命令java –jar blockchain-0.0.1-SNAPSHOT.jar的時候JVM會找到JarLauncher並運行它的main方法,源碼以下bash
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
複製代碼
new JarLauncher會調用父類ExecutableArchiveLauncher的無參構造方法服務器
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
複製代碼
archive是SpringBoot對歸檔文件的一個抽象,對於jar包是JarFileArchive,對於文件目錄是ExplodedArchive。
createArchive方法會找到當前類所在的路徑,構造一個Archive。app
launch方法less
protected void launch(String[] args) throws Exception {
// 註冊Handler
JarFile.registerUrlProtocolHandler();
// 找出fat jar裏包含的全部archive,將其全部URL找出來構建LaunchedURLClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 將LaunchedURLClassLoader設置到線程上下文,調起咱們應用的main方法
launch(args, getMainClass(), classLoader);
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
// 能夠看到SpringBoot應用使用的不是APPClassLoader,而是自定義的LaunchedURLClassLoader
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
// 設置線程的ContextLoader
Thread.currentThread().setContextClassLoader(classLoader);
// 調起應用main方法
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 找出MANIFEST.MF的Start-Class屬性,做爲入口啓動類
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
public void run() throws Exception {
// 使用LaunchedURLClassLoader加載應用啓動類
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
// 反射找出main方法並調用
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
複製代碼
每一個jar都會對應一個url,如
jar中的資源,也會對應一個url,並以'!/'分割,如
對於原始的JarFile URL,只支持一個'!/',SpringBoot擴展了此協議,使其支持多個'!/',以實現jar in jar的資源,如
在JarFile.registerUrlProtocolHandler()方法裏,SpringBoot將org.springframework.boot.loader.jar. Handler註冊,該Handler繼承了URLStreamHandler,支持多個jar的嵌套(即jar in jar),是SpringBoot fat jar加載內部jar資源的基礎。
public class Handler extends URLStreamHandler {
}
複製代碼
接下來掃描全部嵌套的jar,構建自定義的LaunchedURLClassLoader,設置到線程上下文,而後找出應用的啓動類,調用main方法。所以到咱們應用的main方法以前,SpringBoot已經幫咱們配置好LaunchedURLClassLoader,而且具備加載BOOT-INF/class(應用自己的類)和BOOT-INF/lib(第三方依賴類)下面的全部類的能力,以上過程用一個圖簡要歸納一下。
若是咱們用IDE(Intellij IDEA或者eclipse)來啓動SpringBoot應用,因爲依賴的jar都已經放到classpath中,故不存在以上過程。本地調試與服務器運行的場景仍是有少量差別。
SpringBoot應用的初始化十分簡潔,只有一行,對應調用SpringApplication.run靜態方法。跟蹤查看該靜態方法,主要完成兩個操做,一是建立SpringApplication對象,二是調用該對象的run方法。這兩個操做看似簡單,實際上包含了大量複雜的初始化操做,下面咱們就一塊兒來一探究竟。
public static void main(String[] args) {
SpringApplication.run(BlockchainApplication.class, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
複製代碼
咱們先看一下SpringApplication的構造方法:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 解析applicationType
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 設置Initializers
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
// 設置Listeners
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
複製代碼
主要包括三個比較重要的地方 deduceFromClasspath會根據classpath特定類是否存在來決定applicationType,總共有三種類型,分別是REACTIVE,SERVLET和NONE。 REACTIVE是響應式web,若是包含
org.springframework.web.reactive.DispatcherHandler
複製代碼
就會認爲是響應式類型 NONE是普通應用程序,若是不包含
javax.servlet.Servlet
org.springframework.web.context.ConfigurableWebApplicationContext
複製代碼
就認爲是普通應用程序
其他狀況就是SERVLET,也是咱們最經常使用的類型
接下來是設置initializer和listener,參數中都調用了getSpringFactoriesInstances,這是SpringBoot一種新的拓展機制,它會掃描classpath下全部包中的META-INF/spring.factories,將特定的類實例化(使用無參構造方法)。 一個典型spring-boot-starter.jar的spring.factories包含如下內容,initializer有4個,listener有9個。
實際上,算上其餘依賴包,initializer應該是有6個,listener有10個。因此SpringApplication有6個實例化後的initializer,10個實例化後的listener。 到此爲止SpringApplication的構造方法結束。
接下來就是run方法了,如下源碼在關鍵地方進行了一些簡單的註釋
public ConfigurableApplicationContext run(String... args) {
// 開啓定時器,統計啓動時間
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 獲取並初始化全部RunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
// 發佈啓動事件
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 準備好環境environment,即配置文件等
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印SpringBoot Logo
Banner printedBanner = printBanner(environment);
// 建立咱們最經常使用的ApplicationContext
context = createApplicationContext();
// 獲取異常報告器,在啓動發生異常的時候用友好的方式提示用戶
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 準備Context,加載啓動類做爲source
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
// Spring初始化的核心邏輯,構建整個容器
refreshContext(context);
afterRefresh(context, applicationArguments);
// 中止計時,統計啓動耗時
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
// 調用runner接口供應用自定義初始化
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方法,先不急着往下分析,咱們來看下SpringApplicationRunListener,從RunListener這個名字看出它是run方法的listener,監聽事件覆蓋了啓動過程的生命週期,從它下手再好不過了。總共有7個狀態以下所示:
將其整理成表格
SpringApplicationRunListener
順序 | 方法名 | 說明 |
---|---|---|
1 | starting Run | 方法調用時立刻執行,最先執行,所以能夠作一些很早期的工做,這個方法沒有參數,能作的事情也很是有限 |
2 | environmentPrepared | 當environment準備好後執行,此時ApplicationContext還沒有建立 |
3 | contextPrepared | 當ApplicationContext準備好後執行,此時還沒有加載source |
4 | contextLoaded | 加載source後調用,此時還沒有refresh |
5 | started | RefreshContext後執行,說明應用已經基本啓動完畢,還沒有調用ApplicationRunner等初始化 |
6 | running | 調用ApplicationRunner後執行,已經進入應用的就緒狀態 |
7 | failed | 啓動過程當中出現異常時執行 |
而EventPublishingRunListener是惟一一個Runlistener,將上面不一樣時間點包裝成一個個事件傳播出去,對應關係以下
SpringApplicationEvent
順序 | 方法名 | 對應事件 |
---|---|---|
1 | starting | ApplicationStartingEvent |
2 | environmentPrepared | ApplicationEnvironmentPreparedEvent |
3 | contextPrepared | ApplicationContextInitializedEvent |
4 | contextLoaded | ApplicationPreparedEvent |
5 | started | ApplicationStartedEvent |
6 | running | ApplicationReadyEvent |
7 | failed | ApplicationFailedEvent |
上面提到的各個事件都是指SpringBoot裏新定義的事件,與原來Spring的事件不一樣(起碼名字不一樣)
EventPublishingRunListener在初始化的時候會讀取SpringApplication裏面的10個listener(上文已經提到過),每當有對應的事件就會通知這10個listener,其中ConfigFileApplicationListener和LoggingApplicationListener與咱們的開發密切相關,簡單介紹以下,有機會再仔細研究。
ConfigFileApplicationListener
響應事件 | 實現功能 |
---|---|
ApplicationEnvironmentPreparedEvent | 查找配置文件,並對其進行解析 |
ApplicationPreparedEvent | 對defaultProperties的配置文件進行排序,基本沒用到 |
LoggingApplicationListener
響應事件 | 實現功能 |
---|---|
ApplicationStartingEvent | 按照logback、log4j、javaLogging的優先順序肯定日誌系統,並預初始化 |
ApplicationEnvironmentPreparedEvent | 對日誌系統進行初始化,此後就可使用日誌系統了 |
ApplicationPreparedEvent | 將日誌系統註冊到spring容器中 |
ContextClosedEvent | 清理日誌系統 |
ApplicationFailedEvent | 清理日誌系統 |
初始化上文提到的SpringApplicationRunListener,而後發佈ApplicationStartingEvent事件。
Environment在Spring的兩個關鍵部分是profiles和properties,引伸出來的兩個關鍵屬性是propertySources(屬性源,即環境變量、啓動參數和配置文件等)和propertyResolver(屬性解析器)。
propertySources SpringBoot根據applicationType(REACTIVE,SERVLET和NONE)建立Environment,在本例中是SERVLET,會建立StandardServletEnvironment,此時有4個PropertySources,分別是
propertyResolver 接下來就是配置propertyResolver,它有一個很重要的屬性是ConversionService,默認包含了各類各樣的轉換器,共132個,根據代碼直觀感覺一下,光是scalar數量相關的就幾十個了。。。
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
複製代碼
最後就是獲取profile,而且發佈ApplicationEnvironmentPreparedEvent事件。上文提到的ConfigFileApplicationListener在收到該事件後,就會對配置文件進行解析工做。
此時配置文件已經解析完成,能夠盡情享用了。SpringBoot將spring.main的屬性綁定到SpringApplication,打印banner(默認尋找classpath下的banner.png/jpg/txt等),而後開始着手構建context。
Context的構建與environment相似,根據ApplicationType(本例是SERVLET)構建AnnotationConfigServletWebServerApplicationContext,這個類的繼承關係很是複雜,我以爲比較關鍵的幾點是:
而後準備異常報告器exceptionReporters,它也以getSpringFactoriesInstances的方式獲取內置的FailureAnalyzers,FailureAnalyzers以一樣的方式從獲取FailureAnalyzer,默認狀況下總共有17個。
其中有一個咱們常常遇到的ConnectorStartFailureAnalyzer,啓動過程當中若是端口被佔用,拋出ConnectorStartFailedException,就會調用該FailureAnalyzer,提示端口被佔用信息。class ConnectorStartFailureAnalyzer extends AbstractFailureAnalyzer<ConnectorStartFailedException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, ConnectorStartFailedException cause) {
return new FailureAnalysis(
"The Tomcat connector configured to listen on port " + cause.getPort()
+ " failed to start. The port may already be in use or the"
+ " connector may be misconfigured.",
"Verify the connector's configuration, identify and stop any process "
+ "that's listening on port " + cause.getPort()
+ ", or configure this application to listen on another port.",
cause);
}
}
複製代碼
值得一提的是,全部FailureAnalyzer都繼承了AbstractFailureAnalyzer
public abstract class AbstractFailureAnalyzer<T extends Throwable> implements FailureAnalyzer {
@Override
public FailureAnalysis analyze(Throwable failure) {
T cause = findCause(failure, getCauseType());
if (cause != null) {
return analyze(failure, cause);
}
return null;
}
}
複製代碼
SpringBoot在根據泛型尋找合適的FailureAnalyzer時,使用了Spring提供的ResolvableType類。該類普遍應用於Spring的源碼中,是Spring設計的基礎。
@Override
public FailureAnalysis analyze(Throwable failure) {
T cause = findCause(failure, getCauseType());
if (cause != null) {
return analyze(failure, cause);
}
return null;
}
// 找出當前類的泛型
protected Class<? extends T> getCauseType() {
return (Class<? extends T>) ResolvableType
.forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric();
}
複製代碼
// 判斷拋出的異常是否當前類泛型的一個實例
protected final <E extends Throwable> E findCause(Throwable failure, Class<E> type) {
while (failure != null) {
if (type.isInstance(failure)) {
return (E) failure;
}
failure = failure.getCause();
}
return null;
}
複製代碼
我的認爲上述設計針對一個FailureAnalyzer對應處理一種Exception的場景十分適合,而ApplicationListener對多種Event進行監聽的場景更適合使用supportsEventType模式。
扯遠了,再次回到咱們的contextPrepared階段,最後一步是調用上文提到的6個initializer,它們都繼承了ApplicationContextInitializer
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C applicationContext);
}
複製代碼
與上文的FailureAnalyzer相似,SpringBoot根據不一樣的ApplicationContext尋找適合的ApplicationContextInitializer進行調用,因此說這種設計思路在Spring應用十分普遍。
其中一個initializer是ConditionEvaluationReportLoggingListener,它會在啓動成功或失敗後打印SpringBoot自動配置(AutoConfiguration)的Condition匹配信息,對於AutoConfiguration的調試十分有用。
最後發佈ApplicationContextInitializedEvent事件,至此contextPrepared階段結束。
這個階段比較簡單,主要往spring容器註冊一些重要的類(此時Spring稱其爲source),其中最最最重要的就是SpringBoot的啓動類了,稱爲PrimarySource。
SpringBoot支持在配置文件中指定附加的Source,但大多數狀況下咱們只有一個啓動類做爲PrimarySource,在此階段註冊到spring容器,做爲後續refreshContext的依據。 接下來發布ApplicationPreparedEvent事件,本階段結束。
終於到了重頭戲,本階段調用了著名的AbstractApplicationContext.refresh()方法,大多數Spring的功能特性都在此處實現,但裏面的邏輯又十分複雜,還夾雜着各類細枝末節,我也在抽空從新理清其主幹脈絡,限於篇幅,會在下一期的文章中着重介紹AbstractApplicationContext.refresh(),此處先行略過,目前咱們只要大概知道它完成了掃描bean,解析依賴關係,實例化單例對象等工做便可。
發佈ApplicationStartedEvent事件,本階段結束。
此時Spring自己已經啓動完了,SpringBoot設計了ApplicationRunner接口供應用進行一些自定義初始化,都會在這階段逐一調用。
發佈ApplicationReadyEvent事件,本階段結束。
若是在上述的階段中拋出異常,就會進入Failed階段,發佈ApplicationFailedEvent事件通知其餘listener,利用上文介紹的FailureAnalyzers報告失敗緣由。
將上面過程用一張來簡要歸納下
至此run方法結束,那麼SpringBoot應用main方法也會跟着結束了,main線程退出。對於普通應用,因爲沒有其餘守護線程,JVM會立刻關閉。對於web應用,Tomcat會啓動一條守護線程,JVM依然保持工做,待Tomcat收到shutdown指令關閉守護線程後,JVM纔會關閉。
關於Spring refreshContext和Tomcat的內容,我將在下期進行介紹,下期再見!