暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Spring boot 启动的基本流程

大黄奔跑 2021-08-10
513

总有人替你做了一些事儿

我们常常惊叹于 Spring boot 的极大简化开发者的开发流程,将我们从苦逼的 xml 配置中释放出来,可以更加愉快欢乐的摸鱼了。

但是有没有想过几行简单的启动程序,Spring boot 是如何做到的呢?

下面这个程序可以说是最最根本的一个 Spring boot 启动类了,只需要一个 main 方法,就可以让我们只关注业务的开发。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloSpringBootStarterTestApplication {

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

1. SpringBoot启动阶段划分

关于 Spring boot 启动过程,其实可以分为两部分

1、Spring boot 的配置过程。【主要是SpringApplication类的构造方法】

2、Spring boot 启动过程。【真正核心的启动类】

2. Spring boot 的配置过程

首先会调用 SpringApplication() 实例化方法,主要是创建 SpringApplication 实例对象,该方法主要是调用重载的构造器。

// 该方法只是调用重载的实例化构造器
public SpringApplication(Class<?>... primarySources) {
  this(null, primarySources);
}

接下来加载一些系统资源,这里是加载用户自定义配置的主要入口,主要流程主要分为四步:

1、判断应用的类型,NON,SERVLET,REACTIVE 类型,正常情况下,我们这里都是 SERVLET 类型的服务。

2、去spring.factories找 ApplicationContextInitializer,主要是读取该文件信息 spring-boot-2.3.12.RELEASE.jar!/META-INF/spring.factories
,凡是在该文件中订单的初始化器,此时都会被读取到,这里通过 debug 发现主要有 7个容器初始化器。

以后凡是这种 getSpringFactoriesInstances()
(下面会仔细分析这个方法)都是需要从  对应的 spring.factories 文件中寻找初始化器(比如容器初始化、监听器初始化)

初始化器

3、同理。去 spring.factories
找 监听器,获取的思路与 Initializer 一致。

4、寻找有 main 方法的类,这里就可以寻找到我们主程序类了。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  this.resourceLoader = resourceLoader;
  this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

  //  1. WebApplicationType是枚举类,有NONE,SERVLET,REACTIVE,下行webApplicationType是SERVLET
  this.webApplicationType = WebApplicationType.deduceFromClasspath();

  // 2. 去spring.factories找 ApplicationContextInitializer
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

  // 3. 去spring.factories找 监听器,获取的思路与Initializer一致
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

  // 4. 寻找有main方法的类,这里就可以寻找到我们主程序类了。
  this.mainApplicationClass = deduceMainApplicationClass();
}

1. 绕不开的getSpringFactoriesInstances()

主要是加载各种提前预置的初始化器,主要思路如下:

1、 ApplicationContext 场景下的获取类加载器 ,如何获取呢?本质是通过调用 ClassUtils.getDefaultClassLoader(); 获取默认类加载器

2、获取各种配置到 spring.factories 中的初始化器,并且用 set 集合存储,防止重复

3、根据上一步获取的名称开始创建初始化器。实现方式是通过 BeanUtils.instantiateClass() 方式来创建。

4、根据各个初始化器上的 Order 注解对初始化器进行排序。

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) {
  // 1. ApplicationContext 场景下的获取类加载器 
  ClassLoader classLoader = getClassLoader();

  // 2. 获取各种配置到 spring.factories 中的初始化器,并且用 set 集合存储
  Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));

  // 3. 根据上一步获取的名称开始创建初始化器
  List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);

  // 4. 对初始化器进行排序
  AnnotationAwareOrderComparator.sort(instances);
  return instances;
}

绘制一个简单的时序图来说明 Spring boot
在启动之前如何进行配置的

spring boot配置

3. 运行 Spring应用的开始

其实前面说了这么多只是介绍 Spring 的配置过程,下面才开始真正的程序运行。

这里的运行包括了应用程序的运行和创建及初始化容器的过程(Spring 容器初始化过程),我将大致的启动流程划分为

  1. 开始的运行。

  2. 设置 java.awt.headless 属性,作用目前未知。

  3. 获取所有 SpringApplicationRunListener(运行监听器)【为了方便所有 Listener 进行事件感知】**

  4. 调用 starting() 表明,容器开始启动了

  5. 将在容器中使用的参数(一般通过启动命令传入的参数)包装成 ApplicationArguments 对象,

  6. 准备环境基本配置

    1. 返回或者创建基础环境信息对象

    2. 决定用哪个配置文件以及读取配置文件值

    3. 监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成

    4. 绑定环境信息

  7. 打印banner(也就是每次启动的时候打印的 Spring boot 标识)

  8. 创建IOC容器【到了启动的重点了】到这一步还只是创建的一个空的容器

    1. 根据项目类型(Servlet)创建容器

    2. 主要是创建 ConfigurableApplicationContext 容器

  9. 准备容器信息

    1. 遍历所有的 ApplicationContextInitializer (还记得我们开头说的从spring.factories 中加载的7个初始化器吗?就是在这一步进行每一个初始化的)。调用 initialize.。来对ioc容器进行初始化扩展功能

    2. 保存环境信息

    3. IOC容器的后置处理流程(Spring 容器的中的经典 postProcessApplicationContext()方法就是发生在这一步)

    4. 开始初始化所有初始化器;applyInitializers;

    5. 遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器contextPrepared

    6. 打印启动日志,这一步同样可配置

    7. 从容器中获取 beanFactory(熟悉Spring的同学应该只是,这其实就是一个更加简易版本的 ApplicationContext),并且顺带将 命令行传入的参数作为一个 bean 组件注入到 ConfigurableListableBeanFactory 中。

    8. 所有的监听器 调用 contextLoaded。通知所有的监听器  contextLoaded 事件;

  10. 刷新IOC容器。这一步到了Spring 底层的 refresh() 方法了,AbstractApplicationContext#refresh()
    方法主要是刷新容器

    这里的内容过长,后续做一篇文章单独分析,只需要知道经过这一步,基本上准备好了所有的容器的内容了

  11. 所有监听 器 调用 listeners.started(context); 通知所有的监听器容器已经成功started

  12. 调用所有runners

    1. 获取容器中的 ApplicationRunner

    2. 获取容器中的  CommandLineRunner

    3. 合并所有runner并且按照@Order进行排序

    4. 遍历所有的runner。调用 run 方法,该方法适合于用一些容器启动时顺便做的一次性工作

  13. 调用 listeners 的 running()
    方法,通知所有的监听器,容器正在启动。

到这里,整个 Spring boot
应用算是完整启动了。

Spring boot启动过程

源码简单浏览

public ConfigurableApplicationContext run(String... args) {
  // 1. 开始的运行计时
  StopWatch stopWatch = new StopWatch();
  stopWatch.start();

  ConfigurableApplicationContext context = null;
  // 2.设置 java.awt.headless 属性,作用目前未知。
  configureHeadlessProperty();

  // 3. 创建 SpringApplicationRunListeners ,本质还是将 /META-INF/spring.factories 中的监听器包装成 SpringApplicationRunListeners
  SpringApplicationRunListeners listeners = getRunListeners(args);

  // 4. 调用 starting() 表明,容器开始启动了
  listeners.starting();
  try {

    // 5. 将在容器中使用的参数(一般通过启动命令传入的参数)包装成 ApplicationArguments 对象,
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

    // 6. 准备环境基本配置(包括决定用哪个配置文件、)
    ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    configureIgnoreBeanInfo(environment);
    // 7. 打印banner(也就是每次启动的时候打印的 Spring boot 标识)
    Banner printedBanner = printBanner(environment);
    // 8. 创建IOC容器【到了启动的重点了】到这一步还只是创建的一个空的容器
    context = createApplicationContext();

    // 9. 准备容器信息
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);

    // 10. 刷新IOC容器
    refreshContext(context);


    afterRefresh(context, applicationArguments);
    stopWatch.stop();
    if (this.logStartupInfo) {
      new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
    }

    // 11. 通知所有的监听器容器已经成功started
    listeners.started(context);

    // 12. 调用所有runners
    callRunners(context, applicationArguments);
  }
  catch (Throwable ex) {
    handleRunFailure(context, ex, listeners);
    throw new IllegalStateException(ex);
  }    
  // 13. 告知所有的监听器,容器已经启动了,正在运行
  listeners.running(context);

  // 这里暂时忽略不需要关心的异常处理情况
  return context;
}

总结

分析到这里,Spring boot 的启动流程只能说大致分析完成了,这里由于篇幅及避免过于陷于细节,只是分析了主干流程,其实每个流程如果拆开说可以分出多篇文章,比如第十步的刷新容器过程(refreshContext(context)
) ,算是 Spring 源码的核心了,里面包括了容器如何初始化的、如何设置各个组件等,后续会单独给出分析文章。

关于 Spring、Spring boot 的书籍还是再次推荐《Spring揭秘》,虽然该书讲的知识已经比较久远,并且不再更新,但是无论是其描述问题的方式还是娓娓道来探索方式,都对得起『揭秘』二字。

文章转载自大黄奔跑,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论