SpringBoot 启动流程追踪方法分享

 更新时间:2023年08月10日 08:42:40   作者:M-Anonymous  
这篇文章主要介绍了SpringBoot 启动流程追踪方法分享的相关资料,需要的朋友可以参考下

1、初始化 SpringApplication

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		this.bootstrapRegistryInitializers = new ArrayList<>(
				getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}

在完成初始化工作后,可以看到设置了如下属性:bootstrapRegistryInitializers:

initializers:

listeners:

这些属性咋来的上一篇文章中有提到过,会扫描 spring-boot、spring-boot-autoconfigure、spring-beans 包里面 resource 目录下 META-INF/spring.factories 文件进行加载,如果你想添加自己的配置,也可以在自己项目的 resource 目录下添加配置。

2、加载 spring.factories

	private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
		Map<String, List<String>> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		result = new HashMap<>();
		try {
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
								.add(factoryImplementationName.trim());
					}
				}
			}

			// Replace all lists with unmodifiable lists containing unique elements
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		return result;
	}

加载结果如下所示:

以后有些地方加载类的时候,就会直接从缓存取了。

3、环境准备前的工作(run 方法代码片段)

long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);

在 createBootstrapContext 方法里面会调用 bootstrapRegistryInitializers 的 initializer 方法,不过 SpringBoot 该属性没值。然后会调用 getRunListeners 方法加载 SpringApplicationRunListeners,该值同样也是从 spring.factories 文件进行加载的。该 listeners(SpringApplicationRunListeners) 下的 SpringApplicationRunListener 只有一个: EventPublishingRunListener

然后会调用该类的 starting 方法,会触发 ApplicationStartingEvent 事件,该事件会被 SpringApplication 下的 listeners 监听。如果你感兴趣的话,可以看看 8 个 ApplicationListener 干了什么!

4、准备环境(run->prepareEnvironment)

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

这里是先创建一个默认的 Servlet 环境,然后为该环境配置参数,在 configureEnvironment ->configurePropertySources 方法中可以看到会从命令行参数和 SpringApplication 的 defaultProperties 属性获取可配置参数。环境准备好后,会执行 listeners.environmentPrepared 方法,上文提到过,该方法只有一个实现类,调用该方法会触发 ApplicationEnvironmentPreparedEvent 事件,同样也会被监听到。该方法目前就看这两个就行了,其他的方法不知道在哪儿用的,看了也说不明白。

5、配置 Banner 和 上下文

Banner printedBanner = printBanner(environment);
context = createApplicationContext();

如果想要知道怎么自定义 Banner,可以看 printBanner,通过创建 banner.txt 文本格式或 banner.png、banner.gif、banner.gif 等图片格式文件,可实现自定义 banner,文件默认放在 resource 目录下就行,如果不嫌麻烦的话也可以自定义 banner 存放目录。接下来是 context 上下文,这在后面会经常用到,它会使用默认的 contextFactory 来创建 context,并且它是通过 loadSpringFactories 方法来获取的,其实现类在 spring.factories 里配置的是 AnnotationConfigServletWebServerApplicationContext。

6、准备上下文(run->prepareContext)

	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 AbstractAutowireCapableBeanFactory) {
			((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);
			if (beanFactory instanceof DefaultListableBeanFactory) {
				((DefaultListableBeanFactory) beanFactory)
					.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
			}
		}
		if (this.lazyInitialization) {
			context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
		}
		context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
		// 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);
	}

这里调用了 applyInitializers 方法,之前 SpringApplication 加载了 7 个 ApplicationContextInitializer,这里会调用每个 initializer 的 initialize 方法。接着就调用 listeners 的 contextPrepared 方法,还是之前的 EventPublishingRunListener,该方法会触发 8 个 ApplicationListener 监听 ApplicationContextInitializedEvent 事件。在这之后的 bootstrapContext.close 方法也会 BootstrapContextClosedEvent 事件。然后想 set、add、log 啥的可以直接跳过,beanFactory.registerSingleton 方法可以点进去看看,不过也是 add 啥的,这些其实都是为后面实质性的操作做准备,后面可以再追溯数据来源。接着看看 load 方法,load 方法的 source 来源一个是 primarySources,另一个是 sources,都是 SpringApplication 的属性,该方法可以加载 Bean,并且该方法细节也是蛮多的,这里先记着,后面再看。最后就是 listeners.contextLoaded 方法了,该方法会触发 ApplicationPreparedEvent 事件。

7、添加上下文销毁线程(refreshcontext...->addRuntimeShutdownHook)

	void addRuntimeShutdownHook() {
try {
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
}
catch (AccessControlException ex) {
// Not allowed in some environments
}
}
@Override
public void run() {
Set<ConfigurableApplicationContext> contexts;
Set<ConfigurableApplicationContext> closedContexts;
Set<Runnable> actions;
synchronized (SpringApplicationShutdownHook.class) {
this.inProgress = true;
contexts = new LinkedHashSet<>(this.contexts);
closedContexts = new LinkedHashSet<>(this.closedContexts);
actions = new LinkedHashSet<>(this.handlers.getActions());
}
contexts.forEach(this::closeAndWait);
closedContexts.forEach(this::closeAndWait);
actions.forEach(Runnable::run);
}

这里在线程末尾会执行上下文的 closeAndWait 方法,以及支持自定义的 actions。

8、收尾工作

在刷新完 context 之后,会执行 listeners.started、ready 方法,分别会触发 ApplicationStartedEvent、ApplicationReadyEvent 事件。同样会被 8 个 ApplicationListener 监听到。另外还有一个 callRunners 方法值得注意,任何实现了 ApplicationRunner、CommandLineRunner 接口的实现类都会得到执行。

9、加载 Bean(load)

前面提到过 load 方法也是一个值得注意的方法,他可以通过好几种方式注册 Bean:

private void load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class<?>) {
load((Class<?>) source);
return;
}
if (source instanceof Resource) {
load((Resource) source);
return;
}
if (source instanceof Package) {
load((Package) source);
return;
}
if (source instanceof CharSequence) {
load((CharSequence) source);
return;
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}

首先是 load(Class<?> source) 方法:它会判断本地有没有 groovy 环境,然后 source 对象是 GroovyBeanDefinitionSource 类或其子类的实例时,就实例化它,然后将 loader 对象的 bean 方法返回的 Bean 添加到 groovyReader 中。然后就判断其是否有资格注册为 Bean。

private void load(Class<?> source) {
if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
// Any GroovyLoaders added in beans{} DSL can contribute beans here
GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans());
}
if (isEligible(source)) {
this.annotatedReader.register(source);
}
}

然后是 load(Resource source) 方法:总之它会从 grovvy 文件或者 xml 文件中注册 Bean。

private void load(Resource source) {
if (source.getFilename().endsWith(".groovy")) {
if (this.groovyReader == null) {
throw new BeanDefinitionStoreException("Cannot load Groovy beans without Groovy on classpath");
}
this.groovyReader.loadBeanDefinitions(source);
}
else {
if (this.xmlReader == null) {
throw new BeanDefinitionStoreException("Cannot load XML bean definitions when XML support is disabled");
}
this.xmlReader.loadBeanDefinitions(source);
}
}

然后是 load(Package source) 方法:总之它会从 package 里注册 Bean。

private void load(Package source) {
this.scanner.scan(source.getName());
}

最后就是 load(CharSequence source) 方法:它就很有意思了,它会尝试将其作为以上三种方式进行加载。

private void load(CharSequence source) {
String resolvedSource = this.scanner.getEnvironment().resolvePlaceholders(source.toString());
// Attempt as a Class
try {
load(ClassUtils.forName(resolvedSource, null));
return;
}
catch (IllegalArgumentException | ClassNotFoundException ex) {
// swallow exception and continue
}
// Attempt as Resources
if (loadAsResources(resolvedSource)) {
return;
}
// Attempt as package
Package packageResource = findPackage(resolvedSource);
if (packageResource != null) {
load(packageResource);
return;
}
throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'");
}

10、总结

这篇文章我们了解了自动装配的工作方式,也就是 spring.factories。然后就是 Banner 是如何打印的、context 环境准备完毕后如何执行自定义代码,context 的销毁工作以及最后的两种 runner 怎么使用。本文着重介绍了 load 方法通过几种方式注册 Bean的,包括 Groovy、xml等文件方式、Package包、Class类、CharSequence字符串等方式进行注册。最后遗留了一个刷新上下文 refresh 方法没有分析,这也是一个很重要的方法。

到此这篇关于SpringBoot 启动流程追踪方法分享的文章就介绍到这了,更多相关SpringBoot 启动流程追踪内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java8学习教程之lambda表达式语法介绍

    Java8学习教程之lambda表达式语法介绍

    众所周知lambda表达式是JAVA8中提供的一种新的特性,它支持Java也能进行简单的“函数式编程”。 下面这篇文章主要给大家介绍了关于Java8学习教程之lambda表达式语法的相关资料,需要的朋友可以参考下。
    2017-09-09
  • 浅谈使用java解析和生成JSON

    浅谈使用java解析和生成JSON

    在www.json.org上公布了很多JAVA下的json构造和解析工具,其中google-gson和org.json比较简单,两者使用上差不多但还是有些区别。下面我们就来分别介绍下用他们构造和解析Json数据的方法示例。
    2015-08-08
  • Java中使用标签(label)来控制循环的执行流程

    Java中使用标签(label)来控制循环的执行流程

    java 和 label 两个完全不相干的词,今天我把他们连接在一起了,试想一个问题,双循环在不使用 return 的情况下,如何停止外循环?所以本文给大家介绍了Java中使用标签(label)来控制循环的执行流程,需要的朋友可以参考下
    2024-06-06
  • 精通Java泛型的使用与原理

    精通Java泛型的使用与原理

    针对利用继承来实现通用程序设计所产生的问题,泛型提供了更好的解决方案,本文详细的介绍了Java泛型的使用与原理,感兴趣的可以了解一下
    2022-03-03
  • Java格式化小数并保留两位小数的四种方法

    Java格式化小数并保留两位小数的四种方法

    Java中格式化小数并保留两位小数的四种方法:使用DecimalFormat、String.format()、BigDecimal和NumberFormat,每种方法都有其适用场景和特点,文章通过代码示例介绍的非常详细,需要的朋友可以参考下
    2025-03-03
  • Java 包和访问权限操作

    Java 包和访问权限操作

    这篇文章主要介绍了Java 包和访问权限操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 详解Mybatis Generator的具体使用教程

    详解Mybatis Generator的具体使用教程

    Mybatis Generator可以帮助我们自动生成很多结构化的代码,比如每张表对应的Entity、Mapper接口和Xml文件,可以省去很多繁琐的工作,今天通过本文给大家介绍Mybatis Generator的具体使用教程,感兴趣的朋友一起看看吧
    2022-02-02
  • 一文搞懂Java中的反射机制

    一文搞懂Java中的反射机制

    Java的反射机制是在运行状态中,对于任何一个类,都可以知道这个类的所有属性和方法,对于任何一个对象,都可以调用它所有的方法和属性,修改部分类型信息。本文就来详细讲讲Java反射机制的使用
    2022-07-07
  • IDEA 2023创建JSP项目的完整步骤教程

    IDEA 2023创建JSP项目的完整步骤教程

    这篇文章主要介绍了IDEA 2023创建JSP项目的完整步骤教程,创建项目需要经过新建项目、设置项目名称和路径、选择JDK版本、添加模块和工件、配置Tomcat服务器等步骤,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2024-10-10
  • Java中Base64和File之间互转代码示例

    Java中Base64和File之间互转代码示例

    这篇文章主要给大家介绍了关于Java中Base64和File之间互转的相关资料,Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法,需要的朋友可以参考下
    2023-08-08

最新评论