文章目录
SpringBoot源码分析(一):SpringApplication的构造函数
网上看了一些SpringBoot自动配置的博客,大部分都是从@SpringBootApplication注解讲到@EnableAutoConfiguration然后再讲到@Import….等内容,但是我看完以后还是一直有一个疑问,为什么加了这些个注解就可以自动配置了,所以就像从Springboot的启动类开始,一步步探究一下Springboot到底是怎么做到自动配置的。
SpringApplication
在使用Springboot的时候,标准的一个流程是,添加@SpringBootApplication注解,然后在main函数里面调用SpringApplication.run()方法启动一个Springboot应用。如下:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
那么,我们今天就来看一看这个SpringApplication究竟做了那些事情。
SpringApplication的文档如下:
一个被用作从Java 主方法运行和引导一个Spring应用的类,默认情况下,类将会以下面几个步骤去运行你的应用
- 根据你应用的classpath创建一个合适的 ApplicationContext 实例
- 注册CommandLinePropertySource来解释命令行参数作为Spring的参数
- 刷新应用上下文,加载所有的单例Bean
- 触发CommandLineRunner bean
可以看的出来,SpringApplication目的就是引导和运行一个SpringBoot应用。
启动Springboot Application
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);
}
在启动Springboot应用的时候,初始化了一个SpringApplication实例,然后调用的它的run方法,这里就先来看一下它的构造函数做了那些事情
SpringApplication的构造方法
创建一个Application实例,应用上下文将从指定的主来源加载bean
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 初始化资源加载器(默认情况下为null)
this.resourceLoader = resourceLoader;
// 初始化主数据源(这个数据源默认情况下就是我们的主类的class对象
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 推断web应用程序的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 加载初始化器,这些初始化器将用于初始化ApplicationContext
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 加载listener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 推断应用的主入口类(包含main方法的类)
this.mainApplicationClass = deduceMainApplicationClass();
}
SpringApplication的构造方法主要的步骤如上面代码所示,主要做了如下几件事情
1. 推断应用类型(REACTIVE、SERVLET、NONE)
// 推断web应用程序的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
怎么推断出来应用是属于哪一种Web类型呢?具体来看WebApplicationType.deduceFromClassPath();
// WebApplicationType#deduceFromClasspath
static WebApplicationType deduceFromClasspath() {
// 如果存在 org.springframework.web.reactive.DispatcherHandler, 不存在 org.springframework.web.servlet.DispatcherServlet, 并且不存在 org.glassfish.jersey.servlet.ServletContainer 那么web类型为 Reactive
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
// 如果列表中不包含servelet类型服务中需要的任何一个类,那么这个项目就不属于web项目
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
// 其他情况下为servlet类型项目
return WebApplicationType.SERVLET;
}
这里最主要的就是ClassUtils.isPresent(className, classLoader) 这个方法了,这个方法也很简单,根据类路径获取类,如果获取不到(ClassNotFoundException或NoClassDefFoundError)就返回false, 可以实例化成功就返回true,那这里的意图也就很明显了,这里就是根据判断当前的环境中有没有包含对应的类进而判断我们的应用程序是属于哪一种应用类型。
2. 获取初始化器列表
// 获取初始化器列表
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
这里调用了getSpringFactoriesInstances方法来获取初始化器列表,这些初始化器列表将会在Springboot初始化的时候被调用。
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) {
// 获取classLoader
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 类路径列表
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 创建实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
在上面的方法中,最重要的是SpringFactoriesLoader.loadFactoryNames(type, classLoader)函数,它通过扫描所有的jar包下面的 META-INF/spring.factories 然后读取其中的类列表,的它的代码如下:
SpringFactoriesLoader#loadFactoryNames(Class<?> factoryType, ClassLoader classLoader)
/**
* 从 spring.factories 中加载类列表,并且找到属于factoryType的列表
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// factoryTypeName
String factoryTypeName = factoryType.getName();
// 现将内容加载到一个map中,再从map中加载列表,相当于一个缓存
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 缓存中有值的话直接从缓存中取值
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 加载到当前应用的所有的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);
// spring.factories 文件其实就是一个propries文件
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// factoryTypeName: 其实就是我们上面的 Class<?> factoryType 的类路径
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
Springboot的精髓就在这个位置了,Springboot是怎么做到自动配置的?以前我就很纳闷,为什么引入Mybatis框架以后Springboot就可以自动装载Mybatis呢,那些个框架中的自动配置类为什么会被Springboot给调用到呢?
其实就是通过这个方法扫描到的自动配置类的配置文件,而这个配置文件很多有关Springboot自动配置的文章也都描述过了,也就是 META-INF/spring.factories。而且,Springboot不仅仅是会扫描spring-boot-autoconfigure下面的spring.factories文件,而是我们项目中引用的所有的jar包Springboot都会进行扫描,找到所有的spring.factories文件。
通过debug,我们可以看到,上面这行代码扫描到的列表其实是:
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
由于我的项目里面用到了activiti和mybatis,所以,可以看到,除了Spring相关的spring.factories被扫描到了,activiti的配置文件以及mybatis的配置文件也被扫描到了。
我们具体来看一下spring.factories 的文件内容,下面是spring-boot-autoconfigure的
而 spring.factories 的key就是我们想要读取的factoryType的类路径。Spring就是通过这种配置文件的方式,将第三方的自动配置类配置在第三方的配置文件中,Spring加载的时候读取这些配置,然后实例化这些类进行调用。
后面的代码就比较简单了,根据扫描到的类列表,初始化类实例,然后根据类是否实现了Order接口,或者是否被@Order注解标识,对实例进行排序。
// 创建实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 排序
AnnotationAwareOrderComparator.sort(instances);
3. 初始化listener列表
// 加载listener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
下面的 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 和
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));逻辑一样,都是从各个配置文件中读取到相应的ApplicationListener列表,然后进行实例化和排序。
4. 推断程序入口类
deduceMainApplicationClass
这个就比较简单了,初始化一个异常获取到StackTrace,然后根据StackTrce中获取到类列表,判断哪一个类中有main方法就可以推断出哪一个类是应用的入口类了。
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
总结
SpringApplication的构造函数的最主要的功能就是扫描各个jar包下面的spring.factories,将各jar下面中配置的初始化器和listener实例化,在之后的Springboot程序启动过程中进行调用。这也就说明了为什么某些第三方的Springboot项目例如mybatis, activiti…, Springboot可以自动的进行加载和配置。
扩展
仅仅从SpringApplication的代码中,我们就可以看到如果我们自己写一个第三方的库,并且需要利用到Springboot的自动配置该怎么做呢?其实就是可以自己按照上面的逻辑,自己写初始化器和Listener(实现ApplicationContextInitializer接口和实现ApplicationListener接口),然后创建spring.factories文件将我们自己实现的类配置到配置文件中打包为jar包,那么Springboot就会自动调用我们写的逻辑了。如下:
实现很简单,打印一些信息即可
/**
* @author Mengfly
* @date 2021/6/20 17:17
*/
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println(getClass().getName() + " 我是自定义的ApplicationContextInitializer");
}
}
/**
* @author Mengfly
* @date 2021/6/20 17:19
*/
public class MyApplicationListener implements ApplicationListener<SpringApplicationEvent> {
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
System.out.println(getClass().getName() + " 我是自定义的ApplicationListener");
}
}
然后,打包为jar包,在我们的项目中引用这个jar包,运行项目即可看到我们自定义的MyApplicationContextInitializer和MyApplicationListener被调用了
只不过目前会被调用多次,为什么会这样还要慢慢的看之后的代码才能明白。