思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
对于SpringBoot
中run
方法的分析网上已经有很多教程了,本文想试着从全局出发,重新梳理SpringBoot
中run
方法的整体脉络,使你不至于过分专注细节而迷失在run
方法繁琐的调用逻辑中。
前言
SpringBoot
是基于Spring
开发的一种轻量级的全新框架,不仅继承了Spring
框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring
应用的整个搭建和开发过程。
开发者可以通过SpringBoot
轻松地创建独立的,基于生产级别的基于Spring
的应用程序。SpringBoot
的出现极大的降低了开发应用的难度。除此之外,SpringBoot
还具有如下特点:
-
可独立运行的
Spring
项目:Spring Boot
可以以jar
包的形式独立运行。 -
内嵌的
Web
容器:Spring Boot
可以选择内嵌Tomcat
、Jetty
或者Undertow
,无须以war
包形式部署项目。 -
简化的
Maven
配置:Spring
提供推荐的基础POM
文件来简化Maven 配置。 -
自动配置:
SpringBoot
会根据项目依赖来自动配置Spring、SpringMVC
等框架,极大地减少项目要使用的配置。
构建SpringBoot
项目
如果你曾经有过Spring、SpringMVC
的开发经验,你一定会对其中繁琐的xml
配置感到深恶痛绝,而SpringBoot
的出现恰好可以解决这一痛点问题,SpringBoot
可以让你更加快捷的搭建一个企业级应用。具体代码如下:
java
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* SpringBoot应用启动类
* */
@SpringBootApplication
public class LearnSpringBootApplication {
public static void main(String[] args) {
// 启动SpringBoot
SpringApplication.run(LearnSpringBootApplication.class, args);
}
}
通过上述代码不难发现,启动一个SpringBoot
应用非常简单,我们只需如下几步:
- 提供一个
SpringBoot
的启动类LearnSpringBootApplication
(注:启动类的名称可任意指定) - 在
启动类
上标注一个@SpringBootApplication
注解 - 编写一个
main
方法,调用SpringApplication
中的run
方法
经过上述三步操作,就可以构建出一个简单的SpringBoot
应用,随后运行main
方法即可启动一个SpringBoot
的应用。
可以注意到,在main
方法中仅需调用一个run
方法就能完成SpringBoot
应用的启动。那这个run
方法背后究竟又做了那些操作呢?接下来,我们便聚焦在SpringApplication
的run
方法中,看看run
方法背后完成了那些操作。
run
方法背后的逻辑
进入到SpringApplication
的run
方法后,其最终入调用到如下的方法信息,相关代码如下所示:
SpringApplication # run
java
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
可以看到SpringApplication
中对方法进行了重载
,其最终会调用到run(Class<?>[] primarySources, String[] args)
。
此处run
方法逻辑包含了两个操作:
- 构建一个
SpringApplication
对象 - 执行
SpringApplication
对象的run
方法信息
接下来,我们将围绕上述的两个操作展开分析SpringBoot
中run
方法背后的逻辑。
创建SpringApplication
对象
java
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
java
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();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
可以看到对于SpringApplication
而言,其在构建时包含如下信息的构建:
ResourceLoader
指明资源加载器,这个暂时不用太过关注,应为其默认为null
。webApplicationType
推断当前web应用类型,可通过一个deduceFromClasspath
方法推断出的。- 随后设置了
setInitializers、setListeners
两个列表,分别是一堆Initializer
和Listener
,其都是通过getSpringFactoriesInstances
方法获取。 - 此外,还通过
primarySources、mainApplicationClass
记录了启动主要资源类,也就是之前传入的LearnSpringBootApplication.class
。
面对上述代码中诸多的陌生对象
,你肯定什么都不清楚,不知道每个变量有什么用,是干嘛的,这没有关系。因为第一次分析你只要熟悉它的脉络就可以。知道在SpringApplication
的构造方法中,会设置两个集合变量Initializer
和Listener
,了解这些就够了。
等之后你有时间,再逐个去了解每个变量或者组件的作用就可以了。事实上, SpringApplication
的创建时的细节分析 你可以慢慢拆解上面的每一步,单独看看每一个组件大体是作什么的,这就属于细节研究了。
比如,你可以研究下ResourceLoader
是什么? 通过阅读它的类注释后可以发现,ResourceLoader
类负责使用ClassLoader
加载ClassPath
下的class
和各种配置文件的。
对于webApplicationType
类型如何被推断的?其本质就是根据几个静态变量定义的类全限定名称来进行判断的。具体而言,其会判断classPath
下是否存在DispatcherServlet、DispatcherHandler
等类来推断出类型。如果使用了web-starter
则默认推断出为Servlet
类型的应用。
至于primarySources、mainApplicationClass
这个两个变量记录了启动类信息LearnSpringBootApplication.class
, 其目的在于为了后续扫描包路径信息,完成自动配置等考虑的。
至于最后两个集合变量Initializer
和Listener
如何设置的,则比较考验阅读源码的能力了。其基本原理是通过ClassLoader
扫描了classPath
下所有META-INF/spring.factories
这个目录中的文件,通过指定的factoryType
,也就是接口名称,获取对应的所有实现类,并且实例化成对象,返回成一个list
列表。 比如, factoryType=ApplicationContextInitializer
就返回这个接口在META-INF/spring.factories
定义的所有的实现类,并实例化为一个列表List ApplicationContextInitializer
。 ApplicationListener
同理。这里面其实有很多细节,大量使用了类加载器、缓存机制,反射机制等,有兴趣的话可以仔细研究下。
总结来看,上述构建SpringApplication
的过程虽然会包括很多东西,但别慌,其概括成一句话就是:设定某些属性信息,然后通过classLoader
获取classPath
下指定位置某些接口的实现类和实例对象列表。
进一步, 构建SpringApplication
时的相关逻辑如图所示:
熟悉了SpringApplication
的创建,接着我们该分析它的run(String ... args)
方法了。
SpringApplication Run
的脉络
run(String... args)
的逻辑如下:
java
public ConfigurableApplicationContext run(String... args) {
// .......省略其他无关代码
listeners.starting();
try {
// 构建一个应用参数解析器
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 加载系统的属性配置信息
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 用于控制是否忽略BeanInfo的配置
configureIgnoreBeanInfo(environment);
// 打印banner信息
Banner printedBanner = printBanner(environment);
// 创建一个容器,类型为ConfigurableApplicationContext
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 容器准备工作 (可暂时忽略)
prepareContext(context, environment, listeners,
// 解析传入参数信息
applicationArguments, printedBanner);
// 容器刷新 (重点关注)
refreshContext(context);
afterRefresh(context, applicationArguments);
}
// .......省略其他无关代码
return context;
}
我们重点分析其中的refresh
方法信息,对于refresh
而言,其调用逻辑如下所示:
(注:如果你熟悉Spring
中容器刷新的操作流程,相信你一定会秒懂图中蓝色
方框内逻辑)
进一步,上面代码虽然看着复杂, 但本质主要就是执行了一堆方法。从方法名字看出,都是围绕Context、Environment
这些术语。也就是围绕容器和配置文件组织的逻辑。 另外,SpringBoot
整个run
方法中有几个很关键扩展点,设计SpringApplicationRunListeners、Runners
等扩展入口。此外容器创建、刷新等也要各自的扩展点,对容器的增强扩展,如beanFactoryPostProcessor
,对Bean
的增加扩展,如beanPostProcessor
。
(注:原图地址:baijiahao.baidu.com/s?id=171315...)
最后通过一张图来概括上面run
方法脉络,其中:
黑色
部分直观的反映了扩展逻辑相关逻辑白色
部分是run
方法每个方法的字面理解,只是每一步有很多扩展点和做的事情比较多,让你感觉会有点云里雾里的。蓝色
部分,概括了核心逻辑。也就是SpringBoot
启动,说白了我们核心就是要找到这三点:自动装配配置、Spring容器的创建、web容器启动。
总结
如果你第一次接触SpringBoot
源码,可能会感到有些"不适"。这一定程度上是因为你过多的关注了源码细节,事实上,从细节中可以学习到知识,同时从主干脉络上也能学到知识。
而本文的主要作用是先为了梳理清楚一个run
方法的脉络信息,让你熟悉run
脉络的启动逻辑,让你清楚run
的主干逻辑,之后如果你有兴趣要继续深入研究SpringBoot
的启动逻辑只需沿着这一主干分析即可。