SpringBoot是如何啓動的?這篇文章告訴你答案!

本文是經過查看SpringBoot源碼整理出來的SpringBoot大體啓動流程,總體大方向是以簡單爲出發點,不說太多複雜的東西,內部實現細節本文不深扣由於每一個人的思路、理解都不同。java

首先我將SpringBoot的啓動流程整理成如下階段:react

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

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

關於spring.factories

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

# 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的啓動。架構

SpringApplication初始化

Java程序的入口在main方法SpringBoot的一樣能夠經過main方法啓動,只須要少許的代碼加上@SpringBootApplication註解,很容易的就啓動SpringBoot:app

@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的構造函數幹了些啥:socket

  • 基礎變量賦值(resourceLoader、primarySources、...)
  • 審查ApplicationContext類型如(Web、Reactive、Standard)
  • 加載ApplicationContextInitializer
  • 加載ApplicationListener
  • 審查啓動類(main方法的類)

而後再來逐個分析這些步驟。分佈式

2.1

審查ApplicationContext類型

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

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就會有ConfigurableWebApplicationContext和Servlet類,這樣就決定了程序的ApplicationContext類型爲WebApplicationType.SERVLET。

2.2

加載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包也有可能配置ApplicationContextInitializer來實現額外的初始化工做。

2.3

加載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,\
o

ApplicationListener的加載流程跟ApplicationContextInitializer相似都是經過SpringFactoriesLoader加載的。

2.4

小結

完成初始化階段後,能夠知道如下信息:

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

Environment初始化

初始化工做完成後SpringBoot會幹不少事情來爲運行程序作好準備,SpringBoot啓動核心代碼大部分都位於SpringApplication實例的run方法中,在環境初始化大體的啓動流程包括:

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

固然還會有一些別的操做如:

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

這些不是重要的操做就不講解了,能夠看完文章再細細研究。

3.1

解析命令行參數

令行參數是由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中,提供統一的屬性抽象。

3.2

建立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();
    }
}

3.3

準備Environment

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

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

SpringBoot在準備環境時會調用SpringApplication的prepareEnvironment方法:

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的屬性

3.4

配置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

3.5

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

3.6

Environment初始化小結

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

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

ApplicationContext初始化

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

本文不打算深刻ApplicationContext中,由於與ApplicationContext相關的類不少,不是一兩篇文章寫的完的,建議按模塊來看,最後再整合起來看ApplicationContext源碼。

4.1

建立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

4.2

準備ApplicationContext

建立完ApplicationContext完後須要初始化下它,設置環境、應用ApplicationContextInitializer、註冊Source類等,SpringBoot的準備Context的流程能夠概括以下:

  • 爲ApplicationContext設置環境(以前建立的環境)
  • 基礎設置操做設置BeanNameGenerator、ResourceLoader、ConversionService等
  • 執行ApplicationContextInitializer的initialize方法(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裏面。

4.3

刷新ApplicationContext

若是說SpringBoot的是個汽車,要啓動這輛汽車那前面所作的操做都是開門、系安全帶等基本操做了,刷新ApplicationContext就是點火了,沒刷新ApplicationContext只是保存了一個Bean的定義、後處理器啥的沒有真正跑起來。 刷新context的基本步驟:

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

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

運行程序入口

context刷新完成後Spring容器能夠徹底使用了,接下來SpringBoot會執行ApplicationRunner和CommandLineRunner,這兩接口功能類似都只有一個run方法只是接收的參數不一樣而以。經過實現它們能夠自定義啓動模塊,如啓動dubbo、gRPC等,callRunners方法以下:

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的核心功能仍是自動配置。

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

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

推薦閱讀:

  • 」12306「秒殺系統的設計藝術
  • SpringBoot是如何加載配置文件的?
  • 餓了麼千萬級交易系統的重構設計思路
  • 支付系統高可用架構設計實戰,可用性高達99.999!
  • 大型互聯網公司分佈式ID方案總結

-END-
SpringBoot是如何啓動的?這篇文章告訴你答案!
架構文摘
ArchDigest
架構知識丨大型網站丨大數據丨機器學習
SpringBoot是如何啓動的?這篇文章告訴你答案!若有收穫,點個在看,誠摯感謝圖片

相關文章
相關標籤/搜索