如何找到并分析学习SpringBoot启动过程

1 缘起

这是一篇被耽误很久的文章,
从开始写,一直到梳理完成,经过了好几个月,
其中,发生了很多事情,
亲身经历了课程学习、**优化等事情,
经过苦恼之后,逐渐趋于平静,开始耐心研究,并用心写文章,
随着学习的深入,发现我也可以学懂一些知识的,
关于SpringBoot的启动,我参考过别人的文章,
看过小马哥的课程,
但是,始终不能懂得,还是修行不够,
现在终于有那么一点理解,赶紧记录一下。
本篇文章比较长,可以先浏览,看一下第3章的流程图以及第5章的小结,
了解全流程。

声明:

基于:SpringBoot 2.4.5

2 启动主函数

package com.monkey.tutorial;

import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@EnableCaching
@EnableFeignClients
@ServletComponentScan
public class TutorialApplication {

	private static final Logger logger = LoggerFactory.getLogger(TutorialApplication.class);

	public static void main(String[] args) {
		SpringApplication.run(TutorialApplication.class, args);
		logger.info("Tutorial 成功启动");
	}

	@Bean
	MeterRegistryCustomizer<MeterRegistry> prometheusConfiguration() {
		return (registry) -> registry.config().commonTags("application", "tutorial");
	}

}

3 如何找到SpringBoot启动过程?

顺藤摸瓜,通过上面的主函数一步一步找到SpringBoot的启动流程。
整体流程如下图所示。
在这里插入图片描述

3.1 SpringApplication.run(TutorialApplication.class, args)

位置:org.springframework.boot.SpringApplication#run(java.lang.Class<?>, java.lang.String…)
第一层run方法,源码如下图。
根据注释,该方法为静态帮手(Static helper),用于执行指定默认配置资源的SpringApplication,当然,这个资源即主类:TutorialApplication.class,主类上标注了多个注解,相当于添加配置。
在这里插入图片描述

比如添加的注解@SpringBootApplication,源码如下图,
用于配置自动装配、包扫描等功能。
在这里插入图片描述

3.2 run(new Class<?>[] { primarySource }, args)

位置:org.springframework.boot.SpringApplication#run(java.lang.Class<?>[], java.lang.String[])
第二层方法,与第一层一致,是具体的实现,源码如下图。
在这里插入图片描述

3.3 SpringApplication(primarySources).run(args)

位置:org.springframework.boot.SpringApplication#run(java.lang.String…)
第三层,run方法,
好了,到这里就是SpringBoot的启动流程了,
该方法包含了SpringBoot启动的全部流程,源码如下,
通过,注释可知,该方法:
执行Spring应用,创建并刷新ApplicationContext,返回运行中的ApplicationContext,
下面会逐条讲解每一步(基于我能读懂的)。

	/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

4 启动过程

我在这里从源码角度讲一下流程,覆盖所有步骤,
但是,由于每个步骤中,都会涉及较多的步骤,
这里挑一些核心,步骤讲解,后续可能会针对每个步骤单独详细讲解。
从org.springframework.boot.SpringApplication#run(java.lang.String…)一步一步挖掘。

4.1 计时

用于记录SpringBoot的启动时间。
StopWatch stopWatch = new StopWatch();
位置:org.springframework.util.StopWatch

4.1.1 计时开始:start

开始:开始记录一个任务,
通过this.startTimeNanos = System.nanoTime()获取其实时间,
源码如下图所示。
在这里插入图片描述

4.1.2 计时结束:stop

停止:获取事件运行的总时间,
当前事件运行的时间:lastTime=System.nanoTime() - this.startTimeNanos;
总时间:this.totalTimeNanos += lastTime;
在这里插入图片描述

4.2 创建BootstrapContext

DefaultBootstrapContext bootstrapContext = createBootstrapContext();
位置:org.springframework.boot.SpringApplication#createBootstrapContext
使用DefaultBootstrapContext初始化bootstrapRegistryInitializers,
主要用户事件监听,因为DefaultBootstrapContext中的一个重要属性为SimpleApplicationEventMuliticater,
源码如下:
在这里插入图片描述
接下来再看下DefaultBootstrapContext是什么?
两个HashMap属性和一个事件监听器SimpleApplicationEventMulticaster,
由此可知,DefaultBootstrapContext,主要用于事件监听,
创建后,他存储的内容是什么?
请往下看。

在这里插入图片描述
进入调试阶段,过程如下图所示,
看到DefaultBootstrapContext中的属性,只有instanceSupplier存储了4个对象,
事件事件监听器events中defaultRetriever存储了两个对象。

在这里插入图片描述
events的defaultRetriever如下图所示,
applicationListeners已经填充了数据。
在这里插入图片描述

4.3 配置Headless属性

configureHeadlessProperty()
位置:org.springframework.boot.SpringApplication#configureHeadlessProperty
源码如下图所示,其中,SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = “java.awt.headless”;
将headless配置为true,
告程序统这是没有显示设备、键盘或鼠标的系统,
想要使用这些外设提供功能,需要在程序中模拟,
提供给外部客户端使用。

在这里插入图片描述

4.4 获取监听器

SpringApplicationRunListeners listeners = getRunListeners(args);
位置:org.springframework.boot.SpringApplication#getRunListeners
获取当前对象创建的监听器,目前的监听器是由DefaultBootstrapContext创建的SimpleApplicationEventMulticaster。
源码及调试结果如下图所示。

在这里插入图片描述

4.5 添加spring.boot.application.starting

listeners.starting(bootstrapContext, this.mainApplicationClass);
位置:org.springframework.boot.SpringApplicationRunListeners#starting
添加监测标识spring.boot.application.starting,标识mainApplicationClass,
源码如下图所示。
在这里插入图片描述

4.6 获取启动参数

位置:ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
从主类入口获取启动参数,
源码如下图所示。
在这里插入图片描述

4.7 环境准备

ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
位置:org.springframework.boot.SpringApplication#prepareEnvironment
准备SpringBoot需要加载的配置文件路径,
以便从相应的路径中加载资源,主要是MutablePropertySource和PropertySource加载资源,
源码如下图所示。
在这里插入图片描述
其中,最核心的是加载外部配置的激活文件,
调试记录如下图所示,
当前加载的激活文件为application-dev.yml。
在这里插入图片描述
propertySource存储加载的资源,
如下图所示,最熟悉的莫过于application.yml和application-dev.yml。
在这里插入图片描述

4.8 过滤需要忽略的Bean

configureIgnoreBeanInfo(environment);
位置:org.springframework.boot.SpringApplication#configureIgnoreBeanInfo
按照这个属性的注释,可知,该步是为了过滤Bean,配置spring.beaninfo.ignore为true时,
跳过查找BeanInfo,注释如下图。
在这里插入图片描述

源码如下图所示。
在这里插入图片描述

4.9 打印Banner

Banner printedBanner = printBanner(environment);
位置:org.springframework.boot.SpringApplication#printBanner
通过ResourceLoader加载Banner文件,并打印在控制台,
源码如下图所示。

在这里插入图片描述
这里直接给结果,加载的默认Banner为:

在这里插入图片描述
在这里插入图片描述
效果是这样:
在这里插入图片描述

4.10 创建ApplicationContext

ConfigurableApplicationContext context = null;
context = createApplicationContext();

位置:org.springframework.boot.SpringApplication#createApplicationContext
实例化AnnotatedBeanDefinitionReader、ClassPathBeanDefinitionScanner和DefaultListableBeanFactory,
为后续添加BeanDefinition做准备,
其中,
AnnotatedBeanDefinitionReader是可编程的Bean类注册适配器;
ClassPathBeanDefinitionScanner通过classpath获取扫描BeanDefinition;
DefaultListableBeanFactory即默认的Bean工厂,存储Spring中的Bean。
源码如下图所示。

在这里插入图片描述
AnnotatedBeanDefinitionReader对应reader,其中BeanName为空,
如下图所示。
在这里插入图片描述
ClassPathBeanDefinitionScanner对应scanner,BeanName为空,
如下图所示。
在这里插入图片描述

4.11 设置startup

context.setApplicationStartup(this.applicationStartup);

位置:org.springframework.context.ConfigurableApplicationContext#setApplicationStartup
为应用上下文添加启动期间的观测数据,
源码如下图所示。

在这里插入图片描述

4.12 准备应用上下文

prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

位置:org.springframework.boot.SpringApplication#prepareContext
这里,主要是初始化应用上下文,
初始化AnnotatedBeanDefinitionReader、ClassPathBeanDefinitionScanner和DefaultListableBeanFactory,
其中,
AnnotatedBeanDefinitionReader是可编程的Bean类注册适配器;
ClassPathBeanDefinitionScanner通过classpath获取扫描BeanDefinition;
DefaultListableBeanFactory即默认的Bean工厂,存储Spring中的Bean。
源码如下所示。

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
		applyInitializers(context);
		listeners.contextPrepared(context);
		bootstrapContext.close(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);
	}

AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner来自调试信息,
AnnotatedBeanDefinitionReader调试信息如下图所示,
有4个默认的注解BeanDefinition,分别为:EnableFeignClients、SpringBootApplication、EnableCaching和ServletComponentScan,
通过第2章的启动主函数可知,这4个注解BeanDefination均来自类注解。
在这里插入图片描述
ClassPathBeanDefinitionScanner中的BeanDefinition同样有4个,来源与AnnotatedBeanDefinitionReader一致,
调试信息如下图所示。
在这里插入图片描述
初始化阶段的DefaultListableBeanFactory只有6个,
调试信息如下图所示。
在这里插入图片描述

4.13 刷新应用上下文

refreshContext(context);

位置:org.springframework.boot.SpringApplication#refreshContext
这是最重要的一个步骤,
这一步了不得,
加载SpringBoot的Bean(如Service、RestController等)、自定义的Bean、服务器(如Tomcat、Undertow、Netty等)全靠这一步,
源码如下图,这只是入口代码。

在这里插入图片描述

AnnotatedBeanDefinitionReader中的BeanName由4个增加为24个。
在这里插入图片描述

同样,ClassPathBeanDefinitionScanner中BeanName由4个增加为24个。
在这里插入图片描述
服务器选择了Tomcat,如下图所示。
在这里插入图片描述
重头戏是BeanFactory,这里由6个,增加为925个,
这是SpringBoot使用的全部Bean,如下图所示。
在这里插入图片描述

4.14 刷新后处理

afterRefresh(context, applicationArguments);

位置:org.springframework.boot.SpringApplication#afterRefresh
这个,没有实际的逻辑,空操作,
应该可以通过继承的方式,重写。
在这里插入图片描述

4.15 获取启动时间

stopWatch.stop();

到这里,完成了SpringBoot的启动,后续操作是事件监听器相关的操作,
如listeners.started(context)、listeners.running(context);就不展开讲解了。
停止前,totalTimeNanos为0ns,如下图所示。
在这里插入图片描述

停止后,totalTimeNanos为71955983100ns,如下图所示。
在这里插入图片描述

5 小结

启动核心步骤:
(1)启动计时:StopWatch.start
(2)创建BootstrapContext:createBootstrapContext()
(3)配置Headless属性:configureHeadlessProperty()
(4)获取监听器,由createBootstrapContext()创建:getRunListeners(args)
(5)添加监测标识:listeners.starting(bootstrapContext, this.mainApplicationClass),可进一步翻阅源码查看
(6)获取启动时的参数,主函数入口的参数:new DefaultApplicationArguments(args)
(7)准备环境,加载自定义的配置,如application.yml和application-dev.yml,prepareEnvironment(listeners, bootstrapContext, applicationArguments)
(8)过滤Bean,跳过Bean查询:configureIgnoreBeanInfo(environment)
(9)打印Banner,控制台看到的默认SpringBoot就是来自这里:printBanner(environment)
(10)实例化上下文,包括AnnotatedBeanDefinitionReader、ClassPathBeanDefinitionScanner和DefaultListableBeanFactory等:createApplicationContext()
(11)初始化上下文,添加默认的注解Bean:prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
(12)加载自定义的Bean以及SpringBoot的Bean:refreshContext(context);
(13)计时结束,完成启动。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天然玩家

坚持才能做到极致

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值