总有人替你做了一些事儿
我们常常惊叹于 Spring boot 的极大简化开发者的开发流程,将我们从苦逼的 xml 配置中释放出来,可以更加愉快欢乐的摸鱼了。
但是有没有想过几行简单的启动程序,Spring boot 是如何做到的呢?
下面这个程序可以说是最最根本的一个 Spring boot 启动类了,只需要一个 main 方法,就可以让我们只关注业务的开发。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloSpringBootStarterTestApplication {
public static void main(String[] args) {
SpringApplication.run(HelloSpringBootStarterTestApplication.class, args);
}
}
1. SpringBoot启动阶段划分
关于 Spring boot 启动过程,其实可以分为两部分
1、Spring boot 的配置过程。【主要是SpringApplication类的构造方法】
2、Spring boot 启动过程。【真正核心的启动类】
2. Spring boot 的配置过程
首先会调用 SpringApplication() 实例化方法,主要是创建 SpringApplication 实例对象,该方法主要是调用重载的构造器。
// 该方法只是调用重载的实例化构造器
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
接下来加载一些系统资源,这里是加载用户自定义配置的主要入口,主要流程主要分为四步:
1、判断应用的类型,NON,SERVLET,REACTIVE 类型,正常情况下,我们这里都是 SERVLET 类型的服务。
2、去spring.factories找 ApplicationContextInitializer,主要是读取该文件信息 spring-boot-2.3.12.RELEASE.jar!/META-INF/spring.factories
,凡是在该文件中订单的初始化器,此时都会被读取到,这里通过 debug 发现主要有 7个容器初始化器。
以后凡是这种 getSpringFactoriesInstances()
(下面会仔细分析这个方法)都是需要从 对应的 spring.factories 文件中寻找初始化器(比如容器初始化、监听器初始化)
3、同理。去 spring.factories
找 监听器,获取的思路与 Initializer 一致。
4、寻找有 main 方法的类,这里就可以寻找到我们主程序类了。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 1. WebApplicationType是枚举类,有NONE,SERVLET,REACTIVE,下行webApplicationType是SERVLET
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 2. 去spring.factories找 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 3. 去spring.factories找 监听器,获取的思路与Initializer一致
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 4. 寻找有main方法的类,这里就可以寻找到我们主程序类了。
this.mainApplicationClass = deduceMainApplicationClass();
}
1. 绕不开的getSpringFactoriesInstances()
主要是加载各种提前预置的初始化器,主要思路如下:
1、 ApplicationContext 场景下的获取类加载器 ,如何获取呢?本质是通过调用 ClassUtils.getDefaultClassLoader(); 获取默认类加载器
2、获取各种配置到 spring.factories 中的初始化器,并且用 set 集合存储,防止重复
3、根据上一步获取的名称开始创建初始化器。实现方式是通过 BeanUtils.instantiateClass() 方式来创建。
4、根据各个初始化器上的 Order 注解对初始化器进行排序。
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) {
// 1. ApplicationContext 场景下的获取类加载器
ClassLoader classLoader = getClassLoader();
// 2. 获取各种配置到 spring.factories 中的初始化器,并且用 set 集合存储
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 3. 根据上一步获取的名称开始创建初始化器
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 4. 对初始化器进行排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
绘制一个简单的时序图来说明 Spring boot
在启动之前如何进行配置的
3. 运行 Spring应用的开始
其实前面说了这么多只是介绍 Spring 的配置过程,下面才开始真正的程序运行。
这里的运行包括了应用程序的运行和创建及初始化容器的过程(Spring 容器初始化过程),我将大致的启动流程划分为
开始的运行。
设置 java.awt.headless 属性,作用目前未知。
获取所有 SpringApplicationRunListener(运行监听器)【为了方便所有 Listener 进行事件感知】**
调用 starting() 表明,容器开始启动了
将在容器中使用的参数(一般通过启动命令传入的参数)包装成 ApplicationArguments 对象,
准备环境基本配置
返回或者创建基础环境信息对象
决定用哪个配置文件以及读取配置文件值
监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成
绑定环境信息
打印banner(也就是每次启动的时候打印的 Spring boot 标识)
创建IOC容器【到了启动的重点了】到这一步还只是创建的一个空的容器
根据项目类型(Servlet)创建容器
主要是创建 ConfigurableApplicationContext 容器
准备容器信息
遍历所有的 ApplicationContextInitializer (还记得我们开头说的从spring.factories 中加载的7个初始化器吗?就是在这一步进行每一个初始化的)。调用 initialize.。来对ioc容器进行初始化扩展功能
保存环境信息
IOC容器的后置处理流程(Spring 容器的中的经典 postProcessApplicationContext()方法就是发生在这一步)
开始初始化所有初始化器;applyInitializers;
遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器contextPrepared
打印启动日志,这一步同样可配置
从容器中获取 beanFactory(熟悉Spring的同学应该只是,这其实就是一个更加简易版本的 ApplicationContext),并且顺带将 命令行传入的参数作为一个 bean 组件注入到 ConfigurableListableBeanFactory 中。
所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded 事件;
刷新IOC容器。这一步到了Spring 底层的 refresh() 方法了,
AbstractApplicationContext#refresh()
方法主要是刷新容器这里的内容过长,后续做一篇文章单独分析,只需要知道经过这一步,基本上准备好了所有的容器的内容了。
所有监听 器 调用 listeners.started(context); 通知所有的监听器容器已经成功started
调用所有runners
获取容器中的 ApplicationRunner
获取容器中的 CommandLineRunner
合并所有runner并且按照@Order进行排序
遍历所有的runner。调用 run 方法,该方法适合于用一些容器启动时顺便做的一次性工作
调用 listeners 的
running()
方法,通知所有的监听器,容器正在启动。
到这里,整个 Spring boot
应用算是完整启动了。
源码简单浏览
public ConfigurableApplicationContext run(String... args) {
// 1. 开始的运行计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
// 2.设置 java.awt.headless 属性,作用目前未知。
configureHeadlessProperty();
// 3. 创建 SpringApplicationRunListeners ,本质还是将 /META-INF/spring.factories 中的监听器包装成 SpringApplicationRunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);
// 4. 调用 starting() 表明,容器开始启动了
listeners.starting();
try {
// 5. 将在容器中使用的参数(一般通过启动命令传入的参数)包装成 ApplicationArguments 对象,
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 6. 准备环境基本配置(包括决定用哪个配置文件、)
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
// 7. 打印banner(也就是每次启动的时候打印的 Spring boot 标识)
Banner printedBanner = printBanner(environment);
// 8. 创建IOC容器【到了启动的重点了】到这一步还只是创建的一个空的容器
context = createApplicationContext();
// 9. 准备容器信息
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 10. 刷新IOC容器
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 11. 通知所有的监听器容器已经成功started
listeners.started(context);
// 12. 调用所有runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
// 13. 告知所有的监听器,容器已经启动了,正在运行
listeners.running(context);
// 这里暂时忽略不需要关心的异常处理情况
return context;
}
总结
分析到这里,Spring boot 的启动流程只能说大致分析完成了,这里由于篇幅及避免过于陷于细节,只是分析了主干流程,其实每个流程如果拆开说可以分出多篇文章,比如第十步的刷新容器过程(refreshContext(context)
) ,算是 Spring 源码的核心了,里面包括了容器如何初始化的、如何设置各个组件等,后续会单独给出分析文章。
关于 Spring、Spring boot 的书籍还是再次推荐《Spring揭秘》,虽然该书讲的知识已经比较久远,并且不再更新,但是无论是其描述问题的方式还是娓娓道来探索方式,都对得起『揭秘』二字。