源码分析|SpringBoot启动流程
Posted 永久D指针
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析|SpringBoot启动流程相关的知识,希望对你有一定的参考价值。
解析开始前先给出一张图,这是springboot项目启动时,我们可以扩展的接口,以及接口调用的时机,熟悉了这个流程,我们就可以方便快速的自定义我们自己的启动逻辑。如下:
SpringApplicationRunListener的各个方法被调用在SpringApplication.run()的各个时刻
ApplicationContextInitializer---用于编程形式的应用上下文初始化,在刷新ConfigurableApplicationContext#refresh()之前调用
ApplicationListener监听器
CommandLineRunner
这是一个简单的SpringCloud项目,这是EurekaServer模块,正好看到这里,以此来分析一下SpringBoot的启动流程,可能会和单纯的SpringBoot项目不同,因为SpringCloud会实现各种自定义扩展接口以注入各种驱动其自身运行对象。
首先看一下启动入口,依然是一个main方法,调用SpringApplication的静态run方法,传入的是EurekaServerApplication类对象和入参。
/**
* @author wangcheng
*/
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
经过一系列的run重载方法,最终到达SpringApplication的构造方法中。
/**
* Create a new {@link SpringApplication} instance. The application context will load
* beans from the specified primary sources (see {@link SpringApplication class-level}
* documentation for details. The instance can be customized before calling
* {@link #run(String...)}.
* @param resourceLoader the resource loader to use
* @param primarySources the primary bean sources
* @see #run(Class, String[])
* @see #setSources(Set)
*/
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();
// ②使用SpringFactoriesLoader.loadFactoryNames方法从各个jar包的/META-INF/spring.factories配置文件中
// 找到实现了ApplicationContextInitializer上下文初始化接口的类,并实例化它们。
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 同上,实例化ApplicationListener.class
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// ③推断该应用程序的主类,也就是EurekaServerApplication类,怎么实现呢,很巧妙。
// 首先获取到当前程序的堆栈元素数组,依次遍历,找到运行了main方法的哪个类的名称,再反射获取主类对象。
this.mainApplicationClass = deduceMainApplicationClass();
}
①确定应用类型的代码贴出来看一下
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
spring的SPI,很多地方会用到,有兴趣的同学可以看一下JAVA的SPI和Dubbo的SPI,各有千秋。
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
③确定主类的代码
private Class<?> deduceMainApplicationClass() {
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
接下来到真正重要的部分,SpringApplication的run方法,这里也就是整个Springboot启动的核心,当然一些隐藏的非常重要的流程是在各个扩展接口中实现的,我们稍后再说。看如下代码。
/**
* 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) {
// new了一个简单的计时器,用于记录各个任务的时间相关的信息。
StopWatch stopWatch = new StopWatch();
// 启动计时器
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// Headless模式是系统的一种配置模式。在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式。
configureHeadlessProperty();
// 依旧是在spring.factories配置文件中找那些实现了SpringApplicationRunListener接口的类并实例化
SpringApplicationRunListeners listeners = getRunListeners(args);
// 调用各个SpringApplicationRunListener实例的staring方法。这里SpringApplicationRunListeners类封装了一组
// SpringApplicationRunListener的实例,统一入口管理和调用,有点门面模式的意思。
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// ④prepareEnvironment,顾名思义,就是准备应用所需要的环境信息
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 忽略一些bean,因为这些bean就是为启动项目而准备的,spring会提前准备好
configureIgnoreBeanInfo(environment);
// 用来进行自定义启动banner图的,可以换成自己项目的名字等
Banner printedBanner = printBanner(environment);
// createApplicationContext创建应用上下文,依旧根据的是应用的类型穿件不同的上下文。
context = createApplicationContext();
// 依旧是在spring.factories配置文件中找那些实现了SpringBootExceptionReporter接口的类并实例化。当启动流程出现异常时调用。
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// ⑥prepareContext,顾名思义,就是准备应用的上下文信息
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// refreshContext(context)是IOC的流程。
refreshContext(context);
// afterRefresh(context, applicationArguments),留给子类实现,在上下文刷新后回调。
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 发出listeners.started(context)上下文启动事件。
listeners.started(context);
// callRunners(context, applicationArguments)内部是调用那些实现了ApplicationRunner和CommandLineRunner接口的实例。用于在容器启动后,执行我们自定义的逻辑。
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// listeners.running(context)发出容器正在运行事件。
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
④准备运行环境,包括
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
// 获取或创建一个环境实例,这里是根据之前SpringApplicaton对象初始化时确定的应用类型来决定使用哪种环境实例的。
ConfigurableEnvironment environment = getOrCreateEnvironment();
// ⑤configureEnvironment,配置环境。分为配置属性源configurePropertySources和配置活跃环境文件configureProfiles。
// Spring的属性源可以适配各种类型的源,核心接口是PropertySource,有多种实现类如Map,Properties,ServletConfig等相关的属性源。
// 使用了对象适配器模式。configureProfiles就是配置spring.active.profile属性设置的配置。
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
// 调用SpringApplicationRunListeners.environmentPrepared进行回调通知,最终处理该通知的是各个ApplicationListener实例。
// 这里的listeners中有一些还是比较重要的,如和SpringCloud相关的BootstrapApplicationListener。这个以后再说。
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
⑤配置各种propertySource和profiles
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService) conversionService);
}
// 这里面有jvm的环境变量和系统的环境变量,根据应用的类型不同还会有如servlet的web环境变量
configurePropertySources(environment, args);
configureProfiles(environment, args);
}
⑥prepareContext
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
// postProcessApplicationContext为上下文设置beanNameGenerator,resourceLoader和conversionService。
postProcessApplicationContext(context);
// applyInitializers应用初始化器,就是已开始在spring.factories中找到的那些ApplicationContextInitializer实例,依次调用其initialize方法。
applyInitializers(context);
// 通知各个ApplicationListener,上下文已准备好。
listeners.contextPrepared(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);
}
// Load the sources
// ⑦获取所有的源并加载,这里的源就是我们初始化SpringApplication时传入的EurekaServerApplication.class,
// 当然这里还有其他的source,可支持多种不同的加载类型,依次进入load方法,最终到达BeanDefinitionLoader类,
// 此类是各个BeanDefinitionReader实现类的简单门面,如AnnotatedBeanDefinitionReader,XmlBeanDefinitionReader等。
// 下面的load方法就是为了支持各种不同的源而设计的。
// 在spring扫描所有bean时,会以此为起点
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
⑦load方法,最终,相应的source被注入到上下文中的beanFactory中成为beanDefinition。
private int load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class<?>) {
return load((Class<?>) source);
}
if (source instanceof Resource) {
return load((Resource) source);
}
if (source instanceof Package) {
return load((Package) source);
}
if (source instanceof CharSequence) {
return load((CharSequence) source);
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
至此,SpringBoot启动流程结束。
这其中还有一些细节,如在各个阶段各个监听器都干了什么,spring的IOC过程等,这些放在其他文字中细讲。
以上是关于源码分析|SpringBoot启动流程的主要内容,如果未能解决你的问题,请参考以下文章
SpringBoot配置外部Tomcat项目启动流程源码分析(下)
SpringBoot配置外部Tomcat项目启动流程源码分析(下)