目录
2.1 确定应用程序类型 webApplicationType类型
2.2 初始化 初始化器 ApplicationContextInitializer
2.3 初始化 监听器 ApplicationListener
3.2 设置系统属性java.awt.headless,不用显示器
3.15 最后:应用程序就绪事件:ApplicationReadyEvent
SpringBoot启动过程
本文所使用的springboot版本:2.1.8.RELEASE
开发环境:JDK8
概述:
Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》。Spring是为了解决企业级应用开发的复杂性而创建的,使用Spring可以让简单的JavaBean实现之前只有EJB才能完成的事情。但是Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合性等方面从Spring中获益。
下面我们从源头梳理下springboot在启动过程中做了那些事~
ps:作者建议大家跟随本文,DEBUG多走几遍源码,方便各位理解!
1.入口定义
下面是最常见的SpringBoot项目启动入口,由此分析启动过程中都发生了什么。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1.1 SpringApplication静态方法run
org.springframework.boot.SpringApplication#run(java.lang.Class<?>, java.lang.String...)内部调用重载接口,调用下述代码。
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
结论1:启动类内部逻辑核心做了两件事
- new 一个SpringApplication(启动类.class)对象
- 调用他的run方法,传入启动类上的args
接下来分别对这两个过程进行展开探究~。
2.SpringApplication对象初始化
先附上结论
- 确认程序的应用类型
- SPI初始化,解析META-INF/spring.factories 文件
- 初始化初始化器 ApplicationContextInitializer
- 初始化监听器 ApplicationListener
- 设置程序运行的主类
进入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();
// 初始化初始化器 META-INF/spring.factories 文件下的 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 初始化监听器 META-INF/spring.factories 文件下的 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 设置程序运行的主类
this.mainApplicationClass = deduceMainApplicationClass();
}
2.1 确定应用程序类型 webApplicationType类型
WebApplicationType.deduceFromClasspath()方法,根据是否存在对应文件推断当前应用程序的容器。默认使用的是Servlet 容器,除了servlet之外,还有NONE 和 REACTIVE (响应式编程)
2.2 初始化 初始化器 ApplicationContextInitializer
这里我们首次看到了getSpringFactoriesInstances(ApplicationContextInitializer.class)方法。这个方法在SpringBoot的自动装配中很重要,下面我们来看下它的执行过程。
/**
* desc : 通过spi获取指定类型的实例对象
* @param : 想要获取的对象类型
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
/**
* desc : 获取实例方法
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// 拿到指定类型的类全路径名Set
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 如果缓存中存在,不再加载
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 获取指定路径的文件 META-INF/spring.factories
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
由上面的执行逻辑得出结论,SpringBoot初始化时会将下述位置的文件/META-INF/spring.factories 内容初始化到本地缓存中
key:启动类
value:自动装配的Map
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
MultiValueMap<String, String>代表有重复值的Map,可以理解为 Map<K, List<V>>,维护了spring.factories中的内容。
key是接口路径
value:是实现类全路径名List
拿到ApplicationContextInitializer所有的实现类后,通过反射将其初始化,保存到启动类的成员变量中
2.3 初始化 监听器 ApplicationListener
初始化方式和上面初始化器一样,SPI过程不再赘述,从spring.factories配置文件中加载对应的配置,通过反射 进行初始化,再加载到SpringApplication的成员变量中。
ps:区别是spring.factories初始化后就从cache中读取了
// 初始化器
private List<ApplicationContextInitializer<?>> initializers;
// 监听器
private List<ApplicationListener<?>> listeners;
2.4 推测程序运行的主类
推测程序运行的主类,其实现逻辑是基于程序运行堆栈中main方法所属的class,也就是我们程序的入口。
private Class<?> deduceMainApplicationClass() {
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
return null;
}
经过上述操作后,SpringApplication的构造函数就结束了,总结初始化期间,做了如下几件事
SpringApplication构造函数操作结论
- 确认程序的应用类型
- SPI初始化,解析META-INF/spring.factories 文件
- 初始化初始化器 ApplicationContextInitializer
- 初始化监听器 ApplicationListener
- 设置程序运行的主类
3.SpringApplication的run方法
先附上结论
- 定义时间Watch,并记录当前时间
- 设置系统属性java.awt.headless,不用显示器
- 获取应用程序运行监听器监听器,监听器启动
- 获取参数持有类
- 准备环境信息
- 配置忽略Bean信息
- 打印Banner信息
- 创建应用上下文容器
- 加载异常处理器
- 刷新容器前准备操作
- 刷新上下文,容器初始化,自动装配 - spring 初始化那一坨
- 刷新后操作
- 记录启动时间,广播事件
- 执行后置runner
附源码注释:
public ConfigurableApplicationContext run(String... args) {
// 定义时间Watch
StopWatch stopWatch = new StopWatch();
// 记录当前时间
stopWatch.start();
// Spring容器上下文对象
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 设置系统属性java.awt.headless,不用显示器
configureHeadlessProperty();
// 获取应用程序运行监听器监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 监听器启动
listeners.starting();
try {
// 参数持有类
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备环境信息
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 配置忽略Bean信息
configureIgnoreBeanInfo(environment);
// Banner信息
Banner printedBanner = printBanner(environment);
// 创建应用上下文容器
context = createApplicationContext();
// 加载异常处理器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 刷新容器前准备操作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新上下文,容器初始化,自动装配 - spring 初始化那一坨
refreshContext(context);
// 刷新后操作
afterRefresh(context, applicationArguments);
// 记录启动时间
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 事件广播 启动完成了
listeners.started(context);
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;
}
3.1 定义时间Watch,并记录当前时间
run方法第一步,初始化计时器对象,用来统计程序启动过程中的花费时间。具体实现是以:维护开始截止时间戳,做差值。
// 定义时间Watch
StopWatch stopWatch = new StopWatch();
// 记录当前时间
stopWatch.start();
3.2 设置系统属性java.awt.headless,不用显示器
配置系统属性【java.awt.headless】无显示模式运行,当系统无指定java.awt.headless属性时,默认为true。
/**
* desc : 设置系统属性java.awt.headless,代表不用显示器
*/
private void configureHeadlessProperty() {
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
3.3 获取程序运行时监听器,并启动监听器
初始化过程中第三次用到SPI,初始化方式和上述 「2.2通过SPI初始化初始化器」一样,不过加载类型为。SpringApplicationRunListener.class。
/**
* desc : 初始化 SpringApplicationRunListener.class
*/
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
拿到全部监听器后,过滤出监听ApplicationStartingEvent事件的监听器,并进行启动
/**
* desc : 遍历全部监听器,调用start方法
*/
public void starting() {
for (SpringApplicationRunListener listener : this.listeners) {
listener.starting();
}
}
监听器启动实现方式,springboot是由事件广播机制进行实现,底层由SimpleApplicationEventMulticaster发送ApplicationStartingEvent事件【事件1】
/**
* desc : 应用程序启动事件
*/
@Override
public void starting() {
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}
拓展:spring的事件结构,及常用事件类型。基于事件的发布及订阅实现不同节点的广播。具体执行节点下文进行详细阐述。
springboot初始化【事件】表
顶层类
- jdk的EventObject
spring应用事件实现类
- SpringApplicationEvent
springBoot具体事件及执行顺序
- 应用程序启动事件:ApplicationStartingEvent
- 应用环境准备事件:ApplicationEnvironmentPreparedEvent
- 应用程序上下文初始化事件:ApplicationContextInitializedEvent
- 应用程序准备事件:ApplicationPreparedEvent
- 应用程序启动完成事件:ApplicationStartedEvent
- 应用程序就绪事件:ApplicationReadyEvent
- 应用程序失败事件:ApplicationFailedEvent
3.4 获取参数持有类
获取默认应用程序参数,赋值DefaultApplicationArguments对象
// 参数持有类
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
3.5 准备环境信息
准备环境信息,创建配置环境对象,内部存储了systemProperties,systemEnvironment两类系统属性信息,获取系统profile,将配置文件与启动类绑定,准备完成后,发布【事件2】
/**
* desc : 准备环境信息
*/
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 创建配置环境对象,内部存储了systemProperties,systemEnvironment两类系统属性信息
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置系统环境,profile信息
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 配置加载好的属性源propertySource
ConfigurationPropertySources.attach(environment);
// 发布应用环境准备完成事件
listeners.environmentPrepared(environment);
// 绑定环境信息于SpringApplication对象
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
3.6 配置忽略Bean信息
此步逻辑围绕spring.beaninfo.ignore,这个配置用来忽略所有自定义的BeanInfo类的搜索。简单来说程序启动过程中不去检查所有Bean的ClassLoader,提升效率.
/**
* desc : 配置忽略Bean信息
* 程序启动过程中不去检查classNotFound的Bean,提升效率.
*/
private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}
3.7 打印Banner信息
打印控制台Banner(spring的大logo)
// Banner信息
Banner printedBanner = printBanner(environment);
banner共有三种模式。
- OFF:Disable printing of the banner.
- CONSOLE:Print the banner to System.out. -- 默认
- LOG:Print the banner to the log file
支持自定义banner,仅需要在resource目录下增加banner.txt 文件,内部维护想要的图案就好了。
3.8 创建应用上下文容器
通过【2.1 推测webApplicationType类型】属性,通过类路径名拿到class,通过反射构造函数初始化。以此创建对应的应用上下文容器。
// 确定上下文容器类型
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);
}
// 反射初始化
return BeanUtils.instantiateClass(contextClass);
3.9 加载异常处理器
初始化过程中第四次用到SPI,初始化方式和上述 「2.2通过SPI初始化初始化器」一样,不过类型换为。SpringBootExceptionReporter.class。在应用程序启动失败时,广播【事件7】
// 加载异常处理器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
3.10 准备上下文 prepareContext
prepareContext方法作为容器刷新前的准备操作,主要做了如下几件事
- 初始化BeanName生成器
- spi获取的初始化器开始执行
- 发送容器上下文初始化事件【事件3】
- 注册启动参数bean,这里将容器指定的参数封装成bean,注入容器
- 加载启动指定类,将启动类注入容器
- 发送容器准备完成事件【事件4】
/**
* desc : 准备上下文
*/
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
// 初始化BeanName生成器
postProcessApplicationContext(context);
// spi获取的初始化器开始执行
applyInitializers(context);
// 发送容器上下文初始化事件
listeners.contextPrepared(context);
。。。。。。
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//注册启动参数bean,这里将容器指定的参数封装成bean,注入容器
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
。。。。。。
// **加载启动指定类,将启动类注入容器**
load(context, sources.toArray(new Object[0]));
// 发送容器准备完成事件
listeners.contextLoaded(context);
}
3.11 刷新上下文 refreshContext
这里看过spring源码的同学应该就很熟悉,spring的refresh那一套逻辑。核心做了容器初始化,自动装配。内容过多,有兴许可见此文章,这里就不单独展开描述了。
3.12 刷新后操作
springboot预留扩展点,目前无业务逻辑,可通过重写方法进行后置扩展
/**
* Called after the context has been refreshed.
* @param context the application context
* @param args the application arguments
*/
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
3.13 记录启动时间,广播事件
到此节点springboot启动过程基本已经结束了,记录并打印启动耗时,并发布应用程序启动完成事件【事件5】
// 记录启动时间
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 事件广播 启动完成了
listeners.started(context);
3.14 执行后置runner
这里也是一个拓展点,用户可以自定义ApplicationRunner 或者 CommandLineRunner类型的实现类,并交由spring托管,这样spring容器启动后就会在此节点调用。
/**
* desc : 执行后置runner
*/
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
// 获取ApplicationRunner类型的实现类
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// CommandLineRunner
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);
}
}
}
3.15 最后:应用程序就绪事件:ApplicationReadyEvent
事件发布应用程序就绪事件:ApplicationReadyEvent 【事件6】
try {
// 事件广播 容器运行了
listeners.running(context);
}
4.总结
最后总结下springboot启动过程。
结论1:启动类内部逻辑核心做了两件事
- new 一个SpringApplication(启动类.class)对象
- 调用他的run方法,传入启动类上的args
其中new 一个SpringApplication(启动类.class)对象:
结论2:new 一个SpringApplication(启动类.class)对象的核心操作
- 确认程序的应用类型
- SPI初始化,解析META-INF/spring.factories 文件
- 初始化初始化器 ApplicationContextInitializer
- 初始化监听器 ApplicationListener
- 设置程序运行的主类
run方法如下:
结论3:SpringApplication的run方法核心操作
- 定义时间Watch,并记录当前时间
- 设置系统属性java.awt.headless,不用显示器
- 获取应用程序运行监听器监听器,监听器启动
- 获取参数持有类
- 准备环境信息
- 配置忽略Bean信息
- 打印Banner信息
- 创建应用上下文容器
- 加载异常处理器
- 刷新容器前准备操作
- 刷新上下文,容器初始化,自动装配 - spring 初始化那一坨
- 刷新后操作
- 记录启动时间,广播事件
- 执行后置runner
全览:
5.作者感言
完结撒花~
能看到这里的小伙伴真的是很有耐心。身边有的朋友和我说学习源码的过程十分枯燥和无趣,不过每个技术人其实都需要一定的自我驱动能力。并且在枯燥的的学习过程中。找到自己一个又一个”满足点“。当你积累一定的痛苦后,后头看看,可能已经比别人多走了很远。
其实每一个人写代码都有自己的风格与习惯。多去看看开源优秀的源码,从源码中学到他们的设计和想法,然后再对比一下自己的代码,这样就会找到前进的方向。
最后综上所述,springboot的启动过程源码剖析就结束了。笔者再次建议大家打开源码,debug多走几遍,加深对springboot的理解。
「点赞,收藏,评论 三连走一走,谢谢各位Thanks♪(・ω・)ノ」
需要注释源码的可以留下邮箱,我私聊发你~~~~