Spring boot 启动过程分析

总有人替你做了一些事儿

我们常常惊叹于 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. 保存环境信息
    2. IOC容器的后置处理流程(Spring 容器的中的经典 postProcessApplicationContext()方法就是发生在这一步)
    3. 开始初始化所有初始化器;applyInitializers;
      1. 遍历所有的 ApplicationContextInitializer (还记得我们开头说的从spring.factories 中加载的7个初始化器吗?就是在这一步进行每一个初始化的)。调用 initialize.。来对ioc容器进行初始化扩展功能
    4. 遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器****contextPrepared
    5. 打印启动日志,这一步同样可配置
    6. 从容器中获取 beanFactory(熟悉Spring的同学应该只是,这其实就是一个更加简易版本的 ApplicationContext),并且顺带将 命令行传入的参数作为一个 bean 组件注入到 ConfigurableListableBeanFactory 中。
    7. 所有的监听器 调用 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揭秘》,虽然该书讲的知识已经比较久远,并且不再更新,但是无论是其描述问题的方式还是娓娓道来讲解方式,都对得起『揭秘』二字。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值