前言
各位小伙伴大家好。本文属总结性文章,对纵览Spring Boot生命周期很是重要,建议点在看、转发“造福”更多小伙伴。
我最近不是在写Spring Cloud深度剖析的相关专栏么,最近有收到小伙伴发过来一些问题,通过这段时间收集到的反馈,总结了一下有一个问题非常集中:那便是对Spring Boot应用SpringApplication的生命周期、事件的理解。有句话我不是经常挂嘴边说的麽,你对Spring Framework有多了解决定了你对Spring Boot有多了解,你对Spring Boot的了解深度又会制约你去了解Spring Cloud,一环扣一环。因此此问题反馈比较集中是在情理之中的~
为何在Spring Boot中生命周期事件机制如此重要?缘由很简单:Spring Cloud父容器是由该生命周期事件机制来驱动的,而它仅仅是一个典型代表。Spring Cloud构建在Spring Boot之上,它在此基础上构建并添加了一些“Cloud”功能。应用程序事件ApplicationEvent以及监听ApplicationListener是Spring Framework提供的扩展点,Spring Boot对此扩展点利用得非常充分和深入,并且还衍生出了非常多“子”事件类型,甚至自成体系。从ApplicationEvent衍生出来的子事件类型非常多,例如JobExecutionEvent、RSocketServerInitializedEvent、AuditApplicationEvent…
本文并不会对每个子事件分别介绍(也并无必要),而是集中火力主攻Spring Boot最为重要的一套事件机制:SpringApplication生命周期的事件体系。
学习去…
本文将以SpringApplication的启动流程/生命周期各时期发出的Event事件为主线,结合每个生命周期内完成的大事记介绍,真正实现一文让你总览Spring Boot的全貌,这对你深入理解Spring Boot,以及整合进Spring Cloud都将非常重要。
为表诚意,本文一开始便把SpringApplication生命周期事件流程图附上,然后再精细化讲解各个事件的详情。
话外音:赶时间的小伙伴可以拿图走人,但不建议白嫖哟
由于不同版本、类路径下存在不同包时结果会存在差异,不指明版本的文章都是不够负责任的。因此对导包/版本情况作出如下说明:
总的来说:本例导包是非常非常“干净”的,这样在流程上才更有说服力嘛~
它是和SpringApplication生命周期有关的所有事件的父类,@since 1.0.0。
public abstract class SpringApplicationEvent extends ApplicationEvent {
private final String[] args;
public SpringApplicationEvent(SpringApplication application, String[] args) {
super(application);
this.args = args;
}
public SpringApplication getSpringApplication() {
return (SpringApplication) getSource();
}
public final String[] getArgs() {
return this.args;
}
}
它是抽象类,扩展自Spring Framwork的ApplicationEvent,确保了事件和应用实体SpringApplication产生关联(当然还有String[] args)。它有如下实现子类(7个):
每个事件都代表着SpringApplication不同生命周期所处的位置,下面分别进行讲解。
@since 1.5.0,并非1.0.0就有的哦。不过现在几乎没有人用1.5以下的版本了,所以可当它是标准事件。
默认情况下,有4个监听器监听ApplicationStartingEvent事件:
总结:此事件节点结束时,SpringApplication完成了一些实例化相关的动作:本实例实例化、本实例属性赋值、日志系统实例化等。
@since 1.0.0。该事件节点是最为重要的一个节点之一,因为对于Spring应用来说,环境抽象Enviroment简直太重要了,它是最为基础的元数据,决定着程序的构建和走向,所以构建的时机是比较早的。
封装命令行参数(main方法的args)到ApplicationArguments里面
创建出一个环境抽象实例ConfigurableEnvironment的实现类,并且填入值:Profiles配置和Properties属性,默认内容如下(注意,这只是初始状态,后面还会改变、添加属性源,实际见最后的截图):
发送ApplicationEnvironmentPreparedEvent事件,触发对应的监听器的执行对环境抽象Enviroment的填值,均是由监听此事件的监听器去完成,见下面的监听器详解
bindToSpringApplication(environment):把环境属性中spring.main.xxx = xxx绑定到当前的SpringApplication实例属性上,如常用的spring.main.allow-bean-definition-overriding=true会被绑定到当前SpringApplication实例的对应属性上
默认情况下,有9个监听器监听ApplicationEnvironmentPreparedEvent事件:
总结:此事件节点结束时,Spring Boot的环境抽象Enviroment已经准备完毕,但此时其上下文ApplicationContext还没有创建,但是Spring Cloud的应用上下文(引导上下文)已经全部初始化完毕哦,所以SC管理的外部化配置也应该都进入到了SB里面。如下图所示(这是基本上算是Enviroment的最终态了):
小提示:SC配置的优先级是高于SB管理的外部化配置的。例如针对spring.application.name这个属性,若bootstrap里已配置了值,再在application.yaml里配置其实就无效了,因此生产商建议不要写两处。
@since 2.1.0,非常新的一个事件。当SpringApplication的上下文ApplicationContext准备好后,对单例Bean们实例化之前,发送此事件。所以此事件又可称为:contextPrepared事件。
printBanner(environment):打印Banner图,默认打印的是Spring Boot字样spring.main.banner-mode = xxx来控制Banner的输出,可选值为CONSOLE/LOG/OFF,一般默认就好默认在类路径下放置一个banner.txt文件,可实现自定义Banner。关于更多自定义方式,如使用图片、gif等,本处不做过多介绍小建议:别花里胡哨搞个佛祖在那。让它能自动打印输出当前应用名,这样才是最为实用,最高级的(但需要你定制化开发,并且支持可配置,最好对使用者无感,属于一个common组件)
根据是否是web环境、是否是REACTIVE等,用空构造器创建出一个ConfigurableApplicationContext上下文实例(因为使用的是空构造器,所以不会立马“启动”上下文)SERVLET -> AnnotationConfigServletWebServerApplicationContextREACTIVE -> AnnotationConfigReactiveWebServerApplicationContext非web环境 -> AnnotationConfigApplicationContext(SC应用的容器就是使用的它)
既然上下文实例已经有了,那么就开始对它进行一些参数的设置:首先最重要的便是把已经准备好的环境Enviroment环境设置给它设置些beanNameGenerator、resourceLoader、ConversionService等组件实例化所有的ApplicationContextInitializer上下文初始化器,并且排序好后挨个执行它(这个很重要),默认有如下截图这些初始化器此时要执行:
下面对这些初始化器分别做出简单介绍:BootstrapApplicationListener.AncestorInitializer:来自SC。用于把SC容器设置为SB容器的父容器。当然实际操作委托给了此方法:new ParentContextApplicationContextInitializer(this.parent).initialize(context)去完成BootstrapApplicationListener.DelegatingEnvironmentDecryptApplicationInitializer:来自SC。代理了下面会提到的EnvironmentDecryptApplicationInitializer,也就是说在此处就会先执行,用于提前解密Enviroment环境里面的属性,如相关URL等PropertySourceBootstrapConfiguration:来自SC。重要,和配置中心相关,若想自定义配置中心必须了解它。主要作用是PropertySourceLocator属性源定位器,我会把它放在配置中心章节详解EnvironmentDecryptApplicationInitializer:来自SC。属性源头通过上面加载回来了,通过它来实现解密值得注意的是:它被执行了两次哦~DelegatingApplicationContextInitializer:和上面的DelegatingApplicationListener功能类似,支持外部化配置context.initializer.classes = xxx,xxxSharedMetadataReaderFactoryContextInitializer:略ContextIdApplicationContextInitializer:@since 1.0.0。设置应用ID -> applicationContext.setId()。默认取值为spring.application.name,再为application,再为自动生成ConfigurationWarningsApplicationContextInitializer:@since 1.2.0。对错误的配置进行警告(不会终止程序),以warn()日志输出在控制台。默认内置的只有对包名的检查:若你扫包含有"org.springframework"/"org"这种包名就警告若你想自定义检查规则,请实现Check接口,然后…RSocketPortInfoApplicationContextInitializer:@since 2.2.0。暂略ServerPortInfoApplicationContextInitializer:@since 2.0.0。将自己作为一个监听器注册到上下文ConfigurableApplicationContext里,专门用于监听WebServerInitializedEvent事件(非SpringApplication的生命周期事件)该事件有两个实现类:ServletWebServerInitializedEvent和ReactiveWebServerInitializedEvent。发送此事件的时机是WebServer已启动完成,所以已经有了监听的端口号该监听器做的事有两个:“local.” + getName(context.getServerNamespace()) + ".port"作为key(默认值是local.server.port),value是端口值。这样可以通过@Value来获取到本机端口了(但貌似端口写0的时候,SB在显示上有个小bug)作为一个属性源MapPropertySource放进环境里,属性源名称为:server.ports(因为一个server是可以监听多个端口的,所以这里用复数)ConditionEvaluationReportLoggingListener:将ConditionEvaluationReport报告(自动配置中哪些匹配了,哪些没匹配上)写入日志,当然只有LogLevel#DEBUG时才会输出(注意:这不是日志级别哦,应该叫报告级别)。如你配置debug=true就开启了此自动配置类报告槽点:它明明是个初始化器,为毛命名为Listener?
发送ApplicationContextInitializedEvent事件,触发对应的监听器的执行
默认情况下,有2个监听器监听ApplicationContextInitializedEvent事件:
总结:此事件节点结束时,完成了应用上下文ApplicationContext的准备工作,并且执行所有注册的上下文初始化器ApplicationContextInitializer。但是此时,单例Bean是仍旧还没有初始化,并且WebServer也还没有启动
@since 1.0.0。截止到上个事件ApplicationContextInitializedEvent,应用上下文ApplicationContext充其量叫实例化好了,但是还剩下很重要的事没做,这便是本周期的内容。
默认情况下,有6个监听器监听ApplicationContextInitializedEvent事件:
总结:此事件节点结束时,应用上下文ApplicationContext初始化完成,该赋值的赋值了,Bean定义信息也已全部加载完成。但是,单例Bean还没有被实例化,web容器依旧还没启动。
@since 2.0.0。截止到此,应用已经准备就绪,并且通过监听器、初始化器等完成了非常多的工作了,但仍旧剩下被认为最为重要的初始化单例Bean动作还没做、web容器(如Tomcat)还没启动,这便是这个周期所要做的事。
启动Spring容器:AbstractApplicationContext#refresh(),这个步骤会做很多事,比如会实例化单例Bean该步骤属于Spring Framework的核心内容范畴,做了很多事,请参考Spring核心技术内容章节在Spring容器refresh()启动完成后,WebServer也随之完成启动,成功监听到对应端口(们)
输出启动成功的日志:Started Application in xxx seconds (JVM running for xxx)
发送ApplicationStartedEvent事件,触发对应的监听器的执行
callRunners():依次执行容器内配置的ApplicationRunner/CommandLineRunner的Bean实现类,支持sort排序ApplicationRunner:@since 1.3.0,入参是ApplicationArguments,先执行(推荐使用)CommandLineRunner:@since 1.0.0,入参是String… args,后执行(不推荐使用)
默认情况下,有3个监听器监听ApplicationStartedEvent事件:
总结:此事件节点结束时,SpringApplication的生命周期到这一步,正常的启动流程就全部完成了。也就说Spring Boot应用可以正常对对外提供服务了。
@since 1.3.0。该事件所处的生命周期可认为基本同ApplicationStartedEvent,仅是在其后执行而已,两者中间并无其它特别的动作,但是监听此事件的监听器们还是蛮重要的。
同上。
默认情况下,有4个监听器监听ApplicationStartedEvent事件:
总结:此事件节点结束时,应用已经完完全全的准备好了,并且也已经完成了相关组件的周知工作。
SpringApplication是有可能在启动的时候失败(如端口号已被占用),当然任何一步骤遇到异常时交给SpringApplication#handleRunFailure()方法来处理,这时候也会有对应的事件发出。
当SpringApplication在启动时抛出异常:可能是端口绑定、也可能是你自定义的监听器你写了个bug等,就会“可能”发生此事件。
默认情况下,有6个监听器监听ApplicationStartedEvent事件:
总结:此事件节点结束时,会做一些释放资源的操作。一般情况下:我们并不需要监听到此事件
关于SpringApplication的生命周期体系的介绍就到这了,相信通过此“万字长文”你能体会到A哥的用心。翻了翻市面上的相关文章,本文Almost可以保证是总结得最到位的,让你通过一文便可从大的方面基本掌握Spring Boot,这不管是你使用SB,还是后续自行扩展、精雕细琢SB,以及去深入了解Spring Cloud均有非常重要的意义,希望对你有帮助,谢谢你的三连。