Spring Boot原始碼分析-啟動過程

1ishile發表於2019-04-17

Spring Boot作為目前最流行的Java開發框架,秉承“約定優於配置”原則,大大簡化了Spring MVC繁瑣的XML檔案配置,基本實現零配置啟動專案。

本文基於Spring Boot 2.1.0.RELEASE版本瞭解Spring Boot如何啟動

首先讓我們看一下最簡單的Spring Boot啟動程式碼

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
複製程式碼

每一個使用過Spring Boot的同學對於上面的程式碼應該都非常熟悉了,通過這段程式碼即可啟動Spring Boot應用。那麼SpringApplication.run(DemoApplication.class, args)內部到底做了什麼事情呢?

在檢視具體程式碼之前,我們先了解一下SpringApplication內部大概的執行流程,如下圖

執行流程

從上圖中可以看出run()是整個應用的入口,接著初始化SpringApplicationRunListenerEnvironment等例項,然後建立應用上下文物件,“準備”並“重新整理”上下文,到這裡Spring容器已基本啟動完成,最後傳送事件通知各個元件作出相應動作。

原始碼分析

在瞭解完大概的流程之後,下面開始深入原始碼分析Spring Boot具體的啟動過程,首先進入入口方法run

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    // ...
複製程式碼

StopWatch主要是用來統計每項任務執行時長,例如Spring Boot啟動佔用總時長。

Started DemoApplication in 4.241 seconds (JVM running for 5.987)

getRunListeners()完成了SpringApplicationRunListener例項化工作,如何完成的呢?進入方法內部檢視

private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
				SpringApplicationRunListener.class, types, this, args));
}
複製程式碼

SpringApplicationRunListenersSpringApplicationRunListener不是同一個類,它們名稱非常相似

檢視SpringApplicationRunListeners原始碼

SpringApplicationRunListeners(Log log,
			Collection<? extends SpringApplicationRunListener> listeners) {
	this.log = log;
	this.listeners = new ArrayList<>(listeners);
}

public void starting() {
	for (SpringApplicationRunListener listener : this.listeners	 {
		listener.starting();
	}
}
public void environmentPrepared() {
	// ....
}
public void contextPrepared() {
	// ....
}
public void contextLoaded() {
	// ....
}
public void started() {
	// ....
}
public void running() {
	// ....
}
複製程式碼

它是SpringApplicationRunListener的一個集合

觀察SpringApplicationRunListeners所有方法,可以看出,它實際是一個用來傳送SpringApplicationRunListener相關事件的工具類

接著繼續觀察getSpringFactoriesInstances原始碼,看它是如何例項化物件的(此方法後續多處使用)

private <T> Collection<T> getSpringFactoriesInstance(Class<T> type,
		Class<?>[] parameterTypes, Object... args) {
	ClassLoader classLoader = getClassLoader();
	// 載入物件名稱
	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對應的FactoryNames,不明白有什麼用處?進入方法內部檢視

public static List<String> loadFactoryNames(Class<?>factoryClass, @Nullable ClassLoader classLoader) {
	String factoryClassName = factoryClass.getName();
	return loadSpringFactories(classLoader).getOrDefaul(factoryClassName, Collections.emptyList());
}
複製程式碼

繼續進入loadSpringFactories方法內部

public static final String FACTORIES_RESOURCE_LOCATION ="META-INF/spring.factories";
private static Map<String, List<String>> loadSpringFactorie(@Nullable ClassLoader classLoader) {
	MultiValueMap<String, String> result = cache.ge(classLoader);
	if (result != null) {
		return result;
	}
	try {
		// 獲取 META-INF/spring.factories 對應的資源
		Enumeration<URL> urls = (classLoader != null ?
				classLoader.getResource(FACTORIES_RESOURCE_LOCATION) :
				ClassLoader.getSystemResource(FACTORIES_RESOURCE_LOCATION));
		result = new LinkedMultiValueMap<>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			UrlResource resource = new UrlResource(url);
			// 讀取檔案內容
			Properties properties =PropertiesLoaderUtils.loadProperties(resource);
			for (Map.Entry<?, ?> entry : properties.entrySe()) {
				String factoryClassName = ((String)entry.getKey()).trim();
				for (String factoryName :StringUtils.commaDelimitedListToStringArray(String) entry.getValue())) {
					// 獲取 factoryClassName 對應的多個valu(多個value用逗號分隔)
					result.add(factoryClassName,factoryName.trim());
				}
			}
		}
		// 快取已經讀取到的內容
		cache.put(classLoader, result);
		return result;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to loadfactories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}
複製程式碼

看到這裡可能會疑惑META-INF/spring.factories檔案在哪裡?檔案裡面有什麼內容?

其實這個檔案存放在Spring BootSpring Boot autoconfigure的jar包內部(有興趣的同學可以自行下載jar包並解壓檢視),Spring Boot中的檔案內容如下:

# 完整內容請檢視原檔案

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
複製程式碼

可以看到SpringApplicationRunListener對應的值是EventPublishingRunListener

回到SpringFactoriesLoader.loadFactoryNames方法內部,可以發現方法獲取的值實際上是factoryClassMETA-INF/spring.factories中對應的實現類的集合

明白這個方法之後,再回到getSpringFactoriesInstances方法

private <T> Collection<T> getSpringFactoriesInstance(Class<T> type,
		Class<?>[] parameterTypes, Object... args) {
	ClassLoader classLoader = getClassLoader();
	// 獲取 SpringApplicationRunListener 對應的實現類的名稱集合
	Set<String> names = new LinkedHashSet<>(
			SpringFactoriesLoader.loadFactoryNames(type,classLoader));
	// 通過反射例項化物件
	List<T> instances = createSpringFactoriesInstances(type parameterTypes,
			classLoader, args, names);
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}
複製程式碼

到此為止getRunListeners完成了SpringApplicationRunListener對應實現類的例項化,並回撥其starting方法

SpringApplicationRunListeners listeners getRunListeners(args);
listeners.starting();
複製程式碼

從上面分析得知,實際上呼叫的是EventPublishingRunListenerstarting方法,那麼方法內部做了什麼呢?

public void starting() {
	this.initialMulticaster.multicastEvent(
			new ApplicationStartingEvent(this.application,this.args));
}
複製程式碼

傳送了一個ApplicationStartingEvent事件

繼續查詢ApplicationStartingEvent事件的消費者,從spring.factories中可以找到所有預定義的事件消費者

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
複製程式碼
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
複製程式碼

接下來要做的就是從這些消費者中找出ApplicationStartingEvent事件的消費者(查詢過程省略),找到以下兩個消費者

  • LoggingApplicationListener 初始化日誌系統

  • LiquibaseServiceLocatorApplicationListener (引數liquibase.servicelocator.ServiceLocator)如果存在,則使用springboot相關的版本進行替代

瞭解完ApplicationStartingEvent事件之後,回到run方法繼續往下探究prepareEnvironment

private ConfigurableEnvironment prepareEnvironment(
		SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments) {
	// 建立Environment物件
	ConfigurableEnvironment environment =getOrCreateEnvironment();
	configureEnvironment(environment,applicationArguments.getSourceArgs());
	// 釋出ApplicationEnvironmentPreparedEvent事件
	listeners.environmentPrepared(environment);
	bindToSpringApplication(environment);
	if (!this.isCustomEnvironment) {
		environment = new EnvironmentConverte(getClassLoader())
				.convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
	}
	ConfigurationPropertySources.attach(environment);
	return environment;
}
複製程式碼

這裡又釋出了一個ApplicationEnvironmentPreparedEvent事件,繼續查詢事件監聽物件

  • FileEncodingApplicationListener 檢查系統檔案編碼格式是否符合環境變數中配置的檔案編碼格式(如果存在相關設定 - spring.mandatory-file-encoding),如果編碼不符合,則丟擲異常阻止Spring啟動
  • AnsiOutputApplicationListener 是否開啟AnsiOutput
  • DelegatingApplicationListener 代理context.listener.classes配置的監聽者
  • ClasspathLoggingApplicationListener 日誌輸出classpath
  • LoggingApplicationListener 配置日誌系統,logging.config, logging.level...等
  • ConfigFileApplicationListener 這是一個比較重要的監聽物件,具體的方法實現如下
private void onApplicationEnvironmentPreparedEvent(
		ApplicationEnvironmentPreparedEvent event) {
	List<EnvironmentPostProcessor> postProcessors =loadPostProcessors();
	postProcessors.add(this);
	AnnotationAwareOrderComparator.sort(postProcessors);
	for (EnvironmentPostProcessor postProcessor :postProcessors) {
		postProcessor.postProcessEnvironmen(event.getEnvironment(),
				event.getSpringApplication());
	}
}

List<EnvironmentPostProcessor> loadPostProcessors() {
	return SpringFactoriesLoader.loadFactorie(EnvironmentPostProcessor.class,
			getClass().getClassLoader());
}
複製程式碼

通過spring.factories,可以看到這裡載入以下EnvironmentPostProcessor物件

  • CloudFoundryVcapEnvironmentPostProcessor
  • SpringApplicationJsonEnvironmentPostProcessor
  • SystemEnvironmentPropertySourceEnvironmentPostProcessor
  • ConfigFileApplicationListener

很多同學可能會疑問ConfigFileApplicationListener並不存在spring.factories檔案中,這裡為什麼會有它呢?

實際上ConfigFileApplicationListeneronApplicationEnvironmentPreparedEvent方法中,將自身新增到EnvironmentPostProcessor物件列表中。

我們主要關注ConfigFileApplicationListenerpostProcessEnvironment方法

public void postProcessEnvironment(ConfigurableEnvironmentenvironment,
		SpringApplication application) {
	addPropertySources(environment,application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironmentenvironment,
		ResourceLoader resourceLoader) {
	RandomValuePropertySource.addToEnvironment(environment);
	// 讀取applicaiton.yml, application.properties等配置檔案
	new Loader(environment, resourceLoader).load();
}
複製程式碼

ConfigFileApplicationListener監聽到ApplicationEnvironmentPreparedEvent事件之後開始讀取本地配置檔案

關於Spring如何讀取本地配置檔案,請前往Spring Boot原始碼分析-配置檔案載入原理

建立ApplicationContext物件

protected ConfigurableApplicationContextcreateApplicationContext() {
	Class<?> contextClass = this.applicationContextClass;
	if (contextClass == null) {
		try {
			// 根據webApplicationType建立對應上下文物件
			switch (this.webApplicationType) {
			case SERVLET:
				contextClass = Class.forNam(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
				break;
			case REACTIVE:
				contextClass = Class.forNam(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
				break;
			default:
				contextClass = Class.forNam(DEFAULT_CONTEXT_CLASS);
			}
		}
		catch (ClassNotFoundException ex) {
			throw new IllegalStateException(
					"Unable create a defaultApplicationContext, "
							+ "please specify anApplicationContextClass",
					ex);
		}
	}
	return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
}
複製程式碼

這裡是根據webApplicationType決定建立什麼型別的ApplicationContext物件,那麼webApplicationType是何時賦值的呢?

public SpringApplication(ResourceLoader resourceLoader,Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must notbe null");
	this.primarySources = new LinkedHashSet<>(Arrays.asLis(primarySources));
	// 初始化webApplicationType
	this.webApplicationType =WebApplicationType.deduceFromClasspath();
	setInitializers((Collection) getSpringFactoriesInstance(
			ApplicationContextInitializer.class));
	setListeners((Collection) getSpringFactoriesInstance(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass(;
}
複製程式碼

從上面可以看出是通過WebApplicationType.deduceFromClasspath方法初始化的webApplicationType,繼續跟蹤程式碼

private static final String WEBFLUX_INDICATOR_CLASS = "org."
			+ "springframework.web.reactive.DispatcherHandler";
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework."
			+ "web.servlet.DispatcherServlet";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
			"org.springframework.web.context.ConfigurableWebApplicationContext" };

static WebApplicationType deduceFromClasspath() {
	if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, 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;
}
複製程式碼

從上面程式碼中可以看出Spring是通過當前classpath下是否存在相應的類,從而決定webApplicationType型別

初始化ApplicationContext物件

private void prepareContext(ConfigurableApplicationContextcontext,
		ConfigurableEnvironment environment,SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments, BannerprintedBanner) {
	// 初始化context
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	applyInitializers(context);
	// 傳送ApplicationContextInitializedEvent訊息
	listeners.contextPrepared(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}
	// Add boot specific singleton beans
	ConfigurableListableBeanFactory beanFactory =context.getBeanFactory();
	beanFactory.registerSingleto("springApplicationArguments", applicationArguments);
	if (printedBanner != null) {
		beanFactory.registerSingleton("springBootBanner",printedBanner);
	}
	if (beanFactory instanceof DefaultListableBeanFactory) {
		((DefaultListableBeanFactory) beanFactory)
				.setAllowBeanDefinitionOverridin(this.allowBeanDefinitionOverriding);
	}
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	// 註冊DemoApplication
	load(context, sources.toArray(new Object[0]));
	listeners.contextLoaded(context);
}
複製程式碼

這裡註冊了DemoApplicationSpring容器中,為後續bean掃描做準備

接下來繼續深入refreshContext方法,可以發現實際上是執行了AbstractApplicationContext.refresh方法

public void refresh() throws BeansException,IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		prepareRefresh();
		ConfigurableListableBeanFactory beanFactory =obtainFreshBeanFactory();
		prepareBeanFactory(beanFactory);
		try {
			postProcessBeanFactory(beanFactory);
			// 完成bean的載入
			invokeBeanFactoryPostProcessors(beanFactory);
			registerBeanPostProcessors(beanFactory);
			initMessageSource();
			initApplicationEventMulticaster();
			onRefresh();
			registerListeners();
			finishBeanFactoryInitialization(beanFactory);
			finishRefresh();
		}
		catch (BeansException ex) {
			if (logger.isWarnEnabled()) {
				logger.warn("Exception encountered duringcontext initialization - " +
						"cancelling refresh attempt: " + ex;
			}
			destroyBeans();
			cancelRefresh(ex);
			throw ex;
		}
		finally {
			resetCommonCaches();
		}
	}
}
複製程式碼

refresh方法內部做了很多事情。比如:完成BeanFactory設定,BeanFactoryPostProcessorBeanPostProcessor介面回撥,Bean載入,國際化配置等。

到此為止Spring基本完成了容器的初始化工作,最後在呼叫callRunners方法,執行ApplicationRunnerCommandLineRunner介面。

private void callRunners(ApplicationContext context,ApplicationArguments args) {
	List<Object> runners = new ArrayList<>();
	runners.addAll(context.getBeansOfTyp(ApplicationRunner.class).values());
	runners.addAll(context.getBeansOfTyp(CommandLineRunner.class).values());
	AnnotationAwareOrderComparator.sort(runners);
	for (Object runner : new LinkedHashSet<>(runners)) {
		if (runner instanceof ApplicationRunner) {
			callRunner((ApplicationRunner) runner, args);
		}
		if (runner instanceof CommandLineRunner) {
			callRunner((CommandLineRunner) runner, args);
		}
	}
}
複製程式碼

整個啟動過程的核心方法是refresh,此方法內部承載大部分容器啟動所需的工作。由於篇幅原因,後續再進行refresh內部原始碼分析,瞭解Spring Boot載入Bean的整個過程。

相關文章