26

SpringBoot是如何启动的?

 4 years ago
source link: http://www.cnblogs.com/xwgblog/p/11798560.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文是通过查看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 加载的。

小结

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

ApplicationContextInitializer
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
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实现原理
  • ...

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK