本文是通过查看SpringBoot源码整理出来的SpringBoot大致启动流程,整体大方向是以简单为出发点,不说太多复杂的东西,内部实现细节本文不深扣因为每个人的思路、理解都不一样,我个人看的理解跟大家看的肯定不一样,到时候表达的出来的云里雾里也没啥用。

首先我将SpringBoot的启动流程整理成以下阶段:

  • SpringApplicaiton初始化

    • 审查ApplicationContext类型
    • 加载ApplicationContextInitializer
    • 加载ApplicationListener
  • Environment初始化
    • 解析命令行参数
    • 创建Environment
    • 配置Environment
    • 配置SpringApplication
  • ApplicationContext初始化
    • 创建ApplicationContext
    • 设置ApplicationContext
    • 刷新ApplicationContext
  • 运行程序入口

省去了一些不影响主流程的细节,在查看SpringBoot源码之前,不得不提一下spring.factories这个文件的使用和功能。

关于spring.factories

spring.factories是一个properties文件,它位于classpath:/META-INF/目录里面,每个jar包都可以有spring.factories的文件。Spring提供工具类SpringFactoriesLoader负责加载、解析文件,如spring-boot-2.2.0.RELEASE.jar里面的META-INF目录里面就有spring.factories文件:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader # Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
...

关于spring.factories需要知道些什么?

  • spring.factories是一个properties文件
  • spring.factories里的键值对的value是以逗号分隔的完整类名列表
  • spring.factories里的键值对的key是完整接口名称
  • spring.factories键值对的value是key的实现类
  • spring.factories是由SpringFactoriesLoader工具类加载
  • spring.factories位于classpath:/META-INF/目录
  • SpringFactoriesLoader会加载jar包里面的spring.factories文件并进行合并

知道spring.factories的概念后,继续来分析SpringBoot的启动。

SpringApplicaiton初始化

Java程序的入口在main方法SpringBoot的同样可以通过main方法启动,只需要少量的代码加上@SpringBootApplication注解,很容易的就启动SpringBoot:

@SpringBootApplication
@Slf4j
public class SpringEnvApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringEnvApplication.class, args);
} }

SpringApplicaiton初始化位于SpringApplication的构造函数中:

    public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
} 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();
}

简单的说下SpringApplication的构造函数干了些啥:

  • 基础变量赋值(resourceLoader、primarySources、...)
  • 审查ApplicationContext类型如(Web、Reactive、Standard)
  • 加载ApplicationContextInitializer
  • 加载ApplicationListener
  • 审查启动类(main方法的类)

然后再来逐个分析这些步骤。

审查ApplicationContext类型

SpringBoot会在初始化阶段审查ApplicationContext的类型,审查方式是通过枚举WebApplicationTypededuceFromClasspath静态方法:

	static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}

WebApplicationType枚举用于标记程序是否为Web程序,它有三个值:

  • NONE:不是web程序
  • SERVLET:基于Servlet的Web程序
  • REACTIVE:基于Reactive的Web程序

简单的来说该方法会通过classpath来判断是否Web程序,方法中的常量是完整的class类名:

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

例如通过pom.xml文件引入spring-boot-starter-web那classpath就会有org.springframework.web.context.ConfigurableWebApplicationContextjavax.servlet.Servlet类,这样就决定了程序的ApplicationContext类型为WebApplicationType.SERVLET

加载ApplicationContextInitializer

ApplicationContextInitializer会在刷新context之前执行,一般用来做一些额外的初始化工程如:添加PropertySource、设置ContextId等工作它只有一个initialize方法:

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C applicationContext);
}

SpringBoot通过SpringFactoriesLoader加载spring.factories中的配置读取key为org.springframework.context.ApplicationContextInitializer的value,前面提到过spring.factoies中的配置的value都为key的实现类:

org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

上面列出的是spring-boot-2.2.0.RELEASE.jar中包含的配置,其他jar包也有可能配置org.springframework.context.ApplicationContextInitializer来实现额外的初始化工作。

加载ApplicationListener

ApplicationListener用于监听ApplicationEvent事件,它的初始加载流程跟加载ApplicationContextInitializer类似,在spring.factories中也会配置一些优先级较高的ApplicationListener

# 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

ApplicationListener的加载流程跟ApplicationContextInitializer类似都是通过SpringFactoriesLoader加载的。

小结

完成初始化阶段后,可以知道以下信息:

  • ApplicationContext是Web还是其他类型
  • SpringApplication中有一些ApplicationContextInitializer实现类
  • SpringApplication中有一些ApplicationListener的实现类

Environment初始化

初始化工作完成后SpringBoot会干很多事情来为运行程序做好准备,SpringBoot启动核心代码大部分都位于SpringApplication实例的run方法中,在环境初始化大致的启动流程包括:

  • 解析命令行参数
  • 准备环境(Environment)
  • 设置环境

当然还会有一些别的操作如:

  • 实例化SpringApplicationRunListeners
  • 打印Banner
  • 设置异常报告
  • ...

这些不是重要的操作就不讲解了,可以看完文章再细细研究。

解析命令行参数

命令行参数是由main方法的args参数传递进来的,SpringBoot在准备阶段建立一个DefaultApplicationArguments类用来解析、保存命令行参数。如--spring.profiles.active=dev就会将SpringBoot的spring.profiles.active属性设置为dev。

public ConfigurableApplicationContext run(String... args) {
...
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
...
}

SpringBoot还会将收到的命令行参数放入到Environment中,提供统一的属性抽象。

创建Environment

创建环境的代码比较简单,根据之前提到过的WebApplicationType来实例化不同的环境:

private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

准备Environment

环境(Environment)大致由Profile和PropertyResolver组成:

  • Profile是BeanDefinition的逻辑分组,定义Bean时可以指定Profile使SpringBoot在运行时会根据Bean的Profile决定是否注册Bean
  • PropertyResolver是专门用来解析属性的,SpringBoot会在启动时加载配置文件、系统变量等属性

SpringBoot在准备环境时会调用SpringApplicationprepareEnvironment方法:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
...
return environment;
}

prepareEnvironment方法大致完成以下工作:

  • 创建一个环境
  • 配置环境
  • 设置SpringApplication的属性

配置Environment

创建完环境后会为环境做一些简单的配置:

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService) conversionService);
}
configurePropertySources(environment, args);
configureProfiles(environment, args);
} protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { if (this.addCommandLineProperties && args.length > 0) {
...
sources.addFirst(new SimpleCommandLinePropertySource(args));
...
}
}
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

篇幅有限省去一些不重要的代码,配置环境主要用于:

  • 设置ConversionService: 用于属性转换
  • 将命令行参数添加到环境中
  • 添加额外的ActiveProfiles

SpringApplicaton属性设置

配置SpringApplicaton主要是将已有的属性连接到SpringApplicaton实例,如spring.main.banner-mode属性就对应于bannerMode实例属性,这一步的属性来源有三种(没有自定义的情况):

  • 环境变量
  • 命令行参数
  • JVM系统属性

SpringBoot会将前缀为spring.main的属性绑定到SpringApplicaton实例:

protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}

Environment初始化小结

总结下环境准备阶段所做的大致工作:

  • 根据WebApplicationType枚举创建环境
  • 设置ConversionService用于转换属性变量
  • 将命令行参数args添加到环境
  • 将外部设置的Profiles添加到环境
  • 绑定SprinngApplicaiton属性
  • 发送环境Prepared事件

ApplicationContext初始化

前面提到的一些步骤大部分都是为了准备ApplicationContext所做的工作,ApplicationContext提供加载Bean、加载资源、发送事件等功能,SpringBoot在启动过程中创建、配置好ApplicationContext不需要开发都作额外的工作(太方便啦~~)。

本文不打算深入ApplicationContext中,因为与ApplicationContext相关的类很多,不是一两篇文章写的完的,建议按模块来看,最后再整合起来看ApplicationContext源码

创建ApplicationContext

创建ApplicationContext的过程与创建环境基本模相似,根据WebApplicationType判断程序类型创建不同的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有三个成员(SERVLET,REACTIVE,NONE),分别对应不同的context类型为:

  • SERVLET: AnnotationConfigServletWebServerApplicationContext
  • REACTIVE: AnnotationConfigReactiveWebServerApplicationContext
  • NONE: AnnotationConfigApplicationContext

准备ApplicationContext

创建完ApplicationContext完后需要初始化下它,设置环境、应用ApplicationContextInitializer、注册Source类等,SpringBoot的准备Context的流程可以归纳如下:

  • ApplicationContext设置环境(之前创建的环境)
  • 基础设置操作设置BeanNameGenerator、ResourceLoader、ConversionService等
  • 执行ApplicationContextInitializerinitialize方法(ApplicationContextInitializer是在初始化阶段获取的)
  • 注册命令行参数(springApplicationArguments)
  • 注册Banner(springBootBanner)
  • 注册sources(由@Configuration注解的类)

准备ApplicationContext的代码如下所示:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
applyInitializers(context);
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

注意注册sources这一步,sources是@Configuration注解的类SpringBoot根据提供的sources注册Bean,基本原理是通过解析注解元数据,然后创建BeanDefinition然后将它注册进ApplicationContext里面。

刷新ApplicationContext

如果说SpringBoot的是个汽车,那前面所做的操作都是开门、系安全带等基本操作了,刷新ApplicationContext就是点火了,没刷新ApplicationContext只是保存了一个Bean的定义、后处理器啥的没有真正跑起来。刷新ApplicationContext这个内容很重要,要理解ApplicationContext还是要看刷新操作的源码,

这里先简单列一下基本步骤:

  • 准备刷新(验证属性、设置监听器)
  • 初始化BeanFactory
  • 执行BeanFactoryPostProcessor
  • 注册BeanPostProcessor
  • 初始化MessageSource
  • 初始化事件广播
  • 注册ApplicationListener
  • ...

刷新流程步骤比较多,关联的类库都相对比较复杂,建议先看完其他辅助类库再来看刷新源码,会事半功倍。

运行程序入口

context刷新完成后Spring容器可以完全使用了,接下来SpringBoot会执行ApplicationRunnerCommandLineRunner,这两接口功能相似都只有一个run方法只是接收的参数不同而以。通过实现它们可以自定义启动模块,如启动dubbogRPC等。

ApplicationRunnerCommandLineRunner的调用代码如下:

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);
}
}
}

callRunners执行完后,SpringBoot的启动流程就完成了。

总结

通过查看SpringApplication的源码,发现SpringBoot的启动源码还好理解,主要还是为ApplicationContext提供一个初始化的入口,免去开发人员配置ApplicationContext的工作。SpringBoot的核心功能还是自动配置,下次分析下SpringBoot Autoconfig的源码,要充分理解SpringBoot看源码是少了的。

看完SpringApplication的源码还有些问题值得思考:

  • SpringBoot是启动Tomcat的流程
  • SpringBoot自动配置原理
  • SpringBoot Starter自定义
  • BeanFactoryPostProcessor和BeanPostProcessor实现原理
  • ...



《架构文摘》每天一篇架构领域重磅好文,涉及一线互联网公司应用架构(高可用、高性 能、高稳定)、大数据、机器学习等各个热门领域。

SpringBoot是如何启动的?的更多相关文章

  1. spring-boot 根据环境启动

    spring-boot 根据环境启动: java -jar spring-boot--config--SNAPSHOT.jar --spring.profiles.active=prod

  2. 3.Springboot之修改启动时的默认图案Banner

    一.SpringBoot的默认启动图案 在SpringBoot启动的时候,默认的会展示出一个spring的logo,这个图案我们用户是可以自定义的 二.自定义启动图案 方法一: Application ...

  3. 【玩转SpringBoot】通过事件机制参与SpringBoot应用的启动过程

    生命周期和事件监听 一个应用的启动过程和关闭过程是归属到“生命周期”这个概念的范畴. 典型的设计是在启动和关闭过程中会触发一系列的“事件”,我们只要监听这些事件,就能参与到这个过程中来. 要想监听事件 ...

  4. SpringBoot项目快速启动停止脚本

    SpringBoot项目快速启动停止脚本 1.在jar包同级目录下,创建 app.sh #!/bin/bash appName=`ls|grep .jar$` if [ -z $appName ] t ...

  5. SpringBoot 应用程序启动过程探秘

    概述 说到接触 SpringBoot 伊始,给我第一映像最深的是有两个关键元素: 对照上面的典型代码,这个两个元素分别是: @SpringBootApplication SpringApplicati ...

  6. 【玩转SpringBoot】SpringBoot应用的启动过程一览表

    SpringBoot应用的启动方式很简单,就一行代码,如下图01: 其实这行代码背后主要执行两个方法,一个是构造方法,一个是run方法. 构造方法主要内容就是收集一些数据,和确认一些信息.如下图02: ...

  7. SpringBoot学习之启动探究

    SpringApplication是SpringBoot的启动程序,我们通过它的run方法可以快速启动一个SpringBoot应用.可是这里面到底发生了什么?它是处于什么样的机制简化我们程序启动的?接 ...

  8. SpringBoot 标签之启动

    在SpringBoot中入口我们使用: package com.sankuai.qcs.regulation.traffic; import org.springframework.boot.Spri ...

  9. springboot之docker启动参数传递

    这几天有网友问,如何在使用docker的情况下传递spring.profiles.active=test,也就是说springboot切换配置文件.以往我们直接通过java启动jar的时候,直接跟上- ...

随机推荐

  1. C/C++中变量的作用域和存储类型简介

    写在开头 对于很多C/C++的初学者来说,很容易理不清变量的作用域和存储类型这一块的一些概念,也容易将其中的一些概念搞混淆.作为一个C/C++的初学者,笔者希望在这里能够尝试着去理一理这些较为繁杂的概 ...

  2. 阿里云服务器CentOS6.9 tomcat配置域名访问

    之前一直是ip访问项目,今天申请到一个测试域名,想要用设置用域名访问项目. 1.进入阿里云服务器中,修改tomcat中server.xml文件 cd /usr/local/apache-tomcat/ ...

  3. linux脚本入门之终端显示输出

    主要基本命令为 echo 与 printf. 关于echo: 其语法结构为:echo -选项参数 字符串: 例如:echo hello,world   echo 'hello,world'  echo ...

  4. office2019激活

    这个是在网上偶然看见的一个激活方式,分享一下. 复制如下代码保存后修改文件后缀名为".bat",请注意有一个点,然后保存以管理员身份运行即可: @echo off(cd /d &q ...

  5. Java中的static(1)【持续更新】——关于Eclipse的No enclosing instance of type ... 错误的理解和改正

    No enclosing instance of type SomeClass is accessible. Must qualify the allocation with an enclosing ...

  6. Shell之三剑客

    目录 Shell之三剑客 参考 Grep Sed Awk Shell之三剑客

  7. OKR群:为什么说每个程序员都应该有自己的个人OKR

    个人OKR OKR,即Object and Key Result,是IT大厂最近争相推广的目标管理工具,例如腾讯.百度和头条(字节跳动). 其实,OKR并不是仅仅只适用于公司和部门内部,我们个人也可以 ...

  8. 什么是VR中的Locomotion?

    Locomotion,本文中我称之为移位,是VR研究中最重要的话题之一.因为它属于VR中三大元老级操作(Selection选择,Manipulation操纵物体,Locomotion移位),其中,前两 ...

  9. WordPress 使用本地化的 emoji 表情包

    WordPress 结合使用 Native Emoji 和 WP Local Emoji 两个插件,可以达到使用本地化的 emoji 表情包的目的. 安装好上述两个插件并且启用: 为了使 Native ...

  10. 【Python3爬虫】我爬取了七万条弹幕,看看RNG和SKT打得怎么样

    一.写在前面 直播行业已经火热几年了,几个大平台也有了各自独特的“弹幕文化”,不过现在很多平台直播比赛时的弹幕都基本没法看的,主要是因为网络上的喷子还是挺多的,尤其是在观看比赛的时候,很多弹幕不是喷选 ...