spring boot原理分析(六):spring boot應用啓動流程綜述

前言

    在原理分析(一)已經整體概括了spring boot實現,spring boot主要是在已有Servlet容器+Servlet模板的基礎上進行整合。具體來說包括三種,tomcat + spring mvc的模式是其中的一種,另外兩種分別是Undertow+Servlet和Jetty+Servlet的模式。另外,大部分的外部模塊的加入都是使用spring bean的方式進行動態配置,在原理分析(二)(三)(四)(五)中,分別對項目內外bean自動配置進行了分析,並詳細介紹了常見的幾個例子。至此,基於註解的bean的注入已經分析完了。
    本文將會從spring boot
的入口開始分析,展示spring boot在啓動過程中,涉及了哪些組件或者模塊的準備。在介紹spring mvc的DispatcherServlet的文章(tomcat + spring mvc原理(七))中,有過統覽整個流程,再詳細介紹每個局部組件原理的先例。這裏將採用類似的思路。

入口

    前面spring boot的綜述已經說過,spring boot應用內部包含了tomcat和spring mvc,既可以打包成war包,部署在外部的tomcat服務下,也可以打包成jar包,直接啓動內置的tomcat,然後裝載編寫的spring mvc的應用。打包成war是spirng mvc開發過程中常用的,不是很新奇。至於如何使用內置的tomcat啓動,則需要研究一番。

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

    既然要啓動進程,也就逃脫不了使用java的入口函數main。關於上面@SpringBootApplication註解,上面幾篇文章都是關於它的,有興趣可以去查閱。main函數傳入的參數,就是java啓動時的入參。這裏主要的問題是集中在SpringApplication的構造和它的run方法裏。

構造函數的準備

    根據上面SpringApplication的run方法的調用方式,可以很容易推測出run是一個靜態函數。

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
  return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
  return new SpringApplication(primarySources).run(args);
}

在run方法裏構造了一個SpringApplication,只傳入了在main函數中傳入進來的DemoApplication.class。

public SpringApplication(Class<?>... primarySources) {
  this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  //設置resourceLoader,這裏爲null
  this.resourceLoader = resourceLoader;
  Assert.notNull(primarySources, "PrimarySources must not be null");
  //設置基礎類
  this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
  //設置Web應用類型
  this.webApplicationType = WebApplicationType.deduceFromClasspath();
  //設置上下文Context的初始化加載器
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  //設置應用事件監聽器
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  //設置入口類
  this.mainApplicationClass = deduceMainApplicationClass();
}

    構造函數的調用順序就是上面SpringApplication的由上自下,需要注意的是ResourceLoader傳入時就等於null。在下面的SpringApplication的構造函數中,完成了幾項初始化,依次是:

  1. 設置resourceLoader
  2. 設置基礎類
  3. 設置Web應用類型
  4. 設置上下文Context的初始化加載器
  5. 設置應用事件監聽器
  6. 設置入口類

    DemoApplication.class(這個是我自己命名的,名字可以改,都是這個類)作爲基礎類乍一聽很陌生,但是其實在前面的文章裏已經出現了“基礎類包”這個詞(《項目依賴包中容器的自動配置1》的環境上下文:基礎包配置那一段)。@SpringBootApplication是一個@Configuration、@ComponentScan等組合的註解,被這個註解註釋的類所在的包是項目中首要的基礎包,其下所有的子包都會被掃描,如果存在bean會被自動注入到容器中。
    Web應用類型確實是沒出現過,構造函數使用deduceFromClasspath方法設置這個值。deduceFromClasspath方法真如其名,Web應用類型是從class中推測出來的,如果class沒有加載,還可以自行從Class默認路徑中找出來加載。源碼貼出來很多,但是內容很簡單,就是根據靜態String中定義的類有沒有出現來判斷是哪種服務類型。

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
    "org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework." + "web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org." + "springframework.web.reactive.DispatcherHandler";

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

static WebApplicationType deduceFromClasspath() {
  if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
      && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
    return WebApplicationType.REACTIVE;
  }
  for (String className : SERVLET_INDICATOR_CLASSES) {
    if (!ClassUtils.isPresent(className, null)) {
      return WebApplicationType.NONE;
    }
  }
  return WebApplicationType.SERVLET;
}

這裏判斷了三種類型,SERVLET、REACTIVE和NONE。我們講spring mvc是Servlet容器,所以服務即SERVLET WEB類型。REACTIVE也是一種WEB服務的類型,代表着非阻塞響應式編程,正是spirng mvc不擅長的事,具體可以參考spring-webflux。NONE類型說這個服務不是WEB類型,是其他服務類型。
    上下文Context的初始化加載器是用來初始化上下文Context,Context是spring boot加載過程中最重要的模塊,包含了spring boot的配置信息、bean和resource等等,tomcat實例創建工廠和spring mvc的DispatcherServlet都在裏面,可以說上下文Context即萬物。後面會細講。
    應用事件監聽器ApplicationListener是spring boot自己定義的,而不是tomcat文章中(tomcat容器動態加載)提到的容器生命週期管理的容器狀態事件監聽器,ApplicationListener是針對spring boot整個應用的事件監聽,有很多種情況下可能需要使用到,擴展性還是挺好的。這個後面也會細講。
    設置入口類,大家可能會一頭霧水,不是已經傳入了DemoApplication.class嗎?這裏是因爲DemoApplication中不一定要定義main,同理,也不一定在main函數中直接調用SpringApplication.run。deduceMainApplicationClass採用的方法是使用異常棧逆遞歸倒推,找到main函數的類,原理還是挺有意思的,可以借鑑一下。

private Class<?> deduceMainApplicationClass() {
  try {
    StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
    for (StackTraceElement stackTraceElement : stackTrace) {
      if ("main".equals(stackTraceElement.getMethodName())) {
        return Class.forName(stackTraceElement.getClassName());
      }
    }
  }
  catch (ClassNotFoundException ex) {
    // Swallow and continue
  }
  return null;
}

    構造函數裏面還有一個點,是關於spring的,前面也出現過。SpringFactoriesLoader.loadFactoryNames(參考項目依賴包中容器的自動配置1)在getSpringFactoriesInstance中又被用來獲取類的完整類名,然後使用反射獲取構造函數構造了實例,這個部分的細節也值得一看。因爲是技術實現問題,和本主題無關,這裏不多講。

SpringApplication run

public ConfigurableApplicationContext run(String... args) {
  //spring提供的運行時間打印工具,記錄啓動時間
  StopWatch stopWatch = new StopWatch();
  stopWatch.start();
  //主角
  ConfigurableApplicationContext context = null;
  //異常報告
  Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
  //設置java headless模式
  configureHeadlessProperty();
  //獲取應用運行事件監聽器
  SpringApplicationRunListeners listeners = getRunListeners(args);
  listeners.starting();
  try {
    //應用參數構造
    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;
}

    上面是SpringApplication run方法的整個流程,由於是異步的,spring boot啓動時,run方法所在的線程跑完結束後,spring boot應用就算完全啓動了。run方法中,有些組件或者模塊涉及內容比較多,這裏我只會大致過下其作用,後面文章會細分析,有些就比較簡單,可以直接解釋清楚。

啓動StopWatch

    StopWatch是spring提供的一個工具,用來監控和打印運行時間,使用過程包括構造、start和stop,在run方法裏都有提現。最終打印是在StartupInfoLogger方法裏,下面是打印出來的效果。
spring boot原理分析(六):spring boot應用啓動流程綜述-time.png

異常報告

    SpringBootExceptionReporter是可以用來獲取啓動過程中的異常,並上報打印。後面使用了getSpringFactoriesInstances方法進行獲取所有異常上報器,在handleRunFailure方法中使用了這些實例。這個組件需要更加詳細的分析。

java headless模式

    java的服務器開發時會使用這個模式。服務器可能缺少顯示設備、鍵盤或鼠標這些外設,但又需要使用他們提供的功能,生成相應的數據,提供給客戶端。設置這個模式,是告訴程序,不能指望硬件設備提供這些功能,需要依靠系統的計算能力模擬這些特性。設置方法就是:

System.setProperty("java.awt.headless", "true");

應用運行事件監聽器

    SpringApplicationRunListener負責啓動過程中,運行事件的監聽。需要注意,和前面說到的ApplicationListener完全不同,不要混淆。在run方法運行過程中,能夠看到SpringApplicationRunListeners的構造獲取和starting、started、running。starting、started、running方法內部都是遍歷所有SpringApplicationRunListener,單獨調用每個SpringApplicationRunListener相應名字的方法實現的。這個組件也會單獨細講。

應用參數

    ApplicationArguments是spring boot對應用參數進行了封裝,輸入參數是args。

環境配置

    Environment是指應用程序的運行環境,包括profile和properties配置,properties配置不僅包括項目內的properties文件,還有JVM system properties、操作系統環境變量等。由此構造ConfigurableEnvironment需要傳入args也順理成章。

條幅

    Banner是指spring boot啓動過程中,會在日誌中打印一個條幅,顯示本服務基於spring boot。這個應該不用多說吧,炫酷。
spring boot原理分析(六):spring boot應用啓動流程綜述-banner.png

spring boot上下文

    spring boot的上下文Context是spring boot中最重要的一個模塊,內部包含了spring boot服務運行過程中各種重要的信息,比如beanFactory、註冊的bean、environment等等。
    prepareContext方法中對Context進行了初始化,設置了一些Context中的一些內容,比如environment、banner,最後還根據基礎包記錄primarySources,加載了基礎包中的bean。refreshContext方法中調用了context的refresh方法,刷新上下文,創建並啓動了tomcat實例,加載了spring mvc的Servlet,可以說服務的所有啓動工作都是在這個方法中完成的。afterRefresh是一個模板方法,用意是繼承SpringApplication的子類如果想在Context的refresh完成之後做點事,可以實現這個方法。
    所有關於Context的內容,後續會用單獨篇幅來介紹。

啓動後置處理

private void callRunners(ApplicationContext context, ApplicationArguments args) {
  List<Object> runners = new ArrayList<>();
  runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
  runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
  AnnotationAwareOrderComparator.sort(runners);
  for (Object runner : new LinkedHashSet<>(runners)) {
    if (runner instanceof ApplicationRunner) {
      callRunner((ApplicationRunner) runner, args);
    }
    if (runner instanceof CommandLineRunner) {
      callRunner((CommandLineRunner) runner, args);
    }
  }
}

    callRunners(context, applicationArguments)中加載了ApplicationRunner和CommandLineRunner兩種類型的子類實例bean,並調用了它們的run方法。ApplicationRunner和CommandLineRunner是用於在spring boot啓動完成後處理一些用戶自己定義的邏輯,比如加載文件、配置數據庫等等。使用方式就是繼承這兩個類,重載裏面的run方法。至於這兩個類的區別是run方法的輸入參數不一樣,ApplicationRunner中run方法的參數爲ApplicationArguments,而CommandLineRunner接口中run方法的參數爲String數組。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章