【springboot源码解读系列】(一、springboot创建SpringApplication实例,定制SpringApplication)

源码地址: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实例的时候,他做了这些初始化工作。
根据上面的注释看下来,这里想说明两点:

  1. 在调用run方法之前,我们可以定制实例
  2. 通过调用栈推断主入口函数

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

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值