SpringBoot如何啓動與初始化

本文以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的啓動

首先咱們來看一下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

  • Spring-Boot-Version: 2.1.4.RELEASE
  • Main-Class: org.springframework.boot.loader.JarLauncher
  • Start-Class: org.ypq.its.blockchain.BlockchainApplication
  • Spring-Boot-Classes: BOOT-INF/classes/
  • Spring-Boot-Lib: BOOT-INF/lib/
  • Created-By: Apache Maven 3.3.9
  • Build-Jdk: 1.8.0_45

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:file:/blockchain-0.0.1-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

jar中的資源,也會對應一個url,並以'!/'分割,如

  • jar:file:/ blockchain-0.0.1-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

對於原始的JarFile URL,只支持一個'!/',SpringBoot擴展了此協議,使其支持多個'!/',以實現jar in jar的資源,如

  • jar:file:/ blockchain-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

在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應用的初始化

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 清理日誌系統

Starting階段

初始化上文提到的SpringApplicationRunListener,而後發佈ApplicationStartingEvent事件。

environmentPrepared階段

Environment在Spring的兩個關鍵部分是profiles和properties,引伸出來的兩個關鍵屬性是propertySources(屬性源,即環境變量、啓動參數和配置文件等)和propertyResolver(屬性解析器)。

propertySources SpringBoot根據applicationType(REACTIVE,SERVLET和NONE)建立Environment,在本例中是SERVLET,會建立StandardServletEnvironment,此時有4個PropertySources,分別是

  • servletConfigInit
  • servletContextInit
  • systemProperties(user.dir)
  • systemEnviroment(環境變量)

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在收到該事件後,就會對配置文件進行解析工做。

contextPrepared階段

此時配置文件已經解析完成,能夠盡情享用了。SpringBoot將spring.main的屬性綁定到SpringApplication,打印banner(默認尋找classpath下的banner.png/jpg/txt等),而後開始着手構建context。

Context的構建與environment相似,根據ApplicationType(本例是SERVLET)構建AnnotationConfigServletWebServerApplicationContext,這個類的繼承關係很是複雜,我以爲比較關鍵的幾點是:

  1. 擁有beanFactory屬性,在父類GenericApplicationContext裏初始化爲DefaultListableBeanFactory,這也是咱們後面會常常用到的beanFactory實現類
  2. 擁有reader屬性,實現類是AnnotatedBeanDefinitionReader,主要用於編程式註冊的bean。
  3. 擁有scanner屬性,實現類是ClassPathBeanDefinitionScanner,用於尋找Classpath上的候選bean,默認包括被@Component, @Repository,@Service和 @Controller 註解的bean。

而後準備異常報告器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階段結束。

contextLoaded階段

這個階段比較簡單,主要往spring容器註冊一些重要的類(此時Spring稱其爲source),其中最最最重要的就是SpringBoot的啓動類了,稱爲PrimarySource。

SpringBoot支持在配置文件中指定附加的Source,但大多數狀況下咱們只有一個啓動類做爲PrimarySource,在此階段註冊到spring容器,做爲後續refreshContext的依據。 接下來發布ApplicationPreparedEvent事件,本階段結束。

Started階段

終於到了重頭戲,本階段調用了著名的AbstractApplicationContext.refresh()方法,大多數Spring的功能特性都在此處實現,但裏面的邏輯又十分複雜,還夾雜着各類細枝末節,我也在抽空從新理清其主幹脈絡,限於篇幅,會在下一期的文章中着重介紹AbstractApplicationContext.refresh(),此處先行略過,目前咱們只要大概知道它完成了掃描bean,解析依賴關係,實例化單例對象等工做便可。

發佈ApplicationStartedEvent事件,本階段結束。

Running階段

此時Spring自己已經啓動完了,SpringBoot設計了ApplicationRunner接口供應用進行一些自定義初始化,都會在這階段逐一調用。

發佈ApplicationReadyEvent事件,本階段結束。

Failed階段

若是在上述的階段中拋出異常,就會進入Failed階段,發佈ApplicationFailedEvent事件通知其餘listener,利用上文介紹的FailureAnalyzers報告失敗緣由。

小結

將上面過程用一張來簡要歸納下

至此run方法結束,那麼SpringBoot應用main方法也會跟着結束了,main線程退出。對於普通應用,因爲沒有其餘守護線程,JVM會立刻關閉。對於web應用,Tomcat會啓動一條守護線程,JVM依然保持工做,待Tomcat收到shutdown指令關閉守護線程後,JVM纔會關閉。

關於Spring refreshContext和Tomcat的內容,我將在下期進行介紹,下期再見!

相關文章
相關標籤/搜索