springboot系列文章之啟動原理詳解

pjmike_pj發表於2019-03-04

前言

還是從SpringBoot的啟動類說起,這篇文章主要分析啟動類中的SpringApplication

@SpringBootApplication
public class Application {


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

可以看出main函式中重要的就是SpringApplication.run(),這可以分為兩部分來探討:

  • SpringApplication的構造過程
  • SpringApplication的run()方法

SpringApplication的初始化

首先進入SpringApplication的建構函式,先是單個引數的構造方法,後進入兩個引數的構造方法,ResourceLoader是Spring的資源載入器,這裡沒有自定義的ResourceLoader傳入,所以為NULL,而primarySources引數就是我們傳入的Application.class啟動類

public SpringApplication(Class<?>... primarySources) {
		this(null, primarySources);
	}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		//1. 推斷應用型別
		this.webApplicationType = deduceWebApplicationType();
		//2. initializer初始化模組,載入ApplicationContextInitializer
		setInitializers((Collection) getSpringFactoriesInstances(
				ApplicationContextInitializer.class));
		//3. 載入監聽器
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		//4. 配置應用main方法所在的類
		this.mainApplicationClass = deduceMainApplicationClass();
	}
複製程式碼

SpringApplication的初始化主要包括以下4個步驟:

  • 推斷應用型別
  • 載入初始化構造器ApplicationContextInitializer
  • 建立應用監聽器
  • 設定應用main()方法所在的類

1. 推斷應用型別

this.webEnvironment=deduceWebApplicationType(); 判斷應用的型別,是否是servlet應用還是reactive應用或者是none,webEnvironment中定義了這三種型別

webApplication
web

2. 載入初始化構造器ApplicationContextInitializer

setInitializers((Collection)getSpringFactoriesInstances(ApplicationContextInitializer.class)): 通過SpringFactoriesLoader在應用的classpath中查詢並載入所有可用的ApplicationContextInitializer

init

進入loadFactoryNames方法,然後進入loadSpringFactories方法,獲取當前ClassLoader下的所有META-INF/spring.factories檔案的配置資訊

而後通過loadSpringFactories(classloader).getOrDefault(factoryClassName,Collections.emptyList()) 從所有META-INF/spring.factories檔案的配置資訊的map中獲取指定的factory的值

laod
spring

預設情況下,從 spring.factories 檔案找出的 key 為 ApplicationContextInitializer 的類有如上圖中所示4種

對於 ApplicationContextInitializer,它是應用程式初始化器,做一些初始化工作

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

	/**
	 * Initialize the given application context.
	 * @param applicationContext the application to configure
	 */
	void initialize(C applicationContext);

}
複製程式碼

3. 建立應用監聽器

setListeners()方法與setInitializers()方法類似,只不過它是使用SpringFactoriesLoader在應用的classpath的META-INT/spring.factories中查詢並載入所有可用的ApplicationListener

		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

複製程式碼
a

ApplicationListener,應用程式事件(ApplicationEvent)監聽器:

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	void onApplicationEvent(E event);
}
複製程式碼

更詳細的分析可以參閱我之前的文章: springboot系列文章之啟動時初始化資料

4. 設定應用main()方法所在的類

在SpringApplication建構函式的最後一步,根據呼叫棧推斷並設定main方法的定義類

	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;
	}
複製程式碼

SpringApplication的run方法

SpringApplication例項初始化完成並且完成設定後,就可以開始run方法的邏輯了,對於這個run方法我將分為以下幾點進行逐步剖析,而StopWatch是一個工具類,主要是方便記錄程式執行時間,這裡就不仔細介紹了。

	public ConfigurableApplicationContext run(String... args) {
	        //構造一個任務執行觀察期
		StopWatch stopWatch = new
		StopWatch();
		//開始執行,記錄開始時間
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		//1
		configureHeadlessProperty();
		//2
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
	
		     //3
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			//4
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			//5
			configureIgnoreBeanInfo(environment);
			//6
			Banner printedBanner = printBanner(environment);
			//7
			context = createApplicationContext();
			//8
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			//9
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			//10
			refreshContext(context);
			//2.0版本中是空實現
			//11
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			//12
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			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;
	}
複製程式碼

SpringApplication的run方法主要分為以下幾步:

  • Headless模式設定
  • 載入SpringApplicationRunListeners監聽器
  • 封裝ApplicationArguments物件
  • 配置環境模組
  • 根據環境資訊配置要忽略的bean資訊
  • Banner配置SpringBoot彩蛋
  • 建立ApplicationContext應用上下文
  • 載入SpringBootExceptionReporter
  • ApplicationContext基本屬性配置
  • 更新應用上下文
  • 查詢是否註冊有CommandLineRunner/ApplicationRunner

1. Headless模式設定

configureHeadlessProperty()設定 headless 模式,即設定系統屬性java.awt.headless,它是J2SE的一種模式,用於在缺少螢幕,鍵盤,或者滑鼠時的系統配置,該屬性會被設定為true,更多的資訊可以參考這裡

private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";
    ...
private void configureHeadlessProperty() {
		System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
				SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
	}
複製程式碼

2. 載入SpringApplicationRunListeners監聽器

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

getRunListeners(args)也是通過 SpringFactoriesLoaderMETA-INF/spring.factories查詢到並載入的SpringApplicationRunListener。該類實際上是監聽SpringApplication的run方法的執行

	private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
				SpringApplicationRunListener.class, types, this, args));
	}
	
	.....
	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
			Class<?>[] parameterTypes, Object... args) {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// Use names and ensure unique to protect against duplicates
		//通過SpringFactoriesLoader可以查詢到並載入的SpringApplicationRunListner
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
				classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

複製程式碼

這裡的SpringApplicationRunListener監聽器與SpringApplication時載入的ApplicationListener監聽器不同,SpringApplicationRunListener是SpringBoot新增的類,SpringApplicationRunListener目前只有一個實現類EventPublishingRunListener。雖然說是新增的, 但是它們之間是有聯絡的,它們之間的的關係是通過ApplicationEventMulticaster廣播出去的SpringApplicationEvent所聯絡起來的

startup

更詳細的分析請參閱 :SpringBoot原始碼分析之SpringBoot的啟動過程

3. 封裝ApplicationArguments物件

將args引數封裝成 ApplicationArguments 物件

	public DefaultApplicationArguments(String[] args) {
		Assert.notNull(args, "Args must not be null");
		this.source = new Source(args);
		this.args = args;
	}
複製程式碼

官網對 ApplicationArguments 的解釋如下

args

4. 配置環境模組

根據listenersapplicationArguments 建立並配置當前SpringBoot應用將要使用的Enviroment

	private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (this.webApplicationType == WebApplicationType.NONE) {
			environment = new EnvironmentConverter(getClassLoader())
					.convertToStandardEnvironmentIfNecessary(environment);
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
複製程式碼

遍歷呼叫所有SpringApplicationRunListener的enviromentPrepared()方法就是宣告當前SpringBoot應用使用的Enviroment準備好了

5. 根據環境資訊配置要忽略的bean資訊

	private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
		if (System.getProperty(
				CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
			Boolean ignore = environment.getProperty("spring.beaninfo.ignore",
					Boolean.class, Boolean.TRUE);
			System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME,
					ignore.toString());
		}
	}
複製程式碼

6. Banner配置SpringBoot彩蛋

列印banner標誌,就是啟動SpringBoot專案時出現的Spring字樣,當然我們也可以自定義banner,這裡就不多說了

private Banner printBanner(ConfigurableEnvironment environment) {
		if (this.bannerMode == Banner.Mode.OFF) {
			return null;
		}
		ResourceLoader resourceLoader = (this.resourceLoader != null)
				? this.resourceLoader : new DefaultResourceLoader(getClassLoader());
		SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
				resourceLoader, this.banner);
		if (this.bannerMode == Mode.LOG) {
			return bannerPrinter.print(environment, this.mainApplicationClass, logger);
		}
		return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
	}
複製程式碼

7. 建立ApplicationContext應用上下文

createApplicationContext()根據使用者是否明確設定了applicationContextClass型別以及SpringApplication初始化階段的推斷結果,決定該為當前SpringBoot應用建立什麼型別的ApplicationContext並建立完成。

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
        + "annotation.AnnotationConfigApplicationContext";
public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot."
        + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
        "org.springframework.web.context.ConfigurableWebApplicationContext" };	
        
        
protected ConfigurableApplicationContext createApplicationContext() {
    //使用者是否明確設定了applicationContextClass,在SpringApplication中有對應的setter方法
    Class<?> contextClass = this.applicationContextClass;
        //如果沒有主動設定
    if (contextClass == null) {
        try {
            //判斷當前應用的型別,也就是之前SpringApplication初始化階段的推斷結果
            switch (this.webApplicationType) {
            //servlet應用程式
            case SERVLET: 
                contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
                break;
            //reactive響應式程式
            case REACTIVE:
                contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                break;
            //預設型別
            default:
                contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
            }
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                    "Unable create a default ApplicationContext, "
                            + "please specify an ApplicationContextClass",
                    ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
複製程式碼

在SpringBoot官網對ApplicationContext的型別是如下定義的:

Applicaiton
  • 當SpringMVC存在的時候,就使用AnnotationConfigServletWebServerApplicationContext
  • 當SpringMVC不存在的時候,Spring WebFlux響應式存在的時候,使用AnnotationConfigReactiveWebServerApplicationContext
  • 如果以上都不是,預設就用AnnotationConfigApplicationContext
  • SpringApplication存在設定ApplicationContext的方法,在JUnit測試中使用SpringApplication通常要設定ApplicationContext

8. 載入SpringBootExceptionReporter

exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
複製程式碼
	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
			Class<?>[] parameterTypes, Object... args) {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// 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載入META-INF/spring.factories中key為SpringBootExceptionReporter的全類名的value值

  • SpringBootExceptionReporter是一個回撥介面,用於支援對SpringApplication啟動錯誤的自定義報告。裡面就一個報告啟動失敗的方法
  • 其實現類:org.springframework.boot.diagnostics.FailureAnalyzers
    用於觸發從spring.factories載入的FailureAnalyzerFailureAnalysisReporter例項

9. ApplicationContext基本屬性配置

	private void prepareContext(ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
		//設定應用的環境
		context.setEnvironment(environment);
		//對 context 進行了預設定
		postProcessApplicationContext(context);
		applyInitializers(context);
		遍歷呼叫SpringApplicationRunListener的contextPrepared()方法,通告SpringBoot應用使用的ApplicationContext準備好了
		listeners.contextPrepared(context);
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}

		// Add boot specific singleton beans
		context.getBeanFactory().registerSingleton("springApplicationArguments",
				applicationArguments);
		if (printedBanner != null) {
			context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
		}

		// Load the sources
		Set<Object> sources = getAllSources();
		Assert.notEmpty(sources, "Sources must not be empty");
		load(context, sources.toArray(new Object[0]));
		//遍歷呼叫SpringApplicationRunListener的contextLoaded()方法,通告ApplicationContext裝填完畢
		listeners.contextLoaded(context);
	}
複製程式碼

1). applyInitializers(context);

	protected void applyInitializers(ConfigurableApplicationContext context) {   
		for (ApplicationContextInitializer initializer : getInitializers()) {
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
					initializer.getClass(), ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			initializer.initialize(context);
		}
	}
複製程式碼

遍歷呼叫這些ApplicationContextInitializer的initialize(applicationContext)方法來對已經建立好的ApplicationContext進行進一步的處理

2). load(ApplicationContext context, Object[] sources)

protected void load(ApplicationContext context, Object[] sources) {
		if (logger.isDebugEnabled()) {
			logger.debug(
					"Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
		}
		BeanDefinitionLoader loader = createBeanDefinitionLoader(
				getBeanDefinitionRegistry(context), sources);
		if (this.beanNameGenerator != null) {
			loader.setBeanNameGenerator(this.beanNameGenerator);
		}
		if (this.resourceLoader != null) {
			loader.setResourceLoader(this.resourceLoader);
		}
		if (this.environment != null) {
			loader.setEnvironment(this.environment);
		}
		loader.load();
	}
複製程式碼

設定資源載入器,載入各種beans到ApplicationContext物件中

10. 更新應用上下文

	private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);
		if (this.registerShutdownHook) {
			try {
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}
複製程式碼

進入內部的refresh()方法,準備環境所需的bean工廠,通過工廠產生環境所需的bean,重點就是產生bean

	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}
複製程式碼

11. afterRefresh()

上下文重新整理後呼叫該方法,目前沒有操作

	protected void afterRefresh(ConfigurableApplicationContext context,
			ApplicationArguments args) {
	}
複製程式碼

12. callRunner()

	private void callRunners(ApplicationContext context, ApplicationArguments args) {
		List<Object> runners = new ArrayList<>();
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		runners.addAll(context.getBeansOfType(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);
			}
		}
	}
複製程式碼

查詢當前的ApplicationContext中是否註冊有CommandLineRunner或者ApplicationRunner,如果有,就遍歷執行他們。

SpringBoot啟動流程總結

上面從SpringApplication的初始化到SpringApplication.run()方法執行,基本上按照其內部函式呼叫的順序一步一步分析下來,內容非常多,很容易把人搞暈。在網上發現一張圖,圖出自SpringBoot啟動流程解析,畫的比較清楚明白,把SpringBoot啟動整個流程都包含進來了

appl

再總結下run方法中最關鍵的幾步:

  • 載入SpringApplicationRunListeners監聽器
  • 配置環境模組
  • 建立ApplicationContext應用上下文
  • ApplicationContext基本屬性配置
  • 更新應用上下文,產生環境所需要的bean

小結

上面的分析都是基於SpringBoot2.0版本,在之前的版本,內容上可能有些偏差,大體思路是差不多的。在閱讀原始碼的過程中,看了很多前人分析的部落格文章,也借鑑了他們的分析流程,有點「前人栽樹,後人乘涼」的感覺,現在「取之網路,再回饋之網路」

參考資料 & 鳴謝

相關文章