文档章节

SpringBoot源码:启动过程分析(二)

Jacktanger
 Jacktanger
发布于 11/19 23:58
字数 1642
阅读 36
收藏 9

    接着上篇继续分析 SpringBoot 的启动过程。

    SpringBoot的版本为:2.1.0 release,最新版本。

一.时序图

    一样的,我们先把时序图贴上来,方便理解:

 

二.源码分析

    回顾一下,前面我们分析到了下面这步:

    public static ConfigurableApplicationContext run(Class<?>[] primarySources,
			String[] args) {
        // 前面分析了前一部分,SpringApplication的构造方法,本文分析后一部分的run()方法
		return new SpringApplication(primarySources).run(args);
	}

    SpringApplication#run方法的内容较多,准备刷屏了:

    public ConfigurableApplicationContext run(String... args) {
        // StopWatch是Spring中一个任务执行时间控制的类,记录了任务的执行时间
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();// 开始时间
		ConfigurableApplicationContext context = null;
        // 创建一个SpringBootExceptionReporter的List,记录启动过程中的异常信息
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        // 设置系统属性 "java.awt.headless" 为 true
		configureHeadlessProperty();
        // 获取所有启动监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
        // 调用所有监听器的starting()
		listeners.starting();
		try {
            // 根据入参创建 ApplicationArguments 对象
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
            // 准备应用上下文 environment 对象
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
            // 设置系统属性 "spring.beaninfo.ignore",是否忽略bean信息
			configureIgnoreBeanInfo(environment);
            // 获取打印 Banner 对象,也就是应用启动时候看到控制台输出的那个很大的 "SpringBoot"
			Banner printedBanner = printBanner(environment);
            // 创建IOC容器
			context = createApplicationContext();
            // 获取异常报告,在异常时报告异常信息
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
            // 准备上下文
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
            // 刷新上下文
			refreshContext(context);
            // 刷新完后
			afterRefresh(context, applicationArguments);
			stopWatch.stop();// 启动完成,记录启动结束时间
			if (this.logStartupInfo) {// 打印详细启动时间信息
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
            // 调用所有监听器的started(ctx)
			listeners.started(context);
            // 调用实现了 ApplicationRunner、CommandLineRunner 的对象的 run 方法
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);// 调用所有监听器的running()
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;// 返回上下文
	}

    上面从大体上介绍了SpringApplication在run()中做的事情,下面详细分析每步具体的操作。

    SpringApplication#getRunListeners,获取所有启动监听器,一样的套路,通过SPI机制,通过工厂创建SpringApplicationRunListener的实例:

    private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
        // 注意这个地方,我们第二次看到了getSpringFactoriesInstances这个方法,作用就是从类路径下 META-INF/spring.factories 下加载 SpringFactory 实例,最后传入 SpringApplicationRunListener.class,创建实例
		return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
				SpringApplicationRunListener.class, types, this, args));
	}

    从 spring.factories 里面找到如下内容:也就是最后拿到SpringApplicationRunListener的实例是EventPublishingRunListener的对象。

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

    SpringApplication#configureHeadlessProperty,设置系统属性 "java.awt.headless" 。headless是系统的一种配置模式,在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式,也就是告诉服务器,没有这些硬件设施,当需要这是设备信息的时候,别慌,我可以使用awt组件通过计算模拟出这些外设的特性。

    private void configureHeadlessProperty() {
        // this.headless在初始化的时候值为 "true"
		System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
				SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
	}

    SpringApplication#prepareEnvironment,准备应用上下文 environment 对象,像设置当前使用的配置文件profile,就在该方法内完成:

    private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 添加 ConversionService 转换器,保存启动参数
		configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 调用所有监听器的environmentPrepared(env)
		listeners.environmentPrepared(environment);
        // 将 environment 绑定到当前Spring上下文
		bindToSpringApplication(environment);
        // 判断是否用户自定义Environment
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader())
					.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

    SpringApplication#configureEnvironment,添加 ConversionService 转换器,保存启动参数,即最外面main的入参args数组:

    protected void configureEnvironment(ConfigurableEnvironment environment,
			String[] args) {
		if (this.addConversionService) {
			ConversionService conversionService = ApplicationConversionService
					.getSharedInstance();
			environment.setConversionService(
					(ConfigurableConversionService) conversionService);
		}
        // 合并命令行启动参数到当前 enviroment,这个args就是最外层App启动传入的main方法的参数
		configurePropertySources(environment, args);
        // 设置当前运行环境的配置文件(dev/uat/prod)
		configureProfiles(environment, args);
	}

    SpringApplication#configureProfiles ,设置当前运行环境的配置文件,会去查看"spring.profiles.active"中指定的是什么:

    protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
        // 去找当前激活的Profile是哪个 "spring.profiles.active"
		environment.getActiveProfiles(); // ensure they are initialized
		// But these ones should go first (last wins in a property key clash)
		Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
		profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
		environment.setActiveProfiles(StringUtils.toStringArray(profiles));
	}

    不小心有陷入进去了,再次回到run()。

    接着看后面是获取Banner。根据环境找到Banner,默认找classpath下面的 banner.txt,Gitee上面很多管理系统打印出各种自定义名称。实现很简单,只要你提前制作好banner.txt,放到resources下面,启动的时候,就会加载你的,覆盖原始的Banner信息。这部分代码比较简单,只要跟进去看下就能看明白,考虑篇幅问题,省略过了。分享一个在线设计Banner.txt的网站

    如果需要关闭Banner输出,在App里面调用:

SpringApplication.setBannerMode(Banner.Mode.OFF);// 关掉Banner

    接下来重点看一下 SpringApplication#prepareContext,分析SpringBoot如何准备上下文的:

    private void prepareContext(ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
        // 给当前 context 设置 environment
		context.setEnvironment(environment);
        // 设置 beanNameGenerator、resourceLoader、ConversionService
		postProcessApplicationContext(context);
        // 获取所有 ApplicationContextInitializer,调用其 initialize(ctx) 方法
		applyInitializers(context);
        // 调用所有监听器的contextPrepared(ctx)
		listeners.contextPrepared(context);
		if (this.logStartupInfo) {
            // 打印启动进程信息
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);// 打印 profile 信息
		}
		// Add boot specific singleton beans 注册 SpringBoot 特有的单实例
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
		if (printedBanner != null) {
			beanFactory.registerSingleton("springBootBanner", printedBanner);
		}
		if (beanFactory instanceof DefaultListableBeanFactory) {
            // 设置是否允许重写 BeanDefinition
			((DefaultListableBeanFactory) beanFactory)
					.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
		}
		// Load the sources
		Set<Object> sources = getAllSources();
		Assert.notEmpty(sources, "Sources must not be empty");
        // 创建 BeanDefinitionLoader,用于加载 BeanDefinition,此处加载应用启动类有@SpringBootApplication 注解的主类
		load(context, sources.toArray(new Object[0]));
        // 调用所有监听器的contextLoaded(ctx)
		listeners.contextLoaded(context);
	}

    稳住,快启动完了。接着看SpringApplication#refreshContext,完成刷新上下文的操作:

    private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);// 刷新上下文,最后使用了AbstractApplicationContext#refresh方法,进入了Spring的启动流程
		if (this.registerShutdownHook) {
			try {
                // 注册停止钩子,为了在应用程序shutdown的时候销毁所有 bean 实例
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}

    然后是到了SpringApplication#afterRefresh,这是一个模板方法,父类不提供实现,留给子类发挥想象实现,在context刷新好之后需要做的事情可以在此方法实现中完成。

    最后就是 stopWatch.stop() 停止计时,打印启动耗时信息,回调监听器的started(ctx)方法,返回上下文context。

 

    至此,SpringBoot启动过程分析完成。

© 著作权归作者所有

共有 人打赏支持
Jacktanger
粉丝 30
博文 116
码字总数 73193
作品 0
浦东
程序员
私信 提问
从SpringBoot源码分析 配置文件的加载原理和优先级

本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级 跟入源码之前,先提一个问题: SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数(VM Options)传...

tanliwei
07/13
0
0
springboot情操陶冶-初识springboot

前言:springboot由于其轻便和去配置化等的特性已经被广泛应用,基于时代潮流以及不被鄙视,笔者于是开辟此篇开始认识springboot 前话 springboot是基于spring而开发的轻量级框架,所以在学习...

南柯问天
07/24
0
0
SpringBoot源码:启动过程分析(一)

本文主要分析 SpringBoot 的启动过程。 SpringBoot的版本为:2.1.0 release,最新版本。 一.时序图 还是老套路,先把分析过程的时序图摆出来:时序图-SpringBoot2.10启动分析 二.源码分析 首...

Jacktanger
11/18
0
0
SpringBootBucket 2.0.4 发布,代号“傲娇的小二晶”

SpringBootBucket 自从1.0.0版本发布后就有好多人喜欢,目前码云上面star数量接近1.2k。上个月还收到了红薯签名的1000 star奖杯,这个我自己也觉得很惊讶。 由于SpringBoot 1.x官方将终止维护...

一刀
09/16
1K
4
SpringBoot下Redis相关配置是如何被初始化的

参考网页 SpringBoot集成Redis的原理 https://blog.csdn.net/hry2015/article/details/74276423 https://blog.csdn.net/hry2015/article/details/75451705 application.yml配置文件中的属性是......

karma123
07/17
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Android :报错Your project path contains non-ASCII characters.

报错内容如下 Your project path contains non-ASCII characters. This will most likely cause the build to fail on Windows. Please move your project to a different directory. See ht......

lanyu96
4分钟前
0
0
[LintCode] Number of Islands(岛屿个数)

描述 给一个01矩阵,求不同的岛屿的个数。 0代表海,1代表岛,如果两个1相邻,那么这两个1属于同一个岛。我们只考虑上下左右为相邻。 样例 在矩阵: [ [1, 1, 0, 0, 0], [0, 1, 0, 0, ...

honeymose
4分钟前
0
0
Nginx平滑添加模块

Nginx已经编译安装并运行了一段时间, 然后某一天, 发现需要用到某个模块但当初没有编译, 这个时候怎么办呢? 卸载重新安装肯定可以的, 如果Nginx版本没有变更的话, 则有一个相对平滑的方法来添...

老菜鸟0217
9分钟前
0
0
spark安装测试

spark安装测试 由于本地已经安装好hadoop相关组件,所以本文是在yarn的基础上对spark进行安装及测试 确保hdfs及yarn成功启动,hadoop版本为2.7.3 安装scala,由于本人安装的spark是2.4.0,对应...

-九天-
23分钟前
3
0
周末看完了《电能计量自动化技术》

整体质量还行,下面分别将心得记录如下: 第一章:发展历程可以看看,现在算是智能电网阶段 2:讲主站系统。以文件进行各模块的交互很值得思考,尤其是批量数据,多团队合作的情况下。另外线...

max佩恩
43分钟前
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部