深入理解SpringApplication

89961330 2019-11-16

SpringApplication类用于引导和启动一个Spring应用程序(即SpringBoot开发的应用)。通常用SpringBoot开发一个应用程序时,在主类的main函数中可以通过如下代码启动一个Spring应用:

@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类会做如下事情启动应用:

  • 为应用创建一个合适的ApplicationContext
  • 注册一个CommandLinePropertySource,通过CommandLinePropertySource可以对外暴露命令行参数,并将命令行参数与spring应用中用到的properties关联起来
  • 启动ApplicationContext
  • 执行所有的CommandLineRunner类型bean

下面我们通过SpringApplication的源码详细描述上述这些过程。

构建SpringApplication实例

下面是SpringApplication类静态run方法的源码。可以看到,当我们调用这个静态run方法时,实际上会构造一个SpringApplication实例,然后再调用实例的run方法完成spring应用的启动。

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

下面是SpringApplication的构造函数,它主要完成下面初始化工作:

  • 初始化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();
}

下面对这些初始化过程进行一一说明:

  • spring容器配置

SpringApplication能够从各种不同的配置源读取bean的定义。Spring Boot建议采用Java注解配置的方式提供一个全局唯一的配置类。但是,你可以同时使用多种不同的配置源。如果是Java注解的配置方式,会使用AnnotatedBeanDefinitionReader加载配置(通过全类名)。如果是XML的配置方式,则会使用XmlBeanDefinitionReader加载配置(通过XML文件地址)。
如果除了primarySources配置类以外,还需要其它的ApplicationContext配置源,则可以调用SpringApplication#setSources(Set<String> sources)方法进行设置,该方法的参数既可以接受一个配置类的全类名,也可以是一个XML配置文件的地址。

  • 推断应用程序类型

SpringApplication默认的应用类型只有三种:

public enum WebApplicationType {
    /**
     * 非web类应用,无需内嵌web server
     */
    NONE,
    /**
     * servlet类型的web应用,需要启动内嵌的web server
     */
    SERVLET,
    /**
     * reactive类型的web应用,需要启动内嵌的reactive web server
     * 啥是reactive类型的web应用?目前还不知道^_^
     */
    REACTIVE;

判断的逻辑也非常简单,就是检查classpath下是否存在对应的类。

  • 如果classpath下存在org.springframework.web.reactive.DispatcherHandler类,则应用类型是REACTIVE
  • 如果classpath下存在org.springframework.web.servlet.DispatcherServlet类,则应用类型是SERVLET
  • 如果上面两个DispatcherServlet类都不存在,则应用类型是NONE

应用类型直接决定了要创建的ApplicationContext类型,下表整理了三种应用类型和所创建的ApplicationContext间的对应关系:

应用类型ApplicationContext类型
NONEAnnotationConfigApplicationContext
SERVLETAnnotationConfigServletWebServerApplicationContext
REACTIVEAnnotationConfigReactiveWebServerApplicationContext
  • 初始化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进行了配置,如下图所示:

深入理解SpringApplication

  • 推断主类

推断主类过程的实现方式很巧妙,通过遍历异常堆栈找到方法名称是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对象统计的数据打印的。

深入理解SpringApplication

  • 接着,仍然是通过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变得强大吧。

相关推荐