SpringBoot应用启动流程分析

  SpringApplicationSpringBoot提供的帮助应用程序启动的引导类,它负责

  1. 创建合适的ApplicationContext
  2. 将命令行参数融入Environment抽象
  3. 刷新ApplicationContext,初始化所有的singleton bean
  4. 触发所有的CommandLineRunner回调

本文中的源码分析基于spring-boot-2.1.5.RELEASE,各位看官还请注意下版本。

构造函数

  大多数时候,我们使用SpringApplication#run(...)工厂方法来创建SpringApplication实例,其实本质上都是对构造函数的调用。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 资源加载器,一般为空,因为 ApplicationContext 本身实现了 ResourceLoader 接口
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 主配置类,一般情况下是标注了 @SpringBootApplication 注解的类
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 根据 jar 包的引入情况判定应用类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 获取 spring.factories 中配置的 ApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 获取 spring.factories 中配置的 ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 获取 main 函数入口类
    this.mainApplicationClass = deduceMainApplicationClass();
}

primarySources是创建ApplicationContext时传递给它的主配置类,一般情况下是标注了@SpringBootApplication注解的类;webApplicationType则根据类路径下jar包的引入情况来进行推断,比如引入了spring-boot-starter-web就会被推断为WebApplicationType.SERVLET#setInitializers(...)#setListeners(...)则使用之前讲解过的Spring SPI机制来获取对应的实现;最后mainApplicationClass则对应调用栈中声明了main函数的类。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // 使用 Set 防止重复
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 反射创建实例,其中 parameterTypes 是构造函数形参类型,args 是实参
    // 具体代码基本上就是 clazz.getDeclaredConstructor(parameterTypes).newInstance(args)
    // 节约篇幅,就不放出来了
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // 基于 @Order/Ordered 排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

这里对Spring SPI机制的使用是非常直接的,同时也告诉了我们如何注册自定义的ApplicationContextInitializerApplicationListener,比如在自定义starterMETA-INF/spring.factories中加入:

org.springframework.context.ApplicationListener=example.MyApplicationListener

就完成了对MyApplicationListener的注册。

启动过程

public ConfigurableApplicationContext run(String... args) {
    // 用于记录启动时长
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    // 容纳 SPI SpringBootExceptionReporter 的实现
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 服务端程序一般都是没有UI的
    configureHeadlessProperty();
    // 获取 SPI SpringApplicationRunListener 的实现
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 回调 SpringApplicationRunListener#starting(...),此时 #run(...) 刚开始运行
    listeners.starting();

    try {
        // 封装命令行参数
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 1. 创建 Environment 并进行配置
        // 2. 回调 SpringApplicationRunListener#environmentPrepared(...),此时 Environment 已创建
        // 3. 为 ConfigurationProperties binding 做准备
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        // 打印 banner
        Banner printedBanner = printBanner(environment);
        // 根据 webApplicationType 来创建 ApplicationContext
        context = createApplicationContext();
        // 获取 SPI SpringBootExceptionReporter 的实现
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        // 1. 配置 ApplicationContext
        // 2. 运行 ApplicationInitializer(s)
        // 3. 回调 SpringApplicationRunListener#contextPrepared(...),
        //    此时 ApplicationContext 已经创建,但 BeanDefinition 还未加载
        // 4. 加载 Bean 定义信息,但不刷新容器,此时已经有了创建 Bean 的蓝图 BeanDefinition
        // 5. 回调 SpringApplicationRunListener#contextLoaded(...),此时 BeanDefinition 已经
        //    悉数加载完毕,但容器未刷新,对应的 bean 实例还未创建
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        // 刷新容器,初始化所有 non-lazy-init 的 bean
        refreshContext(context);
        // 钩子函数
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // 回调 SpringApplicationRunListener#started(...),此时容器已刷新,bean 也初始化完毕
        listeners.started(context);
        // 回调容器中所有的 ApplicationRunner 和 CommandLineRunner
        callRunners(context, applicationArguments);
    } catch (Throwable ex) {
        // 1. 回调 SpringApplicationRunListener#failed(...)
        // 2. 使用 SpringBootExceptionReporter 来处理异常
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 回调 SpringApplicationRunListener#running(...),此时不仅容器已刷新,各种 *Runner 也已回调
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }

    return context;
}

#run(...)方法虽然略长,逻辑却是很清晰的:

  1. 创建Environment并进行配置
  2. 创建ApplicationContext并进行配置
  3. 加载配置类并解析出对应的BeanDefinition
  4. 刷新容器,根据BeanDefinition创建Bean
  5. 回调*Runner接口

SpringApplicationRunListener则提供了对#run(...)过程中关键节点的回调,默认注册有EventPublishingRunListener——它使用ApplicationEventMulticaster发出对应的事件。其中比较重要的有ApplicationEnvironmentPreparedEventApplicationReadyEventSpringCloud中的很多特性,比如Bootstrap Context的创建和Environment的动态刷新就是基于这些事件来触发的。SpringBootExceptionReporter则提供了对#run(...)过程中异常的处理,默认注册的FailureAnalyzers会打印异常信息并提供一个解决方案,类似下面的输出相信大家都不会陌生(笑)。

***************************
APPLICATION FAILED TO START
***************************

Description:

Field restTemplate in example.TestController required a single bean, but 2 were found:
    - restTemplate1: defined by method 'restTemplate1' in class path resource [example/Config.class]
    - restTemplate2: defined by method 'restTemplate2' in class path resource [example/Config.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

创建Environment并进行配置

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
                                                  ApplicationArguments applicationArguments) {
    // 根据 webApplicationType 来创建 Environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 1. 添加 ConversionService 用于类型转换
    // 2. 添加 defaultProperties 和 commandLineArgs 两个 PropertySource
    // 3. 合并 additionalProfiles 到 activeProfiles
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 回调,发出 ApplicationEnvironmentPreparedEvent 事件
    listeners.environmentPrepared(environment);
    // 将 spring.main 前缀的配置项应用到 SpringApplication,比如 allowBeanDefinitionOverriding
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    // 将 PropertySource 转换成 ConfigurationPropertySource
    // 这一步是为了支持 ConfigurationProperties binding
    ConfigurationPropertySources.attach(environment);
    return environment;
}

创建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创建对应的ApplicationContext,比如引入了spring-boot-starter-web就会创建AnnotationConfigServletWebServerApplicationContext,进而启动内嵌的Tomcat容器,这部分内容之前已经聊过了,大家可以翻翻看。

加载配置类并解析出对应的BeanDefinition

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
      SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    // 设置使用的 Environment
   context.setEnvironment(environment);
    // 设置 ResourceLoader、ClassLoader和ConversionService
   postProcessApplicationContext(context);
    // 应用上前面加载好的 ApplicationContextInitializer
   applyInitializers(context);
    // 回调,发出 ApplicationPreparedEvent 事件
   listeners.contextPrepared(context);
   if (this.logStartupInfo) {
      logStartupInfo(context.getParent() == null);
      logStartupProfileInfo(context);
   }
   // 单独注册一下几个比较特殊的 Bean
   ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
   beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
   if (printedBanner != null) {
      beanFactory.registerSingleton("springBootBanner", printedBanner);
   }
    // 将 allowBeanDefinitionOverriding 配置项应用到 ApplicationContext 底层的 BeanFactory
   if (beanFactory instanceof DefaultListableBeanFactory) {
      ((DefaultListableBeanFactory) beanFactory)
            .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
   }
   // primarySources 和以编程方式加入的其它配置源
   Set<Object> sources = getAllSources();
   Assert.notEmpty(sources, "Sources must not be empty");
    // 从以上配置源中解析出 BeanDefinition
    // 这部分属于 spring-context 包的内容了,无非是使用 AnnotatedBeanDefinitionReader
    // ClassPathBeanDefinitionScanner 等组件来加载并解析
   load(context, sources.toArray(new Object[0]));
    // 回调,发出 ApplicationPreparedEvent 事件
   listeners.contextLoaded(context);
}

大家很熟悉的自动装配就发生在这一阶段,@EnableAutoConfiguration注解一般是标注在primarySource上的,看一下它的定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

   String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

   /**
    * 待排除的自动配置类
    */
   Class<?>[] exclude() default {};
    
   /**
    * 待排除的自动配置类名
    */
   String[] excludeName() default {};
}

它通过@Import注解向容器中导入了一个ImportSelector接口的实现——AutoConfigurationImportSelector,并在这里实现了自动装配逻辑。鉴于这部分内容比较多,咱们下次可以展开聊聊。

刷新容器,根据BeanDefinition创建Bean

protected void refresh(ApplicationContext applicationContext) {
   Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
   ((AbstractApplicationContext) applicationContext).refresh();
}

刷新容器直接调用ApplicationContext#refresh()方法即可,这将导致所有non-lazy-initsingleton bean得到初始化。

回调*Runner接口

private void callRunners(ApplicationContext context, ApplicationArguments args) {
   List<Object> runners = new ArrayList<>();
    // 获取容器中所有类型为 ApplicationRunner 的 bean
   runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    // 获取容器中所有类型为 CommandLineRunner 的 bean
   runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    // 基于 @Order/Ordered 排序 
   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);
      }
   }
}

这部分代码是自解释的,不多提了。

结语

  今天我们一起分析了SpringBoot应用的启动流程,也仅仅是主体的启动流程。Spring本身是完全开放、极易扩展的,大家也看到了伴随着应用启动的各种回调,众多的SpringBoot特性都是在这些扩展点中得到实现的,这也是接下来的文章要分析的内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容