本文基于Spring Boot 2.1.4.RELEASE版本,希望通过"自顶向下"的方法来理解Spring Boot的启动流程。先从整体上了解流程走向,再查看对应源码。
启动类
Spring Boot的启动类通常如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
首先看一下@SpringBootApplication
注解,这个注解由3个注解组成
@SpringBootApplication=@EnableAutoConfiguration+@ComponentScan+@Configuration
注意:
@SpringBootApplication实际上注册的是@SpringBootConfiguration,但@SpringBootConfiguration里面包含了@Configuration。
各个注解作用如下:
- @EnableAutoConfiguration 开启自动配置
- @ComponentScan SpringBean扫描
- @Configuration 开启配置类
然后再看一下SpringApplication
类,这个类主要通过如下步骤启动应用:
- 根据应用的classpath创建对应的ApplicationContext(独立应用、WEB应用或REACTIVE应用)
- 注册CommandLinePropertySource,把命令行参数转换为Spring Properties
- 刷新ApplicationContext,加载所有的单例SpringBean
- 调用实现了CommandLineRunner的SpringBean中的run方法
下面从源码上分别来看下这些步骤,这些步骤主要在SpringApplication
类中的run方法中(public ConfigurableApplicationContext run(String... args)
)
主要源码如下
代码清单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));
// 根据classpath中的类来确定具体是哪种类型的应用
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
代码清单2,SpringApplication
类中run方法的主要代码
public ConfigurableApplicationContext run(String... args) {
......
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
//注册CommandLinePropertySource,把命令行参数转换为Spring Properties
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
//创建ApplicationContext
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//刷新ApplicationContext
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
//调用实现了CommandLineRunner的SpringBean中的run方法
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
......
}
根据应用的classpath创建对应的ApplicationContext(独立应用、WEB应用或REACTIVE应用)
SpringApplication判断启动的应用类型
确定应用类型的具体方法在SpringApplication
的构造方法中,具体看代码清单1中的this.webApplicationType = WebApplicationType.deduceFromClasspath();
这行,deduceFromClasspath方法具体如下:
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;
}
实现比较清晰,具体是通过判断classpath中的类来确定是哪种类型的应用。
SpringApplication创建ApplicationContext
创建ApplicationContext的代码在代码清单2中的context = createApplicationContext();
这行,createApplicationContext的实现如下:
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
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);
}
这个实现也比较清晰,根据之前确定的应用类型,反射实例化对应的ApplicationContext。
注册CommandLinePropertySource,把命令行参数转换为Spring Properties
注册CommandLinePropertySource的代码在代码清单2中的ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
这行,这里从方法名能看出来,主要是准备Spring环境的参数,具体实现如下:
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 根据不同的应用类型返回环境参数
ConfigurableEnvironment environment = getOrCreateEnvironment();
//分别配置PropertySource和Profile
configureEnvironment(environment, applicationArguments.getSourceArgs());
//触发监听事件
listeners.environmentPrepared(environment);
//把环境参数绑定到SpringApplication
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader())
.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
准备Spring环境这个实现相对来说还是挺复杂,需要对Spring结构有较多认识才能看懂,所以这里主要还是专注启动步骤,注册CommandLinePropertySource的实现在configureEnvironment(environment, applicationArguments.getSourceArgs());
这行,具体实现如下:
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
if (this.addConversionService) {
//转换服务,转换配置环境中的日期、数字等
ConversionService conversionService = ApplicationConversionService
.getSharedInstance();
environment.setConversionService(
(ConfigurableConversionService) conversionService);
}
//配置PropertySources
configurePropertySources(environment, args);
//配置Profiles
configureProfiles(environment, args);
}
再具体到configurePropertySources(environment, args);
这行里的方法:
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
MutablePropertySources sources = environment.getPropertySources();
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties));
}
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
"springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
这段代码的作用是注册CommandLinePropertySource,把命令行参数转换为Spring Properties。实现也比较好理解,主要是先把defaultProperties
加入到Spring Properties中,然后再合并COMMAND_LINE_PROPERTY_SOURCE_NAME
中的Spring Properties。
刷新ApplicationContext,加载所有的单例SpringBean
刷新ApplicationContext的代码在代码清单2中的refreshContext(context);
这行,refreshContext的实现如下:
private void refreshContext(ConfigurableApplicationContext context) {
//主要调用ApplicationContext本身的refresh方法
refresh(context);
if (this.registerShutdownHook) {
try {
//注册虚拟机关闭时的钩子,具体实现在AbstractApplicationContext中
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
这里的refresh(context);
主要还是调用了ApplicationContext本身的refresh方法,实现如下:
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}
此时,Spring的初始化已经基本完成。
调用实现了CommandLineRunner接口的SpringBean中的run方法
调用实现了CommandLineRunner的SpringBean中的run方法的代码在代码清单2中的callRunners(context, applicationArguments);
这行,具体实现如下:
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不仅会调用实现了CommandLineRunner的SpringBean的run方法,还会调用实现了ApplicationRunner的run方法,具体区别在ApplicationRunner的run方法会传入Spring包装的ApplicationArguments,而CommandLineRunner的run方法会传入原始的命令行传入参数,具体代码如下:
ApplicationRunner
private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
try {
(runner).run(args);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
}
}
CommandLineRunner
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
try {
(runner).run(args.getSourceArgs());
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
}
}
到这里,基本上对Spring Boot 2的启动流程解析就结束了,主要根据文档上的启动步骤来理解,不过还有很多小细节没有涵盖到,比如exceptionReporters
、打印banner等。
FAQ
为什么Spring Boot启动类上通常会加上@SpringBootApplication注解?
因为大多数开发者都会使用 自动配置、SpringBean扫描和配置类(@EnableAutoConfiguration+@ComponentScan+@Configuration),为了简化配置和遵循“约定优先于配置”设计,所以设计了@SpringBootApplication注解用于简化启动类的配置。
怎么知道SpringApplication主要是通过哪些步骤启动应用?
通过看源码上的类注释,一般文档做得比较好的框架都会有详细解释,有些会在类上,有些会在文档里。从这些解释入手是最方便的。如果框架的文档不完善,就只能靠自己提取重要步骤。
评论
发表评论