源码地址:springboot2.x源码解读仓库,源码中包含有注释
使用过springboot的都知道,我们创建一个主启动类,然后创建一个main函数,然后SpringApplication.run(Application.class); 我们的程序就启动起来了,至于怎么启动起来的,当前是什么样的环境,我们的tomcat容器在哪里启动的等,对于使用者,无需关心。但是对于好奇心比较人来说(比如我),就像一探究竟,他是通过什么样的方式来判断我们的应用程序是什么环境的,他又是如何加载我们的bean的,tomcat容器,他又是怎么嵌入进去的。
话不多说:先看如下代码:
package com.osy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
这段代码,简单粗暴,run完我们的程序就启动完成了。
那么我们就看看他是怎么走的吧。
1、调用静态方法run函数
// 调用静态方法run函数
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
// 调用另一个重载run方法,
return run(new Class<?>[] { primarySource }, args);
}
2、调用另一个重载run方法
/**
* 静态方法启动类入口
* @param primarySources 要加载的主要源,传入主程序的Class对象
* @param args 应用程序参数(通常从java main方法传递)一般是一些jvm调优的参数设置
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
// 首先构造一个SpringApplication实例,然后再调用run方法。看看构造实例的时候他做了什么?
return new SpringApplication(primarySources).run(args);
}
1和2的这两个run方法重载,参数一个是直接传入一个Class对象,一个数传入Class对象数组,
从这里就可以看出,springboot最终需要的是一个数组,但是考虑到我们使用者,所以他写了一个重载方法,传入我们的主启动类,然后对其进行封装一遍成为数组,而且从调用来看,它的第一个run方法是专门的为我们使用者准备的,由此可以看出,spring在用户体验方面,是其他框架无法比拟的。
3、创建SpringApplication实例
public SpringApplication(Class<?>... primarySources) {
// 调用另一个构造函数
this(null, primarySources);
}
构造函数重载,真正做事的是他。
/**
* 创建一个SpringApplication实例,应用程序将会通过基本的资源(主函数下面的所有包下面进行扫描)加载bean,
* 在调用run方法之前,我们可以定制实例,比如添加初始化器,比如添加监听器等...
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 这里的resourceLoader在这里是为null的
this.resourceLoader = resourceLoader;
// 断言,判断传入的主程序入口程序的Class是否为null,如果为null,则抛出异常,校验参数是否合法
Assert.notNull(primarySources, "PrimarySources must not be null");
// 这里将初级的资源赋值,并且将主程序的Class存入。primarySources的值,就是我们的主启动类的Class对象
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 赋值web应用类型,根据classLoader去加载对应的全限定类名来进行判断当前是什么环境
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 通过SpringFactories去加载配置的spring.factories设置初始化器
// 原理:通过类加载器去加载属于ApplicationContextInitializer类型的类,然后放入initializers中,
// 也就是我们在项目中的classpath下面创建META-INF/spring.factories,指定我们实现了ApplicationContextInitializer的类,
// springboot就会加载到,当然,如果需要了解细节,我有文章写到添加初始化器。关注我,不迷路
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 设置监听器,其中的原理和加载初始化器是一样的,方法调用的都是同一个,
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 推断主应用程序类
// 通过调用栈获取所有的方法,然后找到有main函数的那个类,那么springboot就认为他就是主启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
从这个构造方法来看,构造SpringApplication实例的时候,他做了这些初始化工作。
根据上面的注释看下来,这里想说明两点:
SpringApplication实例如何定制?
SpringApplication的构造函数做的事无非就是:设置基础资源、推断应用程序的环境、通过SpringFactories加载我们自定义的初始化器和监听器、推断主入口类。然后调用run方法。
然后我们就可以对他创建实例这里进行定制:
以下是伪代码,只谈思路,不谈具体实现:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(Application.class);
// 硬编码添加初始化器
springApplication.addInitializers(new ZyInitializers());
// 硬编码添加监听器
springApplication.addListeners(new ZyListeners());
// 从新设置应用环境
springApplication.setWebApplicationType(WebApplicationType.SERVLET);
// 设置是否延迟加载bean
springApplication.setLazyInitialization(true);
...
// 开始启动
springApplication.run();
}
}
当然了,大部分情况下使用他默认的即可。如无特殊情况,建议不用定制化。
他是如何推断我们的主入口程序的,我们传入的Application.class的作用又是什么?
private Class<?> deduceMainApplicationClass() {
try {
// 通过RuntimeException实例能够获取到运行到这里的调用栈,
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
// 循环调用栈,获取方法名进行对比,如果有调用了main函数的,那么就把他当做主入口程序
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
// 通过反射获取主入口函数的Class对象
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
// 如果没有调用main函数,那么就返回null
return null;
}
从上面的注释中可以看出,他是通过RuntimeException的实例获取调用栈,然后判断方法名为main的类为主入口函数,那么我们传入的Application.class他放入了Set集合primarySources中,而他是要加载的主要源,也就是他会扫描他下面所以的包下面的所有的bean,然后加入spring容器中。
一个相信努力就会有结果的程序员,以兴趣驱动技术! ------ CoderOu