Spring Boot構造流程淺析

齊天小聖^O^發表於2020-11-11

什麼是Spring Boot的構造流程?即初始化類SpringApplication的例項化過程。

我們都知道Spring Boot專案的啟動非常簡單,只需要執行入口類的main方法即可,如下:

@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class);
    }
}

可以看到main方法中只有一句程式碼:SpringApplication.run(xxxx.class),我們進入這個run方法,如下:

	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);
	}

仔細看這句程式碼:new SpringApplication(primarySources).run(args),發現居然new了一個SpringApplication去呼叫另外一個run方法,其實這句程式碼包含了兩個非常重要的內容,即Spring Boot的構造流程和執行流程,構造流程是指SpringApplication類的例項化過程,執行流程是指SpringApplication類的例項化物件呼叫run方法完成整個專案的初始化和啟動的過程,而本文的重點是前者。

到這一步,我們基本能夠明白一件事:入口類中主要通過SpringApplication的run方法進行SpringApplication類的例項化操作,然後這個例項化物件再去呼叫另外一個更牛逼的run方法來完成整個專案的初始化和啟動。

下面我們將正式進入SpringApplication類的例項化過程的探析,首先看一下SpringApplication兩個構造方法的原始碼:

	public SpringApplication(Class<?>... primarySources) {
		this(null, primarySources);
	}
	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		//賦值成員變數resourceLoader
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		//賦值成員變數primarySources 
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		//推斷Web應用型別
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		//載入並初始化ApplicationContextInitializer及相關實現類
		setInitializers((Collection) getSpringFactoriesInstances(
				ApplicationContextInitializer.class));
		//載入並初始化ApplicationListener及相關實現類
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		//推斷main方法
		this.mainApplicationClass = deduceMainApplicationClass();
	}

第一個構造方法其實是直接呼叫了第二個核心構造方法,核心業務邏輯便在其中,下面將詳細講解核心構造方法涉及到的業務邏輯。

一、賦值成員變數:resourceLoader、primarySources

可以看到核心構造方法包含兩個引數:ResourceLoader和Class<?>...primarySources。其中前者為資源載入的介面,在Spring Boot啟動時可以通過它來指定需要載入的檔案路徑;後者預設傳入的是Spring Boot入口類,作為專案的引導類。構造方法的第一個步驟非常簡單,就是將傳進來的這兩個引數賦值給對應的成員變數。

二、推斷Web應用型別

 接著是呼叫了WebApplicationType的deduceFromClasspath方法來推斷Web應用型別,我們首先進入WebApplicationType類:

public enum WebApplicationType {
	//非Web應用型別
	NONE,
	//基於Servlet的Web應用型別
	SERVLET,
	//基於Reactive的Web應用型別
	REACTIVE;
        ...
}

可以知道WebApplicationType類只是一個列舉型別,包括:非Web應用型別,基於Servlet的Web應用型別,基於Reactive的Web應用型別。另外可以在WebApplicationType類中看到剛才提到的推斷方法deduceFromClasspath(),推斷方法以及用於推斷的常量原始碼如下。

	private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
			"org.springframework.web.context.ConfigurableWebApplicationContext" };

	private static final String WEBMVC_INDICATOR_CLASS = "org.springframework."
			+ "web.servlet.DispatcherServlet";

	private static final String WEBFLUX_INDICATOR_CLASS = "org."
			+ "springframework.web.reactive.DispatcherHandler";

	private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

	private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

	private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

	static WebApplicationType deduceFromClasspath() {
                //如果類路徑中包含DispatcherHandler且不包含DispatcherServlet,也不包含ServletContainer,則為Reactive應用
		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) {
                        //如果類路徑下Servlet或者ConfigurableWebApplicationContext任何一個不存在,則為非Web應用
			if (!ClassUtils.isPresent(className, null)) {
				return WebApplicationType.NONE;
			}
		}
                //否則都是Servlet應用型別
		return WebApplicationType.SERVLET;
	}

isPresent方法可以通過反射機制建立出指定類,根據在建立過程中是否丟擲異常來判斷指定類是否存在。分析該推斷方法可以得知核心邏輯是通過ClassUtils.isPresent()來判斷類路徑classpath下是否存在指定類,從而判斷出應用型別。推斷邏輯如下:

  1. 如果類路徑中包含DispatcherHandler且不包含DispatcherServlet,也不包含ServletContainer,則為Reactive應用。
  2. 如果類路徑下Servlet或者ConfigurableWebApplicationContext任何一個不存在,則為非Web應用。
  3. 其他情況則為Servlet應用型別。

三、載入並初始化ApplicationContextInitializer及相關實現類

ApplicationContextInitializer的作用:它是Spring IOC容器提供的一個回撥介面,通常用於應用程式上下文進行程式設計初始化的Web應用程式中。

在完成Web應用型別推斷之後,接著便開始ApplicationContextInitializer的載入工作,這裡將分成兩個步驟:即獲取相關例項和設定例項。對應的方法為:getSpringFactoriesInstances()和setInitializers()。我們首先來看getSpringFactoriesInstances()方法,原始碼如下:

	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 = Thread.currentThread().getContextClassLoader();
		// Use names and ensure unique to protect against duplicates
                //載入META-INF/spring.factories檔案中的對應配置
		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()方法來載入META-INF/spring.factories檔案中的對應配置,該檔案的內容如下:

 看到這裡大家可能會覺得似曾相識,沒錯,這裡載入META-INF/spring.factories檔案的過程在之前講解Spring Boot自動配置時已經提到過,spring.factories檔案中的內容會被解析到Map<String,List<String>>中,最後loadFactoryNames通過傳遞過來的class名稱作為Key從Map中獲得該類的配置列表,而這個class名稱就是type的值,追溯type的值發現其實就是一開始傳入的ApplicationContextInitializer.class。

上面通過SpringFactoriesLoader.loadFactoryNames()方法獲取到了ApplicationContextInitializer介面具體的實現類的全限定名,下面就要呼叫createSpringFactoriesInstances()方法來建立這些例項,再將這些例項經過排序後返回,至此獲取相關例項結束,下一步是設定例項。

下面看設定例項的方法:setInitializers()

	public void setInitializers(
			Collection<? extends ApplicationContextInitializer<?>> initializers) {
		this.initializers = new ArrayList<>();
		this.initializers.addAll(initializers);
	}

可以看到設定例項的步驟很簡單,將SpringApplication的initializers成員變數例項化為一個新的List,然後將剛才獲取到的例項放入其中即可。至此載入並初始化ApplicationContextInitializer及相關實現類結束。

四、載入並初始化ApplicationListener及相關實現類

ApplicationListener經常用於監聽容器初始化完成之後,執行資料載入、初始化快取等任務。

ApplicationListener的整個載入流程與ApplicationContextInitializer的載入流程完全相同,這裡就不再重複。

五、推斷main方法

最後一步是通過deduceMainApplicationClass()推斷main方法,來看原始碼:

	private Class<?> deduceMainApplicationClass() {
		try {
                        //通過新建一個執行時異常來獲得棧陣列
			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
                        //遍歷棧陣列
			for (StackTraceElement stackTraceElement : stackTrace) {
                                //匹配出第一個main方法
				if ("main".equals(stackTraceElement.getMethodName())) {
                                        //返回該類的class物件
					return Class.forName(stackTraceElement.getClassName());
				}
			}
		}
		catch (ClassNotFoundException ex) {
			// Swallow and continue
		}
		return null;
	}

該方法首先通過一個執行時異常來獲得棧陣列,接著遍歷這個陣列尋找出第一個“main”方法,如果找到了這個main方法,就通過返回這個類的物件,並最終將這個物件賦值給SpringApplication的成員變數mainApplicationClass。至此,SpringApplication的例項化過程結束!

現將SpringApplication類的例項化過程涉及到的核心操作總結如下:

  1. 賦值成員變數:resourceLoader、primarySources
  2. 推斷Web應用型別
  3. 載入並初始化ApplicationContextInitializer及相關實現類
  4. 載入並初始化ApplicationListener及相關實現類
  5. 推斷main方法

 

 

相關文章