深刻理解SpringApplication

SpringApplication類用於引導和啓動一個Spring應用程序(即SpringBoot開發的應用)。一般用SpringBoot開發一個應用程序時,在主類的main函數中能夠經過以下代碼啓動一個Spring應用:html

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

SpringApplication的靜態方法run(Class<?> primarySource, String... args))的第一個參數接受一個Spring容器配置類(用Java代碼對Spring容器進行配置)。第二個參數是命令行參數。將命令行參數轉發給SpringApplication類,就能夠在用java命令啓動應用時,經過命令行參數對Spring應用作一些配置。
SpringApplication類會作以下事情啓動應用:java

  • 爲應用建立一個合適的ApplicationContext
  • 註冊一個CommandLinePropertySource,經過CommandLinePropertySource能夠對外暴露命令行參數,並將命令行參數與spring應用中用到的properties關聯起來
  • 啓動ApplicationContext
  • 執行全部的CommandLineRunner類型bean

下面咱們經過SpringApplication的源碼詳細描述上述這些過程。react

構建SpringApplication實例

下面是SpringApplication類靜態run方法的源碼。能夠看到,當咱們調用這個靜態run方法時,實際上會構造一個SpringApplication實例,而後再調用實例的run方法完成spring應用的啓動。web

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

下面是SpringApplication的構造函數,它主要完成下面初始化工做:spring

  • 初始化Spring容器的配置類primarySources
  • 推斷應用程序的類型,進而根據應用程序的類型建立恰當的ApplicationContext
  • 初始化指定的ApplicationContextInitializer列表
  • 初始化指定的ApplicationListener列表
  • 推斷main class的類名稱
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();
}

下面對這些初始化過程進行一一說明:app

  • spring容器配置

SpringApplication可以從各類不一樣的配置源讀取bean的定義。Spring Boot建議採用Java註解配置的方式提供一個全局惟一的配置類。可是,你能夠同時使用多種不一樣的配置源。若是是Java註解的配置方式,會使用AnnotatedBeanDefinitionReader加載配置(經過全類名)。若是是XML的配置方式,則會使用XmlBeanDefinitionReader加載配置(經過XML文件地址)。
若是除了primarySources配置類之外,還須要其它的ApplicationContext配置源,則能夠調用SpringApplication#setSources(Set<String> sources)方法進行設置,該方法的參數既能夠接受一個配置類的全類名,也能夠是一個XML配置文件的地址。less

  • 推斷應用程序類型

SpringApplication默認的應用類型只有三種:函數

public enum WebApplicationType {
    /**
     * 非web類應用,無需內嵌web server
     */
    NONE,
    /**
     * servlet類型的web應用,須要啓動內嵌的web server
     */
    SERVLET,
    /**
     * reactive類型的web應用,須要啓動內嵌的reactive web server
     * 啥是reactive類型的web應用?目前還不知道^_^
     */
    REACTIVE;

判斷的邏輯也很是簡單,就是檢查classpath下是否存在對應的類。spring-boot

  • 若是classpath下存在org.springframework.web.reactive.DispatcherHandler類,則應用類型是REACTIVE
  • 若是classpath下存在org.springframework.web.servlet.DispatcherServlet類,則應用類型是SERVLET
  • 若是上面兩個DispatcherServlet類都不存在,則應用類型是NONE

應用類型直接決定了要建立的ApplicationContext類型,下表整理了三種應用類型和所建立的ApplicationContext間的對應關係:ui

應用類型 ApplicationContext類型
NONE AnnotationConfigApplicationContext
SERVLET AnnotationConfigServletWebServerApplicationContext
REACTIVE AnnotationConfigReactiveWebServerApplicationContext
  • 初始化ApplicationContextInitializer&ApplicationListener

初始化ApplicationContextInitializer和ApplicationListener的過程比較類似,都是藉助於SpringFactoriesLoader的方式完成初始化的,因此放到一塊兒講述。
SpringFactoriesLoader會讀取META-INF/spring.factories文件中的配置。一個工程項目中能夠同時有多個META-INF/spring.factories文件(每一個jar中都能有一個)。
例如在spring-boot-autoconfigure和spring-boot兩個jar的META-INF/spring.factories文件中,均有針對ApplicationContextInitializer的配置:

# spring-boot-autoconfigure jar中的META-INF/spring.factories
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# spring-boot jar中的META-INF/spring.factories
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

SpringApplication的initializers就由這兩部分配置的類共同組成,一共有6個ApplicationContextInitializer的實現類。
同理下面是關於listeners的配置,一共有10個ApplicationListener實現類。

# spring-boot-autoconfigure jar中的META-INF/spring.factories
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# spring-boot jar中的META-INF/spring.factories
# 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

此處介紹一個IDEA使用的小技巧,咱們能夠藉助IDEA的find usage功能啓動找到哪裏對ApplicationListener進行了配置,以下圖所示:

clipboard.png

  • 推斷主類

推斷主類過程的實現方式很巧妙,經過遍歷異常堆棧找到方法名稱是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;
}

運行階段

在完成SpringApplication對象的構建和初始化以後,就開始執行run方法的邏輯了。具體代碼以下,下面將介紹其執行流程。

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    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;
}
  • 方法執行伊始,會先建立一個StopWatch對象,該對象用於統計應用的啓動時間。下圖中的啓動時間日誌就是根據StopWatch對象統計的數據打印的。

clipboard.png

  • 接着,仍然是經過SpringFactoriesLoader的機制加載全部的SpringApplicationRunListener。從名字就能夠看出,SpringApplicationRunListener的做用就是監聽SpringApplication.run方法的各個執行階段,也能夠理解成SpringApplication運行的生命週期。加載完全部的SpringApplicationRunListener後,會將其包裝在SpringApplicationRunListeners中,後者就是前者的集合。能夠經過調用SpringApplicationRunListeners的API操做全部的SpringApplicationRunListener。
  • listeners.starting()這句代碼通知全部的SpringApplicationRunListener:「注意啦、注意啦,SpringBoot應用要開始啓動啦」。
  • 根據ApplicationType類型,建立並配置SpringApplication要使用的Environment(包括配置要使用的PropertySource和Profile)。
  • listeners.environmentPrepared(environment)代碼再次通知全部的SpringApplicationRunListener:「注意啦、注意啦,SpringBoot應用使用的Environment準備好啦」。
  • 若是showBanner屬性的值是true,打印banner
  • 根據初始化時推斷的ApplicationType結果,建立對應類型的ApplicationContext。而後根據條件決定是否添加ShutdownHook,是否使用自定義的BeanNameGenerator,是否使用自定義的ResourceLoader。固然,最重要的是將以前建立好的Environment設置給建立好的ApplicationContext
  • 調用全部ApplicationContextInitializer的initialize方法,完成ApplicationContext的初始化。
  • listeners.contextPrepared(context)通知全部的SpringApplicationRunListener:「注意啦、注意啦,SpringBoot應用使用的ApplicationContext準備好啦」。
  • 向ApplicationContext中加載全部的bean,bean的定義能夠來自多個source,每一個source既能夠是XML文件形式的bean配置,也能夠Java註解形式的bean配置。在加載bean的過程當中,若是配置了@EnableAutoConfiguration註解的話將會執行自動裝配。加載bean定義的代碼以下所示:
/**
 - Load beans into the application context.
 - @param context the context to load beans into
 - @param sources the sources to load
 */
protected void load(ApplicationContext context, Object[] sources) {
    if (logger.isDebugEnabled()) {
        logger.debug(
                "Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
    }
    BeanDefinitionLoader loader = createBeanDefinitionLoader(
            getBeanDefinitionRegistry(context), sources);
    if (this.beanNameGenerator != null) {
        loader.setBeanNameGenerator(this.beanNameGenerator);
    }
    if (this.resourceLoader != null) {
        loader.setResourceLoader(this.resourceLoader);
    }
    if (this.environment != null) {
        loader.setEnvironment(this.environment);
    }
    loader.load();
}
  • listeners.contextLoaded(context)通知全部的SpringApplicationRunListener:「注意啦、注意啦,SpringBoot應用要使用到的bean都已經加載進ApplicationContext啦」。
  • 調用ApplicationContext的refresh方法,完成Spring容器可用的最後一道工序。
  • listeners.started(context)通知全部的SpringApplicationRunListener:「注意啦、注意啦,SpringBoot應用的ApplicationContext已經啓動啦」。
  • 檢查當前的Spring容器中是否有ApplicationRunner和CommandLineRunner類型的bean,若是有的話就遍歷執行它們,代碼以下:
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);
        }
    }
}
  • listeners.running(context)通知全部的SpringApplicationRunListener:「注意啦、注意啦,SpringBoot應用已經開始運行啦」。

至此,整個run方法執行結束,Spring應用也完成了整個啓動流程。

總結

SpringApplication類將一個SpringBoot應用的啓動流程模板化,並在啓動過程當中提供了一些擴展點讓咱們能夠根據具體需求進行擴展(經過SpringApplicationRunListener機制)。SpringBoot提供的就是這樣一個標準化的平臺,在這個平臺上既能夠運行普通java應用,也能夠運行web應用。SpringBoot平臺會根據classpath自動判斷應用類型,建立對應類型的ApplicationContext和加載恰當的配置。固然,除了平臺的能力外,SpringBoot還提供了不少XXXAutoConfiguration支持自動裝配。我想,正是平臺的能力和自動裝配的能力,才讓SpringBoot變得強大吧。

相關文章
相關標籤/搜索