[深入剖析Spring Boot]启动、事件通知与配置加载原理

概述

Spring历来一直是JAVA研发中不可或缺的框架,它提供了完美的控制反转功能,使应用能够达到低耦合的设计规范。在微服务时代,Spring Boot的免配置设计更是丢弃了原来复杂的xml声明模式,使程序员能更加专心业务代码的编写。

最近在面试时,我问过很多同学关于Spring生命周期的问题,可能完整的回答上来的人寥寥无几。Spring其实并不难,反而是很经典。其架构设计巧妙,代码逻辑清晰,即便对编程不太了解的人也能看得懂,不信你继续往下看。

本文主要关注Spring Boot是如何创建启动类和如何管理bean的生命周期这两点内容,我会深入到Spring Boot框架的源码,从神秘的 SpringApplication 类开始,一步步揭开Spring Boot美丽的面纱。如果你是初学者,这篇文章可能会带给你更多迷茫,请移步搜索Spring Boot基础使用教程。

问题

首先来看一个最简单的Spring Boot应用,代码如下。

@SpringBootApplication
public class PeopleBatchApplication {

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

public static void main 多简单熟悉的语句,就差一个System.out.println("Hello World")就是我们几年前初次接触java时的第一个程序。这段代码中的 SpringApplication.run(PeopleBatchApplication.class, args);语句就是Spring Boot的 Hello World,这一句代码就完整的创建了一个Spring的运行环境。

我们的问题就此开始,这句代码是如何完成Spring上下文创建以及相关bean的声明呢?那个在主类上标记的奇怪的@SpringBootApplication注解是什么?Spring Boot如何识别web环境,并创建Servlet上下文环境?Spring Boot是如何识别

Spring Boot 如何创建上下文环境

创建 SpringApplication

SpringApplication类是Spring Boot应用的标配,它可以启动Spring应用并加载配置文件,并创建Spring上下文环境。

源码展示

SpringApplication类部分源码如下。

public class SpringApplication {

    /**
    *这个是在入门示例中所调用的方法
    **/
    public static ConfigurableApplicationContext run(Object source, String... args) {
        return run(new Object[] { source }, args);
    }
    
    /**
    * 创建可配置的应用上下文
    * @param sources 要作为配置类的对象
    * @param args 程序启动参数
    */
    public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return new SpringApplication(sources).run(args);
    }

    /**
    * 构造方法
    */
    public SpringApplication(Object... sources) {
        initialize(sources);
    }
    
    /**
    * 初始化 SpringApplication 的方法
    **/
    private void initialize(Object[] sources) {
        if (sources != null && sources.length > 0) {
            this.sources.addAll(Arrays.asList(sources));
        }
        this.webEnvironment = deduceWebEnvironment();
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }

    private boolean deduceWebEnvironment() {
        for (String className : WEB_ENVIRONMENT_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return false;
            }
        }
        return true;
    }
}

初始化SpringApplication对象流程

SpringApplication对象的初始化经过了以下步骤:

  1. 记录下用户指定的Spring Boot配置类信息,并将配置类对象存入到全局变量 Set<Object> sources
  2. 验证运行环境是否为web环境,并将验证结果存入 boolean webEnvironment全局变量。
  3. 设置需要新初始化的上下文配置对象(需要在 META_INFO/spring.factories 文件中指定的bean工厂创建类的实例,默认实现类有org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer,org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,org.springframework.boot.context.ContextIdApplicationContextInitializer,org.springframework.boot.context.config.DelegatingApplicationContextInitializer,org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer这几个类)。并将初始化类放入到 List<ApplicationContextInitializer<?> initializers 全局变量。
  4. 设置默认bean监听器(默认加载spring-boot-autoconfigure.jar包中META_INFO/spring.factories文件所指定的监听器实现org.springframework.boot.autoconfigure.BackgroundPreinitializer)。并将初始化监听器放入到 List<ApplicationListener<?>> listeners
  5. 设置main方法所在的类到全局变量 Class mainApplicationClass

小结

SpringApplication对象在初始化时主要将应用的各个配置都存入全部变量,并没有进行逻辑操作。如果我们需要自定义bean工厂或者监听器的话,可以选择在classpath下建立自己的META_INF/spring.factories文件分别指定初始化bean工厂和bean监听器。注意自建的初始化工厂或监听器并没有覆盖spring原有的bean工厂。

从上面的代码我们可以看出,只要classpath中存在javax.servlet.Servletorg.springframework.web.context.ConfigurableWebApplicationContext类,Spring Boot都自动构建Servlet运行环境。

SpringApplication 的 run 方法

run方法主要用于创建或刷新一个应用上下文,是 Spring Boot的核心。

run 方法源码

比起 SpringApplication 的创建的代码,run方法的逻辑复杂了许多。不但有加载配置文件,创建上下文环境的逻辑,还有对创建Spring上下文的计时信息,另外我们启用Spring Boot应用时所打印的那个字符串图像标识,也是在这个方法上控制的(如果你很不喜欢这个标识,可以考虑在这里去掉)。

    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            analyzers = new FailureAnalyzers(context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            listeners.finished(context, null);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, analyzers, ex);
            throw new IllegalStateException(ex);
        }
    }

run 方法执行流程

  1. 创建计时器,用于记录SpringBoot应用上下文的创建所耗费的时间。
  2. 开启所有的SpringApplicationRunListener监听器,用于监听Sring Boot应用加载与启动信息。
  3. 创建应用配置对象(main方法的参数配置) ConfigurableEnvironment
  4. 创建要打印的Spring Boot启动标记 Banner
  5. 创建 ApplicationContext应用上下文对象,web环境和普通环境使用不同的应用上下文。
  6. 创建应用上下文启动失败原因分析对象 FailureAnalyzers
  7. 刷新应用上下文,并从xml、properties、yml配置文件或数据库中加载配置信息,并创建已配置的相关的单例bean。到这一步,所有的非延迟加载的Spring bean都应该被创建成功。
  8. 调用实现了*Runner类型的bean的run方法,开始应用启动。
  9. 完成Spring Boot启动监听
  10. 打印Spring Boot上下文启动耗时
  11. 如果在上述步骤中有异常发生则日志记录下才创建上下文失败的原因并抛出IllegalStateException异常。

运行事件

事件就是Spring Boot启动过程的状态描述,在启动Spring Boot时所发生的事件一般指:

  • 开始启动事件
  • 环境准备完成事件
  • 上下文准备完成事件
  • 上下文加载完成
  • 应用启动完成事件

Spring启动时的事件都是继承自 SpringApplicationEvent 抽象类,每一个事件都包含了应用的 SpringApplication 对象和应用程序启动时的参数。

SpringApplicationRunListener 运行监听器

顾名思意,运行监听器的作用就是为了监听 SpringApplication 的run方法的运行情况。在设计上监听器使用观察者模式,以总信息发布器 SpringApplicationRunListeners 为基础平台,将Spring启动时的事件分别发布到各个用户或系统在 META_INF/spring.factories文件中指定的应用初始化监听器中。使用观察者模式,在Spring应用启动时无需对启动时的其它业务bean的配置关心,只需要正常启动创建Spring应用上下文环境。各个业务'监听观察者'在监听到spring开始启动,或环境准备完成等事件后,会按照自己的逻辑创建所需的bean或者进行相应的配置。观察者模式使run方法的结构变得清晰,同时与外部耦合降到最低。

运行时监听器继承自 SpringApplicationRunListener 接口,其代码如下:

package org.springframework.boot;
public interface SpringApplicationRunListener {

    /**
     * 在run方法业务逻辑执行、应用上下文初始化前调用此方法
     */
    void starting();

    /**
     * 当环境准备完成,应用上下文被创建之前调用此方法
     */
    void environmentPrepared(ConfigurableEnvironment environment);

    /**
     * 在应用上下文被创建和准备完成之后,但上下文相关代码被加载执行之前调用。因为上下文准备事件和上下文加载事件难以明确区分,所以这个方法一般没有具体实现。
     */
    void contextPrepared(ConfigurableApplicationContext context);

    /**
     *当上下文加载完成之后,自定义bean完全加载完成之前调用此方法。
     */
    void contextLoaded(ConfigurableApplicationContext context);

    /**
     *当run方法执行完成,或执行过程中发现异常时调用此方法。
     */
    void finished(ConfigurableApplicationContext context, Throwable exception);
}

默认情况下Spring Boot会实例化EventPublishingRunListener作为运行监听器的实例。在实例化运行监听器时需要SpringApplication对象和用户对象作为参数。其内部维护着一个事件广播器(被观察者对象集合,前面所提到的在META_INF/spring.factories中注册的初始化监听器的有序集合 ),当监听到Spring启动等事件发生后,就会将创建具体事件对象,并广播推送给各个被观察者。

运行事件广播

下面的代码来自SimpleApplicationEventMulticaster,主要描述如何将Spring Boot启动时的各个事件推送到被观察者。

/**
* 将接受的事件进行广播
*/
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        Executor executor = getTaskExecutor();
        if (executor != null) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    invokeListener(listener, event);
                }
            });
        }
        else {
            invokeListener(listener, event);
        }
    }
}

/**
* 将给定的事件发送到指定的监听器
*/
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
        try {
            doInvokeListener(listener, event);
        }
        catch (Throwable err) {
            errorHandler.handleError(err);
        }
    }
    else {
        doInvokeListener(listener, event);
    }
}

@SuppressWarnings({"unchecked", "rawtypes"})
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
    try {
        listener.onApplicationEvent(event);
    }
    catch (ClassCastException ex) {
        String msg = ex.getMessage();
        if (msg == null || msg.startsWith(event.getClass().getName())) {
            // Possibly a lambda-defined listener which we could not resolve the generic event type for
            Log logger = LogFactory.getLog(getClass());
            if (logger.isDebugEnabled()) {
                logger.debug("Non-matching event type for listener: " + listener, ex);
            }
        }
        else {
            throw ex;
        }
    }
}

细读这段代,之所以我们的应用会启动的很慢,很大的原因就是因为在创建应用时我们对事件的处理机制都是同步的,如果业务逻辑允许,我们将广播方法改为异步的(通过 public void setTaskExecutor(Executor taskExecutor)可以借助线程池实现异步),可能会大幅提高应用启动速度。

运行业务监听器

这里的'运行业务监听器'指的是每个组件对Spring Boot启动事件的监听器,其主要作用对在Spring启动状态做出明确响应。如日志监听器LoggingApplicationListener会对启动时的状态做日志记录,ConfigFileApplicationListener会在接收到环境配置完成事件后解析加载配置文件。

下面以ConfigFileApplicationListener为例,简要的看看运行业务监听器时怎么处理事件的。

package org.springframework.boot.context.config;
public class ConfigFileApplicationListener
        implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
            
    /*
    *收到事件请求后执行这个方法
    *配置文件监听器只监听环境配置完成事件和上下文加载完成事件
    **/
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

    /**
    * 获取所有环境配置处理器,并根据事件所给出的环境执行加载文件配置任务
    **/
    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }
    
    
    /**
    * 环境配置方法
    * 首先加载应用配置文件 application.properties 包括已激活的profiles的配置文件
    * 其次配置是否忽略bean的信息
    * 最后将配置文件的配置信息绑定到 SpringApplication中
    */
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
        configureIgnoreBeanInfo(environment);
        bindToSpringApplication(environment, application);
    }
        }

onApplicationEnvironmentPreparedEvent()方法相关代码的主要作用是首先从META_INF/spring.factories中找到所有属性名为org.springframework.boot.env.EnvironmentPostProcessor的环境前置处理器,并将其加入到有序的环境处理器列表中,且ConfigFileApplicationListener类就恰恰在这个前置处理器列表里。然后逐个执行每个环境前置处理器的前置处理方法。

@Enable* 注解就是通过在 META_INF/spring.factories 中配置 org.springframework.boot.autoconfigure.EnableAutoConfiguration 属性,其值指向对注解的解析类而实现的。学会了这段内容后,我们也可以自己设计一个 spring-boot-*-start.jar 包,并完成其自动配置了。

对于ConfigFileApplicationListener的环境前置处理方法请注意,当收到环境配置完成事件后从classpath中加载并解析application.properties/application.yml 配置文件,在以下位置的应用配置文件都会被扫描到:
<ul>
<li>file:./config/:</li>
<li>file:./</li>
<li>classpath:config/</li>
<li>classpath:</li>
</ul>
注意路径的扫描顺序,在不同路径下的应用配置文件中如果有相同的属性,后加载的属性会覆盖先加载的属性。另外如果在应用配置文件中指定了 spring.config.location 属性,该路径下的配置文件也会自动被spring扫描并加载。

Spring Boot配置属性的优先级(前面会覆盖后面的):

  1. 命令行启动参数的配置
  2. 后加载的配置文件中的属性会覆盖先加载的
  3. 后加载的 application-profiles.yml 中的属性会覆盖先加载的 application.yml

引用

本文是对Spring Boot源码的深度分析,为为在此首先特别感谢Spring团队无私的将经典的代码开源,另外感谢我的同事对我写作此文期间给予真切的鼓励与支持。

关于

文章内容着重源码分析与设计详解,可能没有对实际使用的用例,因此不太适合Spring Boot的初学者。

后记

本文主要介绍了Spring Boot启动时的主体流程、事件解耦设计与配置加载原理,后续下篇文章将深入剖析上下文环境 ApplicationContext 的加载原理与流程。

本文内容主要是对 Spring Boot 1.5.9RELEASE的源码解析,不过作者水平有限,有不尽然的地方敬请指出。本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容