前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot 系列-启动过程分析

SpringBoot 系列-启动过程分析

作者头像
用户4044670
发布2020-03-02 10:10:15
7480
发布2020-03-02 10:10:15
举报
文章被收录于专栏:安徽开发者圈安徽开发者圈

02

小伙伴们,大家好!昨天因为操作失误,所以没有更新成功。在这个中国澳门回归20周年的今天,我继续更新SpringBoot 系列的第二篇,上次文章还没看的小伙伴,可以点击查看:《SpringBoot 系列-FatJar 启动原理》

我将持续更新磊叔的SpringBoot 系列文章,还请大家多多关注,多多转发,为我们开发者圈多分享些干货,在此谢过各位了!

文 | 磊叔

SpringBoot 作为目前非常流行的微服务框架,它使得构建独立的 Spring 生产级应用变得非常简单,因此受到很多互联网企业的青睐。

最近在写 SOFATracer 集成 Spring Cloud Stream RocketMQ 的过程中,遇到了一些问题,比如:BeanPostProcessor 不生效,如何在 BeanPostProcessor 不生效的情况下去修改一个 Bean 等,这些问题其实都是和 Bean 的生命周期有关系的,当然也和容器启动的过程有关系。

SpringBoot 的启动过程对于我来说其实不算陌生,也可以说是比较熟悉,但是之前没有完整的梳理过这一块的东西,在实际的应用过程成难免再去踩一些坑。另外想到之前也写过一篇 SpringBoot系列- FatJar 启动原理,刚好承接上篇,继续来探索 SpringBoot 中的一些知识点。

注:本篇基于 SpringBoot 2.1.0.RELEASE 版本,SpringBoot 各个版本之间可能存在差异,不过大体流程基本差不多,所以各位看官在实际的工作过程中也。

从一份配置文件开始说起

Spring 的启动过程实际上就是 Ioc 容器初始化以及载入 Bean 的过程;SpringBoot 的启动过程最核心的容器刷新流程也是复用了 Spring 容器刷新的逻辑。在分析 SpringBoot 启动过程之前,我们先来简单回顾下 Spring web 应用基于 tomcat 容器部署的启动过程。这就需要从一个大家都熟悉的配置文件开始说起:

在一般的传统 WEB 项目中,项目的启动一般是从 web.xml 文件的载入开始,如果我们的项目中使用了Spring,那么你肯定会在你的 web.xml 文件中看到上面的配置。Spring 正是通过 ContextLoaderListener 监听器作为容器初始化入口的。

ContextLoaderListener 继承了 ContextLoader 类和 ServletContextListener 接口,并且重写了 ServletContextListener 中的contextInitialized 和 contextDestroyed 方法。在 contextInitialized 中,通过调用父类(ContextLoader)的 initWebApplicationContext 方法进行容器创建:

对于上述 Spring 容器引导刷新大概可以分为两个点来做简单的归纳:

  • 1、通过监听 ServletContextEvent 事件,为 web 容器提供一个全局的 ServletContext 上下文环境,并作为后面 spring 容器的宿主环境
  • 2、在 contextInitialized 方法被调用时,spring 开始初始化一个上下文,这个上下文被称为根上下文,也就是 WebApplicationContext(实际的实现类是 XmlWebApplicationContext )。这个 WebApplicationContext 就是 spring 的 IoC 容器,其对应的 Bean 定义的配置文件由 web.xml 中的 context-param 指定。

关于依赖监听 ServletContextEvent 事件来引导启动的过程大致可以描述为一下过程:

相对于通过监听 ServletContextEvent 事件方式引导刷新 Spring 上下文,SpringBoot 给我的感觉是回归了 java 的本源,即通过 main 方法方式引导启动。由于 SpringBoot 中对于 web 容器也是使用了嵌入式+自动配置的方式,所以在启动入口上差异还是比较大的,当然 SpringBoot 除了支持 fatjar 方式之外,也提供了 war 包方式来保持对原有 Spring 工程的兼容。

本篇文章将承接上一篇《SpringBoot FatJar 启动原理》,来分析下 SpringBoot 的启动过程。希望通过本篇文章,能够让大家了解到与传统基于 servlet 事件引导启动和基于 main 方式启动的不同,从而对 SpringBoot 的整体启动过程有比较清楚的认识。

启动入口

在这篇《SpringBoot系列- FatJar 启动原理》文章中介绍得到,JarLaunch 最后是构建了一个 MainMethodRunner 实例对象,然后通过反射的方式调用了 BootStrap 类中的 main 方法,这里的 ’BootStrap 类中的 main 方法‘ 实际上就是 SpringBoot 的业务入口,也就是常见的下面的代码片段:

从代码可以非常直观的了解到,启动是通过调用 SpringApplication 的静态方法 run;这个 run 方法内部其实是会构造一个 SpringApplication 的实例,然后再调用这里实例的 run 方法来启动 SpringBoot 的。

因此,如果要分析 SpringBoot 的启动过程,我们需要熟悉 SpringApplication 的构造过程以及 SpringApplication 的 run 方法执行过程即可。

SpringApplication 实例的构建

篇幅原因,我们只分析核心的构建流程。

上面代码段中,需要关注两个点:

  • 1、初始化 ApplicationContextInitializer;
  • 2、初始化 ApplicationListener

要注意的是这里的实例化,并非是通过注解和扫包完成,而是通过一种不依赖 Spring 上下文的加载方法;这种做法是为了能够使得在 Spring 完成启动前做各种配置。Spring 的解决方法是以接口的全限定名作为 key,实现类的全限定名作为 value 记录在项目的 META-INF/spring.factories 文件中,然后通过SpringFactoriesLoader 工具类提供静态方法进行类加载并缓存下来,spring.factories 是 SpringBoot 的核心配置文件。SpringFactoriesLoader 可以理解为 Spring 自己提供的一种 spi 扩展实现。SpringBoot 中提供的默认的 spring.factories 配置如下:

关于 SpringFactoriesLoader 如何加载这些资源这里就不过多分析,有兴趣的读者可以自行查看相关源码。

run 方法主流程

SpringApplication 的 run 方法 SpringBoot 进行 Spring 容器刷新的实际入口方法,这个方法中包括了很多 SpringBoot 自己扩展出来的一些特性机制,比如 SpringApplicationRunListener、打印启动 Banner、统一的异常处理扩展等等。下面就直观的看下代码,然后再逐个分析各个流程的具体细节:

上面对代码基本都做了一些详细的注释,有几个需要关注的点:

  • 1、prepareEnvironment 的处理过程
  • 2、prepareContext 的处理过程
  • 3、refreshContext 的处理过程
  • 4、listeners 执行时机及顺序
  • 5、异常处理逻辑

下面就对其他的 4 个点做下详细的分析。

分析启动过程,本质上是对其整个容器生命周期有个了解,包括 listeners 执行各个事件的时机、PostProcessor 执行的时机,Enviroment Ready 的时机等等。掌握这些扩展和时机,可以在实际的业务开发中来做很多事情。

prepareEnvironment 的处理过程

prepareEnvironment 过程相对来说是比较早的,这里主要就是为上下文刷新提供 Environment。

这里面做的事情就是将我们的配置,包括系统配置、application.properties、-D 参数等等统统打包给 environment。在 Spring 中,我们最常见的 xml 中使用的 ${xxx} 或者代码中使用的 @Value(“${xxxx}”) 等,最后都是从 environment 中拿值的。

这里需要关注的一个比较重要的点是发布 ApplicationEnvironmentPreparedEvent 事件,我们可以通过监听这个事件来修改 environment

prepareContext 的处理过程

prepareContext 的处理过程中可以利用的点是非常多的,比如 ApplicationContextInitializer 的执行、ApplicationContextInitializedEvent 和 ApplicationPreparedEvent 事件发布。

ApplicationContextInitializer 是 spring 容器刷新之前初始化 Spring ConfigurableApplicationContext 的回调接口,ApplicationContextInitializer 的 initialize 方法执行之前,context 是还没有刷新的。可以看到在 applyInitializers 之后紧接着发布了 ApplicationContextInitializedEvent 事件。其实这两个点都可以对 context 搞一些事情,ApplicationContextInitializer 更纯粹些,它只关注 context;而 ApplicationContextInitializedEvent 事件源中除了 context 之外,还有 springApplication 对象和参数 args。

prepareContext 最后阶段是发布了 ApplicationPreparedEvent 事件,表示上下文已经准备好了,可以随时执行 refresh 了。

refreshContext 的处理过程

refreshContext 是 Spring 上下文刷新的过程,这里实际调用的是 AbstractApplicationContext 的 refresh 方法;所以 SpringBoot 也是复用了 Spring 上下文刷新的过程。

这个过程涉及到的东西非常多,可扩展的点也非常多,包括 BeanFactoryPostProcessor 处理、BeanPostProcessor 处理、LifecycleProcessor 处理已经 发布 ContextRefreshedEvent 事件等。到这里容器刷新已经完成,容器已经 ready,DI 和 AOP 也已经完成。

BeanFactoryPostProcessor 处理

BeanFactoryPostProcessor 可以对我们的 beanFactory 内所有的 beandefinition(未实例化)数据进行修改,这个过程是在 bean 还没有实例化之前做的。所以在这,我们通过自己去注册一些 beandefinition ,也可以对 beandefinition 做一些修改。关于 BeanFactoryPostProcessor 的用法在很多框架中都有体现,这里以 SOFATracer 中修改 Datasource 为例来说明下。

SOFATracer 中为了对有所基于 jdbc 规范的数据源进行埋点,提供了一个 DataSourceBeanFactoryPostProcessor,用于修改原生 DataSource 来实现一层代理。代码详见:com.alipay.sofa.tracer.boot.datasource.processor.DataSourceBeanFactoryPostProcessor

这里只看核心代码部分,在 postProcessBeanFactory 方法中会根据 Datasource 的类型来创建不同的 DataSourceProxy;创建 DataSourceProxy 的过程就是修改原生 Datasource 的过程。

上面这段代码就是 BeanFactoryPostProcessor 一种典型的应用场景,就是修改 BeanDefinition。

BeanFactoryPostProcessor 处理过程代码比较长,这里就不在具体分析处理的流程。需要关注的点是:1、BeanFactoryPostProcessor 的作用,它能做哪些事情;2、它是在容器启动的哪个阶段执行的。

registerBeanPostProcessors 的处理过程

registerBeanPostProcessors 是用于注册 BeanPostProcessor 的。BeanPostProcessor 的作用时机相对于 BeanFactoryPostProcessor 来说要晚一些,BeanFactoryPostProcessor 处理的是 BeanDefinition,Bean 还没有实例化;BeanPostProcessor 处理的是 Bean,BeanPostProcessor 包括两个方法,分别用于在 Bean 实例化之前和实例化之后回调。

开篇有提到,在某些场景下会出现 BeanPostProcessor 不生效。对于 Spring 来说,BeanPostProcessor 本身也会被注册成一个 Bean,那么自然就可能会出现,BeanPostProcessor 处理的 bean 在 BeanPostProcessor 本身初始化之前就已经完成了的情况。

registerBeanPostProcessors 大体分为以下几个部分:

  • 注册 BeanPostProcessorChecker。(当一个 bean 在 BeanPostProcessor 实例化过程中被创建时,即当一个bean没有资格被所有 BeanPostProcessor 处理时,它记录一个信息消息)
  • 实现优先排序、排序和其他操作的 BeanPostProcessor 之间进行排序
  • 注册实现 PriorityOrdered 的 BeanPostProcessor
  • 注册实现 Ordered 的
  • 注册所有常规的 BeanPostProcessor
  • 重新注册所有的内部 BeanPostProcessor
  • 将后处理器注册为用于检测内部 bean 的 applicationlistener,将其移动到处理器链的末端(用于获取代理等)。

这里还是以扩展时机为主线,Bean 的 IoC、DI 和 AOP 初始化过程不细究。

LifecycleProcessor 的处理过程

LifecycleProcessor 的处理过程是在 finishRefresh 方法中执行,下面先看下 finishRefresh 方法:

初始化 initLifecycleProcessor 是从容器中拿到所有的 LifecycleProcessor ,如果业务代码中没有实现 LifecycleProcessor 接口的 bean ,则使用默认的 DefaultLifecycleProcessor。

onRefresh 过程是 最后会调用到 Lifecycle 接口的 start 方法。LifeCycle 定义 Spring 容器对象的生命周期,任何 spring 管理对象都可以实现该接口。然后,当 ApplicationContext 本身接收启动和停止信号(例如在运行时停止/重启场景)时,spring 容器将在容器上下文中找出所有实现了 LifeCycle 及其子类接口的类,并一一调用它们实现的类。spring 是通过委托给生命周期处理器 LifecycleProcessor 来实现这一点的。Lifecycle 接口定义如下:

至此,容器刷新其实已经就完成了。可以看到 Spring 或者 SpringBoot 在整个启动过程中,有非常多的口子暴露出来,供用户使用,非常灵活。

异常处理逻辑

与正常流程类似,异常处理流程同样作为 SpringBoot 生命周期的一个环节,在异常发生时,会通过一些机制来处理收尾过程。异常处理部分 SpringBoot 1.x 版本和 SpringBoot 2.x 版本差异还是比较大的。这里只分析 SpringBoot 2.x 的处理过程。这里直接贴一段代码:

上述代码片段主要做了以下几件事:

  • handleExitCode:这里会拿到异常的 exitCode,随后发布一个 ExitCodeEvent 事件,最后交由 SpringBootExceptionHandler 处理。
  • SpringApplicationRunListeners#failed:循环遍历调用所有 SpringApplicationRunListener 的 failed 方法
  • reportFailure:用户可以自定义扩展 SpringBootExceptionReporter 接口来实现定制化的异常上报逻辑

在 SpringApplicationRunListeners#failed 中,业务产生的异常将直接被抛出,而不会影响异常处理的主流程。

总结

至此,SpringBoot 启动的主流程已经全部分析完成了。从扩展和扩展时机的角度来看,整个过程中,SpringBoot 提供了非常多的扩展口子,让用户可以在容器启动的各个阶段(无论是启动,环境准备,容器刷新等等)做一些定制化的操作。用户可以利用这些扩展接口来修改 bean 、修改环境变量,给用户极大的空间。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 安徽开发者圈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档