Skip to main content

SpringBoot的启动流程?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术

总的来说,SpringBoot 应用的启动流程就分为 SpringApplication的创建,与 SpringApplication.run 方法的调用两部分。

  • 创建 Spring 应用:
    1. 获取应用类型:根据当前项目是否引入特定依赖而决定当前的应用是否是非 Web 应用、普通 Web 应用或响应式 Web 应用;
    2. 加载初始化器与事件监听器:根据 spring.factories 文件中的全限定名加载初始化器 ApplicationContextInitializer 与事件监听器 ApplicationListener
    3. 获取启动类:通过异常堆栈信息获取 main 方法所在的类。
  • 启动 Spring 应用:
    1. 加载运行监听器:根据 spring.factories 文件中的全限定名加载运行时监听器 SpringApplicationRunListener,后续所有关键步骤都会调用监听器中的对应方法;
    2. 初始化环境变量:加载命令行启动参数与配置文件,并基于此初始化一个 Environment 以供后续使用;
    3. 初始化上下文:根据应用类型创建一个 ConfigurableApplicationContext 实例,并对为其配置一些基本组件,再使用初始化器完成初始化之后调用 refresh 方法完成刷新,将其启用;
    4. 调用回调接口:一切都完成后,从 Spring 容器中获取所有的 ApplicationRunnerCommandLineRunner并调用。

问题详解

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.contexApplicationContextInitializerorg.springframework.context.ApplicationListener

所有必要的初始化器和监听器都已经在 spring-boot 这个模块的 spring.factories 文件中指定好了。

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
  • 从上下文中获取回调接口并调用ApplicationRunnerCommandLineRunner )。

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) { }
}

需要注意的是,运行监听器 SpringApplicationRunListenerApplicationListener 两者虽然功能由一定的相似,但是不能一概而论。

事件监听器是 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 方法,这一步最终会调用 AbstractApplicationContextrefresh,完成 Spring 容器的启动,并预先加载所有的单例 Bean。

关于这步的内容,请参见:✅ Spring 容器的启动过程?

2.4. 调用回调接口

在一切准备就绪后,Spring 将会调用容器级的回调接口ApplicationRunnerCommandLineRunner

由于此时 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 应用就启动完成了。