【springboot】初识启动流程

一、spring是如何启动的

springboot的启动代码非常简洁优雅,通常只需一个注解@SpringBootApplication和一行代码就能将应用启动起来:SpringApplication.run(App.class, args);主线程执行完后,由于有其他非daemon线程还存活着(例如tomcat的线程),所以整个应用在没有发生重启的情况下能实现7*24不间断运行。这一个run方法最后进入源码其实就是这一行代码new SpringApplication(primarySources).run(args);,可以看到主要做了两件事:

  1. 构造SpringApplication对象并初始化成员变量
  2. 进入springboot启动过程中的生命周期

二、SpringApplication的初始化

构造方法中的初始化主要进行环境信息的推断,以及spring.factories的加载和相关类的实例化。

// org.springframework.boot.SpringApplication#SpringApplication

	// 构造方法进行初始化
	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// primarySources通常是Application.class
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 推断web环境,通常是servlet环境
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		/*
		 获取ApplicationContextInitializer实例对象,也是在这里开始首次加载spring.factories文件
		 如何加载的可以查看下面SPI与spring.factories小节
		 ApplicationContextInitializer和ApplicationListener来源于spring-boot-${ver}.jar中的factories
		 找到实现类后用无参构造方法实例化对象
		*/
		setInitializers((Collection) getSpringFactoriesInstances(
				ApplicationContextInitializer.class));
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 通过当前线程栈帧找到main方法所在的类
		this.mainApplicationClass = deduceMainApplicationClass();
	}

来通过下面几个小节详细看下相应的几个方法

1. 推断环境

WebApplicationType主要有三种环境NONE、SERVLET和REACTIVE,spring通过判断类路径下是否存在某些类来推断环境类型。

若存在org.springframework.web.reactive.DispatcherHandler且不存在org.springframework.web.servlet.DispatcherServlet和org.glassfish.jersey.servlet.ServletContainer则当前web环境是REACTIVE;

若类路径不存在javax.servlet.Servlet或者org.springframework.web.context.ConfigurableWebApplicationContext则当前web环境是NONE;

其他情况则是SERVLET环境

// org.springframework.boot.WebApplicationType#deduceFromClasspath

static WebApplicationType deduceFromClasspath() {
	// 该方法比较关键,相当于Class.forName("xxx"),如果抛出ClassNotFoundException这类异常就说明不存在。
	if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", null)
			&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
			&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
		return WebApplicationType.REACTIVE;
	}
	for (String className : SERVLET_INDICATOR_CLASSES) {
		if (!ClassUtils.isPresent(className, null)) {
			return WebApplicationType.NONE;
		}
	}
	return WebApplicationType.SERVLET;
}

还有一个推断方法是deduceMainApplicationClass,这个方法用于推断拥有main方法的入口类,推断的过程也非常有趣。它是通过getStackTrace找到当前线程的栈帧,在栈帧中找到main方法及其class对象。

private Class<?> deduceMainApplicationClass() {
	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;
}

2. SPI与spring.factories

首先SPI(Service Provider Interface),即服务提供者接口,是一种机制用于发现接口的具体实现类。简单来说就是底层框架定义接口,上层应用实现接口,并通过某种规定的方式使底层框架获取到接口实现类。JDBC是Java中SPI非常典型的一个场景。
SPI-JDBC

JDBC中定义了java.sql.Driver接口,该接口的实现是由各个数据库厂商(例如MySQL、Oracle等)完成的。java.sql.DriverManager在类加载的时候利用java.util.ServiceLoader<S>来扫描jar包中实现java.sql.Driver接口的具体实现类,实现厂商需要在jar包的META-INF/services/目录下放置一个名为java.sql.Driver的文件(如上图所示),该文件中的每一行都是该接口的实现类类名,例如com.mysql.cj.jdbc.Driver。ServiceLoader读取到这个类名后,会存储起来,然后通过懒加载的方式加载这个类,即调用到ServiceLoader迭代器的next方法。具体源码可以参见java.sql.DriverManager#loadInitialDrivers方法。

spring.factories

spring则借鉴了这一思想,在应用启动时它会扫描并加载类路径下所有的"META-INF/spring.factories"文件,该文件是一个properties格式的文件,key为接口名,value为接口的实现类(可以有多个,用逗号分隔,如上图所示)。通过这种方式可以在启动过程中实现动态插入新的功能需求,降低了耦合的同时也提供了很大的扩展性,因为上层调用者只需要增加spring.factories配置文件就行了,很多自定义starter也是根据这个特性实现的。

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
	String factoryClassName = factoryClass.getName();
	return loadSpringFactories(classLoader).getOrDefault(factoryClassName, 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 {
		// 关键方法,获取类路径下包含META-INF/spring.factories的所有url
		Enumeration<URL> urls = (classLoader != null ?
				classLoader.getResources("META-INF/spring.factories") :
				ClassLoader.getSystemResources("META-INF/spring.factories"));
		// 一个value是List类型的Map
		result = new LinkedMultiValueMap<>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			UrlResource resource = new UrlResource(url);
			/* 
			这个方法比较关键,做了两件事情:
			1. new Properties(),该Properties是JDK中的类
			2. 调用Properties的load()方法将spring.factories的键值对加载进来
			*/
			Properties properties = PropertiesLoaderUtils.loadProperties(resource);
			for (Map.Entry<?, ?> entry : properties.entrySet()) {
				String factoryClassName = ((String) entry.getKey()).trim();
				for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
					result.add(factoryClassName, factoryName.trim());
				}
			}
		}
		// 将properties的内容存入缓存
		cache.put(classLoader, result);
		return result;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}

3. spring.factories类的实例化

在扫描完spring.factories中ApplicationContextInitializer和ApplicationListener的实现类后,会进入这些类的实例化阶段。源码如下,这个阶段的入参中,type为接口类,names为实现类的全类名,无参数。

// org.springframework.boot.SpringApplication#createSpringFactoriesInstances
private <T> List<T> createSpringFactoriesInstances(Class<T> type,
		Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
		Set<String> names) {
	List<T> instances = new ArrayList<>(names.size());
	for (String name : names) {
		try {
			// 获得实现类的class对象
			Class<?> instanceClass = ClassUtils.forName(name, classLoader);
			// 断言是type的派生类
			Assert.isAssignable(type, instanceClass);
			// parameterTypes为空,因此获得无参构造方法
			Constructor<?> constructor = instanceClass
					.getDeclaredConstructor(parameterTypes);
			// 相当于constructor.newInstance(args),通过反射的方法实例化对象
			T instance = (T) BeanUtils.instantiateClass(constructor, args);
			instances.add(instance);
		}
		catch (Throwable ex) {
			throw new IllegalArgumentException(
					"Cannot instantiate " + type + " : " + name, ex);
		}
	}
	return instances;
}

三、进入springboot的启动生命周期

下面这个run方法可以说是整个启动过程中最重要的一段代码了,所有生命周期的执行包括配置文件的读取和bean的装配等等都是在这里发散开来的。因此这边只做一个简单的介绍,后续在其他文章中再对细节进行探究。下面代码中有调用到getRunListeners方法,这个方法也是通过spring.factories找到SpringApplicationRunListener接口类(默认只有EventPublishingRunListener),这个接口类中定义了七个生命周期钩子方法,并贯穿在run的执行过程中。从类名中也可以发现,这个过程是观察者模式的一个实现场景,被观察的对象是每个生命周期时间点产生的事件,观察者则是一个个ApplicationListener(可在spring.factories中注册)。

  1. starting:应用刚启动时时候,也就是SpringApplication初始化完后便会执行的方法。一般在此周期内spring会初始化日志系统、启动后台预加载等。对应的事件类是ApplicationStartingEvent
  2. environmentPrepared:环境准备完的时候,一般在这个周期内,spring会加载外部配置、决定使用哪个profile的配置文件等。对应的事件类是ApplicationEnvironmentPreparedEvent
  3. contextPrepared:上下文准备完毕。对应的事件类是ApplicationContextInitializedEvent
  4. contextLoaded:上下文加载完毕。对应的事件类是ApplicationPreparedEvent
  5. started:应用启动完毕。对应的事件类是ApplicationStartedEvent
  6. running:应用正在执行。对应的事件类是ApplicationReadyEvent
  7. failed:启动过程中出现异常的时候。对应的事件类是ApplicationFailedEvent
// org.springframework.boot.SpringApplication#run(java.lang.String...)
public ConfigurableApplicationContext run(String... args) {
	// 秒表计时器
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	// 启动过程中的异常处理器
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	// 可以理解为开启非GUI模式
	configureHeadlessProperty();
	// 通过SPI方式获取SpringApplicationRunListener的实例
	SpringApplicationRunListeners listeners = getRunListeners(args);
	// 第一个生命周期回调
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(
				args);
		// 准备环境并执行第二个生命周期回调
		ConfigurableEnvironment environment = prepareEnvironment(listeners,
				applicationArguments);
		configureIgnoreBeanInfo(environment);
		// 打印springboot的横幅
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		exceptionReporters = getSpringFactoriesInstances(
				SpringBootExceptionReporter.class,
				new Class[] { ConfigurableApplicationContext.class }, context);
		// 准备上下文环境,并执行第三个、第四个生命周期回调
		prepareContext(context, environment, listeners, applicationArguments,
				printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass)
					.logStarted(getApplicationLog(), stopWatch);
		}
		// 应用启动完毕,执行第五个生命周期回调
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		// 出现异常则调用异常报告器,以及failed的生命周期回调方法
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	try {
		// 应用已经运行,执行第六个生命周期回调
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

猜你喜欢

转载自blog.csdn.net/hch814/article/details/108058494