SpringBoot是如何啟動的?

  • 2019 年 11 月 5 日
  • 筆記

本文是通過查看SpringBoot源碼整理出來的SpringBoot大致啟動流程,整體大方向是以簡單為出發點,不說太多複雜的東西,內部實現細節本文不深扣因為每個人的思路、理解都不一樣,我個人看的理解跟大家看的肯定不一樣,到時候表達的出來的雲里霧裡也沒啥用。

首先我將SpringBoot的啟動流程整理成以下階段:

  • SpringApplicaiton初始化
    • 審查ApplicationContext類型
    • 載入ApplicationContextInitializer
    • 載入ApplicationListener
  • Environment初始化
    • 解析命令行參數
    • 創建Environment
    • 配置Environment
    • 配置SpringApplication
  • ApplicationContext初始化
    • 創建ApplicationContext
    • 設置ApplicationContext
    • 刷新ApplicationContext
  • 運行程式入口

省去了一些不影響主流程的細節,在查看SpringBoot源碼之前,不得不提一下spring.factories這個文件的使用和功能。

關於spring.factories

spring.factories是一個properties文件,它位於classpath:/META-INF/目錄裡面,每個jar包都可以有spring.factories的文件。Spring提供工具類SpringFactoriesLoader負責載入、解析文件,如spring-boot-2.2.0.RELEASE.jar裡面的META-INF目錄裡面就有spring.factories文件:

# PropertySource Loaders  org.springframework.boot.env.PropertySourceLoader=  org.springframework.boot.env.PropertiesPropertySourceLoader,  org.springframework.boot.env.YamlPropertySourceLoader    # Run Listeners  org.springframework.boot.SpringApplicationRunListener=  org.springframework.boot.context.event.EventPublishingRunListener  ...

關於spring.factories需要知道些什麼?

  • spring.factories是一個properties文件
  • spring.factories里的鍵值對的value是以逗號分隔的完整類名列表
  • spring.factories里的鍵值對的key是完整介面名稱
  • spring.factories鍵值對的value是key的實現類
  • spring.factories是由SpringFactoriesLoader工具類載入
  • spring.factories位於classpath:/META-INF/目錄
  • SpringFactoriesLoader會載入jar包裡面的spring.factories文件並進行合併

知道spring.factories的概念後,繼續來分析SpringBoot的啟動。

SpringApplicaiton初始化

Java程式的入口在main方法SpringBoot的同樣可以通過main方法啟動,只需要少量的程式碼加上@SpringBootApplication註解,很容易的就啟動SpringBoot:

@SpringBootApplication  @Slf4j  public class SpringEnvApplication {      public static void main(String[] args) {          ConfigurableApplicationContext context = SpringApplication.run(SpringEnvApplication.class, args);      }    }

SpringApplicaiton初始化位於SpringApplication的構造函數中:

    public SpringApplication(Class<?>... primarySources) {          this(null, primarySources);      }        public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {          this.resourceLoader = resourceLoader;          Assert.notNull(primarySources, "PrimarySources must not be null");          this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));          this.webApplicationType = WebApplicationType.deduceFromClasspath();          setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));          setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));          this.mainApplicationClass = deduceMainApplicationClass();      }

簡單的說下SpringApplication的構造函數幹了些啥:

  • 基礎變數賦值(resourceLoader、primarySources、…)
  • 審查ApplicationContext類型如(Web、Reactive、Standard)
  • 載入ApplicationContextInitializer
  • 載入ApplicationListener
  • 審查啟動類(main方法的類)

然後再來逐個分析這些步驟。

審查ApplicationContext類型

SpringBoot會在初始化階段審查ApplicationContext的類型,審查方式是通過枚舉WebApplicationTypededuceFromClasspath靜態方法:

    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;      }

WebApplicationType枚舉用於標記程式是否為Web程式,它有三個值:

  • NONE:不是web程式
  • SERVLET:基於Servlet的Web程式
  • REACTIVE:基於Reactive的Web程式

簡單的來說該方法會通過classpath來判斷是否Web程式,方法中的常量是完整的class類名:

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";

例如通過pom.xml文件引入spring-boot-starter-web那classpath就會有org.springframework.web.context.ConfigurableWebApplicationContextjavax.servlet.Servlet類,這樣就決定了程式的ApplicationContext類型為WebApplicationType.SERVLET

載入ApplicationContextInitializer

ApplicationContextInitializer會在刷新context之前執行,一般用來做一些額外的初始化工程如:添加PropertySource、設置ContextId等工作它只有一個initialize方法:

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {      void initialize(C applicationContext);  }

SpringBoot通過SpringFactoriesLoader載入spring.factories中的配置讀取key為org.springframework.context.ApplicationContextInitializer的value,前面提到過spring.factoies中的配置的value都為key的實現類:

org.springframework.context.ApplicationContextInitializer=  org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,  org.springframework.boot.context.ContextIdApplicationContextInitializer,  org.springframework.boot.context.config.DelegatingApplicationContextInitializer,  org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,  org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

上面列出的是spring-boot-2.2.0.RELEASE.jar中包含的配置,其他jar包也有可能配置org.springframework.context.ApplicationContextInitializer來實現額外的初始化工作。

載入ApplicationListener

ApplicationListener用於監聽ApplicationEvent事件,它的初始載入流程跟載入ApplicationContextInitializer類似,在spring.factories中也會配置一些優先順序較高的ApplicationListener

# Application Listeners  org.springframework.context.ApplicationListener=  org.springframework.boot.ClearCachesApplicationListener,  org.springframework.boot.builder.ParentContextCloserApplicationListener,  org.springframework.boot.context.FileEncodingApplicationListener,  org.springframework.boot.context.config.AnsiOutputApplicationListener,  org.springframework.boot.context.config.ConfigFileApplicationListener,  org.springframework.boot.context.config.DelegatingApplicationListener,  org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,  org.springframework.boot.context.logging.LoggingApplicationListener,  org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

ApplicationListener的載入流程跟ApplicationContextInitializer類似都是通過SpringFactoriesLoader載入的。

小結

完成初始化階段後,可以知道以下資訊:

  • ApplicationContext是Web還是其他類型
  • SpringApplication中有一些ApplicationContextInitializer實現類
  • SpringApplication中有一些ApplicationListener的實現類

Environment初始化

初始化工作完成後SpringBoot會幹很多事情來為運行程式做好準備,SpringBoot啟動核心程式碼大部分都位於SpringApplication實例的run方法中,在環境初始化大致的啟動流程包括:

  • 解析命令行參數
  • 準備環境(Environment)
  • 設置環境

當然還會有一些別的操作如:

  • 實例化SpringApplicationRunListeners
  • 列印Banner
  • 設置異常報告

這些不是重要的操作就不講解了,可以看完文章再細細研究。

解析命令行參數

命令行參數是由main方法的args參數傳遞進來的,SpringBoot在準備階段建立一個DefaultApplicationArguments類用來解析、保存命令行參數。如--spring.profiles.active=dev就會將SpringBoot的spring.profiles.active屬性設置為dev。

public ConfigurableApplicationContext run(String... args) {      ...      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);      ...  }

SpringBoot還會將收到的命令行參數放入到Environment中,提供統一的屬性抽象。

創建Environment

創建環境的程式碼比較簡單,根據之前提到過的WebApplicationType來實例化不同的環境:

private ConfigurableEnvironment getOrCreateEnvironment() {      if (this.environment != null) {          return this.environment;      }      switch (this.webApplicationType) {      case SERVLET:          return new StandardServletEnvironment();      case REACTIVE:          return new StandardReactiveWebEnvironment();      default:          return new StandardEnvironment();      }  }

準備Environment

環境(Environment)大致由Profile和PropertyResolver組成:

  • Profile是BeanDefinition的邏輯分組,定義Bean時可以指定Profile使SpringBoot在運行時會根據Bean的Profile決定是否註冊Bean
  • PropertyResolver是專門用來解析屬性的,SpringBoot會在啟動時載入配置文件、系統變數等屬性

SpringBoot在準備環境時會調用SpringApplicationprepareEnvironment方法:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,          ApplicationArguments applicationArguments) {      // Create and configure the environment      ConfigurableEnvironment environment = getOrCreateEnvironment();      configureEnvironment(environment, applicationArguments.getSourceArgs());      ConfigurationPropertySources.attach(environment);      listeners.environmentPrepared(environment);      bindToSpringApplication(environment);      ...      return environment;  }

prepareEnvironment方法大致完成以下工作:

  • 創建一個環境
  • 配置環境
  • 設置SpringApplication的屬性

配置Environment

創建完環境後會為環境做一些簡單的配置:

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {      if (this.addConversionService) {          ConversionService conversionService = ApplicationConversionService.getSharedInstance();          environment.setConversionService((ConfigurableConversionService) conversionService);      }      configurePropertySources(environment, args);      configureProfiles(environment, args);  }    protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {        if (this.addCommandLineProperties && args.length > 0) {              ...              sources.addFirst(new SimpleCommandLinePropertySource(args));              ...          }      }      protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {          Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);          profiles.addAll(Arrays.asList(environment.getActiveProfiles()));          environment.setActiveProfiles(StringUtils.toStringArray(profiles));      }      

篇幅有限省去一些不重要的程式碼,配置環境主要用於:

  • 設置ConversionService: 用於屬性轉換
  • 將命令行參數添加到環境中
  • 添加額外的ActiveProfiles

SpringApplicaton屬性設置

配置SpringApplicaton主要是將已有的屬性連接到SpringApplicaton實例,如spring.main.banner-mode屬性就對應於bannerMode實例屬性,這一步的屬性來源有三種(沒有自定義的情況):

  • 環境變數
  • 命令行參數
  • JVM系統屬性

SpringBoot會將前綴為spring.main的屬性綁定到SpringApplicaton實例:

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初始化小結

總結下環境準備階段所做的大致工作:

  • 根據WebApplicationType枚舉創建環境
  • 設置ConversionService用於轉換屬性變數
  • 將命令行參數args添加到環境
  • 將外部設置的Profiles添加到環境
  • 綁定SprinngApplicaiton屬性
  • 發送環境Prepared事件

ApplicationContext初始化

前面提到的一些步驟大部分都是為了準備ApplicationContext所做的工作,ApplicationContext提供載入Bean、載入資源、發送事件等功能,SpringBoot在啟動過程中創建、配置好ApplicationContext不需要開發都作額外的工作(太方便啦~~)。

本文不打算深入ApplicationContext中,因為與ApplicationContext相關的類很多,不是一兩篇文章寫的完的,建議按模組來看,最後再整合起來看ApplicationContext源碼

創建ApplicationContext

創建ApplicationContext的過程與創建環境基本模相似,根據WebApplicationType判斷程式類型創建不同的ApplicationContext

protected ConfigurableApplicationContext createApplicationContext() {      Class<?> contextClass = this.applicationContextClass;      if (contextClass == null) {          try {              switch (this.webApplicationType) {              case SERVLET:                  contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);                  break;              case REACTIVE:                  contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);                  break;              default:                  contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);              }          }          catch (ClassNotFoundException ex) {              throw new IllegalStateException(                      "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);          }      }      return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);  }

前面提到過WebApplicationType有三個成員(SERVLET,REACTIVE,NONE),分別對應不同的context類型為:

  • SERVLET: AnnotationConfigServletWebServerApplicationContext
  • REACTIVE: AnnotationConfigReactiveWebServerApplicationContext
  • NONE: AnnotationConfigApplicationContext

準備ApplicationContext

創建完ApplicationContext完後需要初始化下它,設置環境、應用ApplicationContextInitializer、註冊Source類等,SpringBoot的準備Context的流程可以歸納如下:

  • ApplicationContext設置環境(之前創建的環境)
  • 基礎設置操作設置BeanNameGenerator、ResourceLoader、ConversionService等
  • 執行ApplicationContextInitializerinitialize方法(ApplicationContextInitializer是在初始化階段獲取的)
  • 註冊命令行參數(springApplicationArguments)
  • 註冊Banner(springBootBanner)
  • 註冊sources(由@Configuration註解的類)

準備ApplicationContext的程式碼如下所示:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,          SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {      context.setEnvironment(environment);      postProcessApplicationContext(context);      applyInitializers(context);      listeners.contextPrepared(context);      if (this.logStartupInfo) {          logStartupInfo(context.getParent() == null);          logStartupProfileInfo(context);      }      // Add boot specific singleton beans      ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();      beanFactory.registerSingleton("springApplicationArguments", applicationArguments);      if (printedBanner != null) {          beanFactory.registerSingleton("springBootBanner", printedBanner);      }      if (beanFactory instanceof DefaultListableBeanFactory) {          ((DefaultListableBeanFactory) beanFactory)                  .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);      }      if (this.lazyInitialization) {          context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());      }      // Load the sources      Set<Object> sources = getAllSources();      Assert.notEmpty(sources, "Sources must not be empty");      load(context, sources.toArray(new Object[0]));      listeners.contextLoaded(context);  }

注意註冊sources這一步,sources是@Configuration註解的類SpringBoot根據提供的sources註冊Bean,基本原理是通過解析註解元數據,然後創建BeanDefinition然後將它註冊進ApplicationContext裡面。

刷新ApplicationContext

如果說SpringBoot的是個汽車,那前面所做的操作都是開門、系安全帶等基本操作了,刷新ApplicationContext就是點火了,沒刷新ApplicationContext只是保存了一個Bean的定義、後處理器啥的沒有真正跑起來。刷新ApplicationContext這個內容很重要,要理解ApplicationContext還是要看刷新操作的源碼,
這裡先簡單列一下基本步驟:

  • 準備刷新(驗證屬性、設置監聽器)
  • 初始化BeanFactory
  • 執行BeanFactoryPostProcessor
  • 註冊BeanPostProcessor
  • 初始化MessageSource
  • 初始化事件廣播
  • 註冊ApplicationListener

刷新流程步驟比較多,關聯的類庫都相對比較複雜,建議先看完其他輔助類庫再來看刷新源碼,會事半功倍。

運行程式入口

context刷新完成後Spring容器可以完全使用了,接下來SpringBoot會執行ApplicationRunnerCommandLineRunner,這兩介面功能相似都只有一個run方法只是接收的參數不同而以。通過實現它們可以自定義啟動模組,如啟動dubbogRPC等。

ApplicationRunnerCommandLineRunner的調用程式碼如下:

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執行完後,SpringBoot的啟動流程就完成了。

總結

通過查看SpringApplication的源碼,發現SpringBoot的啟動源碼還好理解,主要還是為ApplicationContext提供一個初始化的入口,免去開發人員配置ApplicationContext的工作。SpringBoot的核心功能還是自動配置,下次分析下SpringBoot Autoconfig的源碼,要充分理解SpringBoot看源碼是少了的。

看完SpringApplication的源碼還有些問題值得思考:

  • SpringBoot是啟動Tomcat的流程
  • SpringBoot自動配置原理
  • SpringBoot Starter自定義
  • BeanFactoryPostProcessor和BeanPostProcessor實現原理


《架構文摘》每天一篇架構領域重磅好文,涉及一線互聯網公司應用架構(高可用、高性 能、高穩定)、大數據、機器學習等各個熱門領域。