主要讲解Spring Boost的执行流程。(建议大家先看《【Spring Boot系列1】一文带你了解Spring Boot(上)》,然后再看本篇内容。)
SpringApplication.run执行流程
执行流程
我们看到Spring Boot的启动类:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
当执行run()方法时,该方法的主要流程大体可以归纳如下:
- 如果我们使用的是 SpringApplication 的静态 run 方法,那么,这个方法里面首先需要创建一个 SpringApplication 对象实例,然后调用这个创建好的 SpringApplication 的实例 run方 法。在 SpringApplication 实例初始化的时候,它会提前做几件事情:
- 根据 classpath 里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为 Web 应用使用的 ApplicationContext 类型,还是应该创建一个标准 Standalone 应用使用的 ApplicationContext 类型。
- 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationContextInitializer。
- 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationListener。
- 推断并设置 main 方法的定义类。
- SpringApplication 实例初始化完成并且完成设置后,就开始执行 run 方法的逻辑了,方法执行开始,首先遍历执行所有通过 SpringFactoriesLoader 可以查找到并加载的 SpringApplicationRunListener,调用它们的 started() 方法,告诉这些 SpringApplicationRunListener,“嘿,SpringBoot 应用要开始执行咯!”。
- 创建并配置当前 SpringBoot 应用将要使用的 Environment(包括配置要使用的 PropertySource 以及 Profile)。
- 遍历调用所有 SpringApplicationRunListener 的 environmentPrepared()的方法,告诉它们:“当前 SpringBoot 应用使用的 Environment 准备好咯!”。
- 如果 SpringApplication的showBanner 属性被设置为 true,则打印 banner(SpringBoot 1.3.x版本,这里应该是基于 Banner.Mode 决定 banner 的打印行为)。这一步的逻辑其实可以不关心,我认为唯一的用途就是“好玩”(Just For Fun)。
- 根据用户是否明确设置了applicationContextClass 类型以及初始化阶段的推断结果,决定该为当前 SpringBoot 应用创建什么类型的 ApplicationContext 并创建完成,然后根据条件决定是否添加 ShutdownHook,决定是否使用自定义的 BeanNameGenerator,决定是否使用自定义的 ResourceLoader,当然,最重要的,将之前准备好的 Environment 设置给创建好的 ApplicationContext 使用。
- ApplicationContext 创建好之后,SpringApplication 会再次借助 Spring-FactoriesLoader,查找并加载 classpath 中所有可用的 ApplicationContext-Initializer,然后遍历调用这些 ApplicationContextInitializer 的 initialize(applicationContext)方法来对已经创建好的 ApplicationContext 进行进一步的处理。
- 遍历调用所有 SpringApplicationRunListener 的 contextPrepared()方法,通知它们:“SpringBoot 应用使用的 ApplicationContext 准备好啦!”
- 最核心的一步,将之前通过 @EnableAutoConfiguration 获取的所有配置以及其他形式的 IoC 容器配置加载到已经准备完毕的 ApplicationContext。
- 遍历调用所有 SpringApplicationRunListener 的 contextLoaded() 方法,告知所有 SpringApplicationRunListener,ApplicationContext "装填完毕"!
- 调用 ApplicationContext 的 refresh() 方法,完成 IoC 容器可用的最后一道工序。
- 查找当前 ApplicationContext 中是否注册有 CommandLineRunner,如果有,则遍历执行它们。
- 正常情况下,遍历执行 SpringApplicationRunListener 的 finished() 方法,告知它们:“搞定!”。(如果整个过程出现异常,则依然调用所有 SpringApplicationRunListener 的 finished() 方法,只不过这种情况下会将异常信息一并传入处理)。
至此,一个完整的 SpringBoot 应用启动完毕!
整个过程看起来冗长无比,但其实很多都是一些事件通知的扩展点,如果我们将这些逻辑暂时忽略,那么,其实整个 SpringBoot 应用启动的逻辑就可以压缩到极其精简的几步:
个人理解:
通过框架扫描出所有的ApplicationListener,然后通过主流程执行以下操作:start->创建并准备Environment->通过Environment创建上下文Context->initialize上下文Context->将Bean注入Spring->通过refresh启动程序->finish。
对于主流程执行的每一步,都会去遍历所有的ApplicationListener,然后告诉他们每一步流程执行到哪里。我理解其实就是通过ApplicationListener提供的钩子,然后主程序通过这些钩子告诉每个Application需要执行哪一步,然后完成所有Application的初始化和启动工作。
前后对比我们就可以发现,其实 SpringApplication 提供的这些各类扩展点近乎“喧宾夺主”,占据了一个 Spring 应用启动逻辑的大部分“江山”,除了初始化并准备好 ApplicationContext,剩下的大部分工作都是通过这些扩展点完成的,所以,我们接下来对各类扩展点进行逐一剖析。
SpringApplicationRunListener
SpringApplicationRunListener 是一个只有 SpringBoot 应用的 main 方法执行过程中接收不同执行时点事件通知的监听者:
public interface SpringApplicationRunListener { void started(); void environmentPrepared(ConfigurableEnvironment environment); void contextPrepared(ConfigurableApplicationContext context); void contextLoaded(ConfigurableApplicationContext context); void finished(ConfigurableApplicationContext context, Throwable exception); }
对于我们来说,基本没什么常见的场景需要自己实现一个 Spring-ApplicationRunListener,假设我们真的有场景需要自定义一个 SpringApplicationRunListener 实现,可以通过下述方式:
public class DemoSpringApplicationRunListener implements SpringApplicationRunListener { @Override public void started() { // do whatever you want to do } @Override public void environmentPrepared(ConfigurableEnvironment environment) { // do whatever you want to do } @Override public void contextPrepared(ConfigurableApplicationContext context) { // do whatever you want to do } @Override public void contextLoaded(ConfigurableApplicationContext context) { // do whatever you want to do } @Override public void finished(ConfigurableApplicationContext context, Throwable exception) { // do whatever you want to do } }
之后,我们可以通过 SpringFactoriesLoader 立下的规矩,在当前 SpringBoot 应用的 classpath 下的 META-INF/spring.factories 文件中进行类似如下的配置:
org.springframework.boot.SpringApplicationRunListener=\com.keevol.springboot.demo.DemoSpringApplicationRunListener
然后 SpringApplication 就会在运行的时候调用它啦!
这里就是定义一个自己的SpringApplicationRunListener,只需要实现里面的钩子,剩下的框架可以帮我们做所有的事情。
ApplicationContextInitializer
ApplicationContextInitializer 也是 Spring 框架原有的概念,这个类的主要目的就是在 ConfigurableApplicationContext 类型(或者子类型)的 ApplicationContext 做 refresh 之前,允许我们对 ConfigurableApplicationContext 的实例做进一步的设置或者处理。
实现一个 ApplicationContextInitializer 很简单,因为它只有一个方法需要实现:
public class DemoApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext applicationContext) { // do whatever you want with applicationContext, // e.g. applicationContext.registerShutdownHook(); } }
不过,一般情况下我们基本不会需要自定义一个 ApplicationContext-Initializer,即使 SpringBoot 框架默认也只是注册了三个实现:
org.springframework.context.ApplicationContextInitializer= \org.springframework.boot.context.ConfigurationWarningsApplication-ContextInitializer, \org.springframework.boot.context.ContextIdApplicationContextInitia-lizer, \org.springframework.boot.context.config.DelegatingApplicationContex-tInitializer
如果我们真的需要自定义一个 ApplicationContextInitializer,那么只要像上面这样,通过 SpringFactoriesLoader 机制进行配置,或者通过 SpringApplication.addInitializers(..)设置即可。
这个其实就是准备上下文,为应用启动作准备,我们可以定义自己的ApplicationContextInitializer,不过一般不会自己定义,用系统默认的即可。
ApplicationContextInitializer
CommandLineRunner 是很好的扩展接口,不是 Spring 框架原有的“宝贝”,它属于 SpringBoot 应用特定的回调扩展接口。源码如下所示:
public interface CommandLineRunner { void run(String... args) throws Exception; }
CommandLineRunner 需要大家关注的其实就两点:
- 所有 CommandLineRunner 的执行时点在 SpringBoot 应用的 Application-Context 完全初始化开始工作之后(可以认为是 main 方法执行完成之前最后一步)。
- 只要存在于当前 SpringBoot 应用的 ApplicationContext 中的任何 Command-LineRunner,都会被加载执行(不管你是手动注册这个 CommandLineRunner 到 IoC 容器,还是自动扫描进去的)。
与其他几个扩展点接口类型相似,建议 CommandLineRunner 的实现类使用 @org.springframework.core.annotation.Order 进行标注或者实现 org.springframework.core.Ordered 接口,便于对它们的执行顺序进行调整,这其实十分重要,我们不希望顺序不当的 CommandLineRunner 实现类阻塞了后面其他 CommandLineRunner 的执行。
这里也就记录一下,其实没有看懂有啥用~~衰!
SpringApplication.run源码分析
上面讲述了Spring Boot的执行流程,我们再从源码的角度,结合上面的讲解,再看看。项目启动的入口就不再贴代码了,直接进入主流程。
SpringApplication实例创建
public SpringApplication(Class<?>... primarySources) { this(null, primarySources); } @SuppressWarnings({ "unchecked", "rawtypes" }) public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.sources = new LinkedHashSet(); // Banner 模式 this.bannerMode = Mode.CONSOLE; this.logStartupInfo = true; // 是否添加 JVM 启动参数 this.addCommandLineProperties = true; this.addConversionService = true; this.headless = true; this.registerShutdownHook = true; this.additionalProfiles = new HashSet(); this.isCustomEnvironment = false; // 资源加载器 this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); //项目启动类 SpringbootDemoApplication.class设置为属性存储起来 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //设置应用类型是SERVLET应用(Spring 5之前的传统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用) // deduceFromClasspath()方法用于查看 ClassPath类路径下是否存在某个特征类,从而判断webApplicationType this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 设置初始化器(Initializer),最后会调用这些初始化器 //所谓的初始化器就是org.springframework.context.ApplicationContextInitializer的实现类,在Spring上下文被刷新之前进行初始化的操作 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 设置监听器(Listener) setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 初始化 mainApplicationClass 属性:用于推断并设置项目main()方法启动的主程序启动类 this.mainApplicationClass = deduceMainApplicationClass(); }
项目初始化启动
public ConfigurableApplicationContext run(String... args) { // 创建 StopWatch 对象,并启动。StopWatch 主要用于简单统计 run 启动过程的时长。 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 初始化应用上下文和异常报告集合 ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 配置 headless 属性 configureHeadlessProperty(); //(1)获取并启动监听器 SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { // 创建 ApplicationArguments 对象 初始化默认应用参数类 // args是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问。如:--server.port=9000 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); //(2)项目运行环境Environment的预配置 // 创建并配置当前SpringBoot应用将要使用的Environment // 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // 排除不需要的运行环境 configureIgnoreBeanInfo(environment); // 准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体 Banner printedBanner = printBanner(environment); // (3)创建Spring容器 // 根据 webApplicationType 进行判断,确定容器类型,如果为 SERVLET 类型,会反射创建相应的字节码,AnnotationConfigServletWebServerApplicationContext, 接着使用之前传世话设置的context、environment、listeners、applicationArgument 进行应用上下文的组装配置 context = createApplicationContext(); // 获得异常报告器 SpringBootExceptionReporter 数组 //这一步的逻辑和实例化初始化器和监听器的一样, // 都是通过调用 getSpringFactoriesInstances 方法来获取配置的异常类名称并实例化所有的异常处理类。 exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // (4)Spring容器前置处理 //这一步主要是在容器刷新之前的准备动作。包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础。 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // (5):刷新容器 // 开启(刷新)Spring 容器,通过refresh方法对整个IoC容器的初始化(包括Bean资源的定位、解析、注册等等),同时向JVM运行时注册一个关机钩子,在JVM关机时关闭这个上下文,除非它当时已经关闭 refreshContext(context); // (6):Spring容器后置处理 //扩展接口,设计模式中的模板方法,默认为空实现。 // 如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理 afterRefresh(context, applicationArguments); // 停止 StopWatch 统计时长 stopWatch.stop(); // 打印 Spring Boot 启动的时长日志。 if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // (7)发出结束执行的事件通知 listeners.started(context); // (8):执行Runners //用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序 //Runner 运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务启动后执行一次。 //Spring Boot提供了ApplicationRunner和CommandLineRunner两种服务接口 callRunners(context, applicationArguments); } catch (Throwable ex) { // 如果发生异常,则进行处理,并抛出 IllegalStateException 异常 handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } // (9)发布应用上下文就绪事件 //表示在前面一切初始化启动都没有问题的情况下,使用运行监听器SpringApplicationRunListener持续运行配置好的应用上下文ApplicationContext, // 这样整个Spring Boot项目就正式启动完成了。 try { listeners.running(context); } catch (Throwable ex) { // 如果发生异常,则进行处理,并抛出 IllegalStateException 异常 handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } //返回容器 return context; }
执行流程图
其实整个启动流程,主要分2大块:
后记
这篇文章其实只讲述了SpringBoot的执行流程,如果只是纯粹使用SpringBoot,也可以不用去了解这些,因为只需要知道怎么用就可以,但是学习一门技术,还是需要多去深究一下,不能仅仅停留在会使用的基础上。
当然,这些知识也不是我总结的,我也是处于好奇,然后就从网上找到的相关资料,然后加入一些自己的理解,反正初步学习完后,感觉心里是有些底了,不至于一直是雾里探花的感觉。其实本篇文章对SpringBoot执行流程的分析是比较浅显的,网上也有很多详细的源码分析资料,感兴趣的同学可以去Debug,然后再仔细研究。