SpringBoot的启动流程?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
回答话术
总的来说,SpringBoot 应用的启动流程就分为 SpringApplication
的创建,与 SpringApplication.run
方法的调用两部分。
- 创建 Spring 应用:
- 获取应用类型:根据当前项目是否引入特定依赖而决定当前的应用是否是非 Web 应用、普通 Web 应用或响应式 Web 应用;
- 加载初始化器与事件监听器:根据 spring.factories 文件中的全限定名加载初始化器
ApplicationContextInitializer
与事件监听器ApplicationListener
; - 获取启动类:通过异常堆栈信息获取 main 方法所在的类。
- 启动 Spring 应用:
- 加载运行监听器:根据 spring.factories 文件中的全限定名加载运行时监听器
SpringApplicationRunListener
,后续所有关键步骤都会调用监听器中的对应方法; - 初始化环境变量:加载命令行启动参数与配置文件,并基于此初始化一个
Environment
以供后续使用; - 初始化上下文:根据应用类型创建一个
ConfigurableApplicationContext
实例,并对为其配置一些基本组件,再使用初始化器完成初始化之后调用refresh
方法完成刷新,将其启用; - 调用回调接口:一切都完成后,从 Spring 容器中获取所有的
ApplicationRunner
和CommandLineRunner
并调用。
- 加载运行监听器:根据 spring.factories 文件中的全限定名加载运行时监听器
问题详解
1. 创建 Spring 应用
在启动 main 方法之后,程序将会创建一个 SpringApplication
实例,在构造方法中,它将会做四件事:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader; // 默认都是 null
Assert.notNull(primarySources, "PrimarySources must not be null"); // 一般即启动类本身
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 推断当前的应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 获取初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 获取监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 获取应用类的启用类
this.mainApplicationClass = deduceMainApplicationClass();
}
- 获取应用类型:即推断当前是一个普通非 Web 应用(
NONE
)、web 应用(SERVLET
)还是一个响应式的 Web 应用(REACTIVE
)。 - 获取初始化器:即获取初始化 Spring 应用所需的
ApplicationContextInitializer
实例。 - 获取监听器:获取用于监听应用事件的事件监听器
ApplicationListener
。 - 获取启动类:即获取有
main
方法的启动类。
1.1. 获取应用类型
这一步非常简单粗暴,就是判断当前项目中是否引入了指定的包:
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
// WebMVC
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
// WebFlux
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler"
// Jersy
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
// Servlet
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
// Reactive
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
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;
}
简单的来说:
- 如果你没有引入了 WebMVC、WebFlux 或者 Jersy 任意一者,那么你肯定不是一个 Web 应用。
- 如果你只引入了 WebFlux,那你必定是一个响应式 Web 应用。
- 否则必然是一个基于 WebMVC 或者 Jersy 的普通 Web 应用。
- Jersy 也是基于 Servlet 的 Web 框架,它的工作原理与 SpringMVC 类似, 同样可以用于构建基于注解映射的 RESTful 风格 AIP。
- WebFlux 即 Spring 提供的响应式风格 Web 框架,它是 SpringMVC 的对等替代品。
1.2. 获取应用初始化器与事件监听器
这一步也没什么好说的,和自动装配一样,初始化器 ApplicationContextInitializer
与事件监听器 ApplicationListener
的加载实际上也是基于 Spring 的 SPI 机制完成的:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// 通过 SpringFactoriesLoader 加载 spring.factories 文件指定的组件
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
ClassLoader classLoader, Object[] args, Set<String> names) {
List<T> instances = new ArrayList<>(names.size());
for (String name : names) {
try {
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
Assert.isAssignable(type, instanceClass);
Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
T instance = (T) BeanUtils.instantiateClass(constructor, args);
instances.add(instance);
}
catch (Throwable ex) {
throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
}
}
return instances;
}
这里的逻辑与自动装配那里完全一样:
- 通过
SpringFactoriesLoader
加载META-INF/spring.factories
文件中的配置。 - 根据指定的组件类型获取
spring.factories
文件中指定的组件类全限定名。 - 根据类全限定名加载类并通过反射创建实例。
这里的 type —— 即自动装配那篇文章中提到的 factoryType —— 就为分别 org.springframework.contexApplicationContextInitializer
和 org.springframework.context.ApplicationListener
。
所有必要的初始化器和监听器都已经在 spring-boot
这个模块的 spring.factories 文件中指定好了。
- 关于自动装配、
SpringFactoriesLoader
与 factoryType,请参见:✅ 什么是自动装配?- 关于 Spring 的事件机制,请参见:什么是 Spring 的事件机制?
- 关于 Spring 默认支持的事件类型,请参见:Spring 默认支持哪些事件?
1.3. 获取启动类
这一步更是简单粗暴,直接创建一个 RuntimeException
异常,然后递归堆栈一直到找到 main
方法为止:
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;
}
2. 启动 Spring 应用
当 SpringApplication
中加载了必要的初始化器和事件监听器后,就正式进入启动流程:
public ConfigurableApplicationContext run(String... args) {
// 创建一个计时器,在启动成功后输出启动时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 默认创建一个 ConfigurableApplicationContext 上下文
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 通过 SPI 加载应用运行监听器 SpringApplicationRunListener,
// 该监听器用于监听整个 ApplicationBoot 应用的关键启动步骤
SpringApplicationRunListeners listeners = getRunListeners(args);
// 触发启动事件
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备环境变量 Environment
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印 banner
Banner printedBanner = printBanner(environment);
// 创建 Spring 上下文,也就是真正的核心容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 初始化上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新上下文,即触发 refresh 方法
refreshContext(context);
afterRefresh(context, applicationArguments);
// 到此启动完成,输出启动时间
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 触发启动成功事件
listeners.started(context);
// 调用 ApplicationRunner 和 CommandLineRunner
// 注意,此时由于 Spring 容器已经刷新完毕,因此两者都是直接从 Spring 容器获取的
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
这一步主要的逻辑可以分为四部分:
- 加载所有应用运行监听器
SpringApplicationRunListener
。 - 准备应用的运行环境
ConfigurableEnvironment
。 - 创建并刷新上下文
ConfigurableApplicationContext
。 - 从上下文中获取回调接口并调用(
ApplicationRunner
和CommandLineRunner
)。
2.1. 加载运行监听器
这一步与上文创建 SpringApplication 时获取事件监听器和初始化器的操作一样,也是通过 SPI 从 spring.factories 文件中加载所有实现了 SpringApplicationRunListener
接口并以此为 factoryType 的组件,然后将其封装为 SpringApplicationRunListeners
以便在后文调用。
public interface SpringApplicationRunListener {
default void starting() { }
default void environmentPrepared(ConfigurableEnvironment environment) { }
default void contextPrepared(ConfigurableApplicationContext context) { }
default void contextLoaded(ConfigurableApplicationContext context) { }
default void started(ConfigurableApplicationContext context) { }
default void running(ConfigurableApplicationContext context) { }
default void failed(ConfigurableApplicationContext context, Throwable exception) { }
}
需要注意的是,运行监听器 SpringApplicationRunListener
与 ApplicationListener
两者虽然功能由一定的相似,但是不能一概而论。
事件监听器是 Spring 中的一个公共抽象,支持监听所有 Spring 或用户自定义的事件,而 SpringApplicationRunListener 只能监听的是应用的运行状态的改变,比如启动中,启动成功或失败以及关闭等关键操作。
关于 Spring 的事件机制,请参见:什么是 Spring 的事件机制?
2.2. 准备环境变量
在初始化上下文之前,Spring 需要先准备好运行环境,也就是 Environment
,这个组件包含有我们在启动 SpringBoot 应用时指定的启动参数:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 根据应用类型选择不同的运行环境
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置运行环境参数,这里分为两类:
// propertySources:即配置文件中的普通参数。
// profiles:即我们通过 spring.profiles.active 指定的激活状态的配置文件类型。
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 更新启动参数
ConfigurationPropertySources.attach(environment);
// 触发启动监听器
listeners.environmentPrepared(environment);
// 将运行环境绑定到当前的 SpringApplication
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
// 将参数封装为 PropertySources 以供后面的程序使用
ConfigurationPropertySources.attach(environment);
return environment;
}
关于这里提到的“启动参数”,简单的来说,就是我们在服务器中通过 java -jar
命令启动程序时指定的参数。
比如,我们假如我们现在配置文件中如下:
server.port = 8081
spring.profiles.active = dev
那么我们可以在启动时指定动态的去覆盖它:
java -jar -Dspring.profiles.active=test -Dserver.port=8081 sb.jar
2.3. 准备上下文
2.3.1. 创建上下文
我们知道,Spring 的容器是一切的核心,而在这一步,Spring 将会根据应用类型创建一个 ConfigurableApplicationContext
:
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
// AnnotationConfigServletWebServerApplicationContext
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
// AnnotationConfigReactiveWebServerApplicationContext
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
// AnnotationConfigApplicationContext
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
2.3.2. 初始化上下文
在这一步 Spring 将会对上下文进行后处理,然后应用各种初始化器,并加载一些必要的组件:
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置运行环境
context.setEnvironment(environment);
// 对应上下文进行后处理
postProcessApplicationContext(context);
// 对上下文应用初始化器
applyInitializers(context);
// 发布上下文处理回调
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注册默认的几个单例 Bean,包括(springApplicationArguments 和 springBootBanner )
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 设置是否运行同名 Bean 之间互相覆盖
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// 添加一个默认的懒加载后处理器
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// 将启动类作为一个 Bean 加载到上下文
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
我们主要关注 applyInitializers
方法,Spring 通过初始化器,在 ApplicationContext
真正刷新启用前做了一些额外的事情。比如:
ServletContextApplicationContextInitializer
:为当前的WebApplicationContext
设置ServletContext
。ContextIdApplicationContextInitializer
:设置上下文 ID,也就是我们spring.application.name
配置的值。ServerPortInfoApplicationContextInitializer
:将 Web 监听端口写入配置文件。DelegatingApplicationContextInitializer
:允许我们在配置文件中通过context.initializer.classes
配置额外的初始化器,而不必一定要通过spring.factories
文件。
2.3.3. 上下文的刷新
即 refreshContext
方法,这一步最终会调用 AbstractApplicationContext
的 refresh
,完成 Spring 容器的启动,并预先加载所有的单例 Bean。
关于这步的内容,请参见:✅ Spring 容器的启动过程?
2.4. 调用回调接口
在一切准备就绪后,Spring 将会调用容器级的回调接口ApplicationRunner
和 CommandLineRunner
。
由于此时 Spring 的容器已经完成初始化,因此两者可以直接从 Spring 容器中获取,而不必要求一定要通过 SPI 配置:
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
至此,一个 Spring 应用就启动完成了。