SpringBoot是如何启动的

5次阅读

共计 13867 个字符,预计需要花费 35 分钟才能阅读完成。

本文是通过查看 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 实现原理


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

正文完
 0