Spring啟動過程(一)

社會我敏哥發表於2018-05-02

 Spring的啟動過程,就是其IoC容器的啟動過程,本質就是建立和初始化bean的工廠(BeanFactory),BeanFactory其實就是整個SpringIoc的核心,Spring 使用 BeanFactory 來例項化、配置和管理 Bean。

對於web程式,IoC容器啟動過程即是建立上下文的過程,在web應用中,web容器會提供一個全域性的ServletContext上下文環境,ServletContext上下文為Spring IoC提供了一個宿主環境。

Spring應用在Web容器中啟動的整個過程如下:

Spring啟動過程(一)

先介紹下原始碼中出現的各位小夥伴:

  • Resource:是spring對於資源的一種抽象,因為資源的來源可能很豐富,利於File,Class Path Resource,Url Resource等,進行統一封裝,暴露出getInputStream進行統一讀取解析
  • Document:這個沒啥好講的,XML文件物件
  • EncodedResource:封裝了Resource,指定Resource的編碼
  • ReaderContext:Bean Definition解析過程中的上下文物件,封裝了Resource、ProblemReporter、EventListener、SourceExtractor等
  • Element:XML中的元素節點物件
  • BeanDefinition:這個介面及其實現類非常重要,他描述了XML中一個bean節點及其子節點的所有屬性,將xml中的描述轉變成內部的field資訊,舉例:scope,lazyinit,ConstructorArgumentValues(描述構造器),PropertyValues(描述屬性值)等,是一個保羅永珍的介面,其子類實現包括GenericBeanDefinition、RootBeanDefinition、ChildBeanDefinition等
  • BeanDefinitionHolder:顧名思義包含了一個BeanDefinition,同時其包含了beanName和aliases,更好的封裝了一次
  • BeanDefinitionReader:定義了讀取BeanDefinition的介面,主要作用是從Resource中讀取Bean定義,XmlBeanDefinitionReader是其具體的實現類
  • BeanDefinitionDocumentReader:定義了從Document物件中解析BeanDefinition並且註冊到Registry中設計到的介面,其預設實現類是DefaultBeanDefinitionDocumentReader,主要是被XmlBeanDefinitionReader委派去處理Document物件
  • BeanDefinitionParserDelegate:看到Delegate就知道這個哥們是個受苦的物件,是個最終幹活的人,官方定義是“Stateful delegate class used to parse XML bean definitions.* Intended for use by both the main parser and any extension* {@link BeanDefinitionParser BeanDefinitionParsers} or* {@link BeanDefinitionDecorator BeanDefinitionDecorators}.”是用於最終處理XML bean定義的人,它做的可都是髒活累活,import/alias/bean等element以及element的子節點以及屬性都是它解析並且填充到BeanDefinition中然後使用ReaderContext中的Registry(實際就是DefaultListableBeanFactory)來將該BeanDefinition註冊

在Web專案使用Spring,是通過在web.xml裡面配置:

org.springframework.web.context.ContextLoaderListener 來初始化IOC容器的。

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>複製程式碼

確切的說,ContextLoaderListener這個監聽物件,監聽的是ServletContext這個,當web容器初始化,ServletContext發生變化的時候,會觸發相應的事件。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener複製程式碼

ContextLoaderListener繼承了ContextLoader,並實現了ServletContextListener介面,在web容器初始化的時候,會觸發ServletContextListener介面中的contextInitialized()方法,同理,在容器關閉的時候,會觸發對應的contextDestroyed()方法。

Spring啟動過程(一)

其中,initWebApplicationContext()方法為父類ContextLoader中的方法:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext)複製程式碼

由於initWebApplicationContext()方法原始碼較長,我們進行拆分閱讀。

if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
	throw new IllegalStateException(
			"Cannot initialize context because there is already a root application context present - " +
			"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
//日誌記錄
Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
	logger.info("Root WebApplicationContext: initialization started");
}
//啟動startTime,記錄啟動耗時
long startTime = System.currentTimeMillis();複製程式碼

首先判斷是否建立了WebApplicationContext,正常情況下建立了一個WebApplicationContext後,會把context set到ServletContext中,setAttribute的本質其實是往LinkedHashMap中set值。

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);複製程式碼

如果從servletContext中根據key獲得了WebApplicationContext物件,則表示之前已經建立過了根上下文WebApplicationContext,此時丟擲異常,提示檢查web.xml中的配置,避免重複建立root上下文,保證只有一個Spring容器。

再看下面的程式碼:

try {
	// Store context in local instance variable, to guarantee that
	// it is available on ServletContext shutdown.
	if (this.context == null) {
		this.context = createWebApplicationContext(servletContext);
	}
	if (this.context instanceof ConfigurableWebApplicationContext) {
		ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
		if (!cwac.isActive()) {
			// The context has not yet been refreshed -> provide services such as
			// setting the parent context, setting the application context id, etc
			if (cwac.getParent() == null) {
				// The context instance was injected without an explicit parent ->
				// determine parent for root web application context, if any.
				ApplicationContext parent = loadParentContext(servletContext);
				cwac.setParent(parent);
			}
			configureAndRefreshWebApplicationContext(cwac, servletContext);
		}
	}
	servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
	ClassLoader ccl = Thread.currentThread().getContextClassLoader();
	if (ccl == ContextLoader.class.getClassLoader()) {
		currentContext = this.context;
	}
	else if (ccl != null) {
		currentContextPerThread.put(ccl, this.context);
	}
	if (logger.isDebugEnabled()) {
		logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
				WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
	}
	if (logger.isInfoEnabled()) {
		long elapsedTime = System.currentTimeMillis() - startTime;
		logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
	}
	return this.context;
}複製程式碼

程式碼片段較長,一步一步來,try中有倆關鍵的地方:

createWebApplicationContext()和configureAndRefreshWebApplicationContext()

先逐步往下看

首先是判斷 this.context == null,通過createWebApplicationContext方法建立一個

WebApplicationContext,具體程式碼如下:

	protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
		Class<?> contextClass = determineContextClass(sc);
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
					"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}
		return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}複製程式碼

determineContextClass,字面意思為檢測Context的class型別,會讀取servletContext的初始化引數contextClass,大部分情況下我們不會配置此引數,在未配置的情況下,Spring會去org.springframework.web.context包中的ContextLoader.properties配置檔案讀取預設配置:

Spring啟動過程(一)

通過Spring提供的ClassUtil進行反射,反射出XmlWebApplicationContext類,再通過BeanUtils進行反射,呼叫無參構造器,instance出一個WebApplicationContext並返回ConfigurableWebApplicationContext。

if (this.context instanceof ConfigurableWebApplicationContext) {
	ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
	if (!cwac.isActive()) {
		// The context has not yet been refreshed -> provide services such as
		// setting the parent context, setting the application context id, etc
		if (cwac.getParent() == null) {
			// The context instance was injected without an explicit parent ->
			// determine parent for root web application context, if any.
			ApplicationContext parent = loadParentContext(servletContext);
			cwac.setParent(parent);
		}
		configureAndRefreshWebApplicationContext(cwac, servletContext);
	}
}複製程式碼

獲得WebApplicationContext後,下一步判斷獲得的context是否為

ConfigurableWebApplicationContext的例項,預設的XmlWebApplicationContext滿足判斷條件。

判斷父上下文是否為active狀態,如果active為false下,需要判斷父上下文是否為null,如果parent上下文為null的情況,則執行loadParentContext()。loadParentContext()方法,是一個預設的是模板實現方法,用於載入/獲取ApplicationContext,此context為根WebApplicationContext的父上下文。

下一步,進入configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)複製程式碼

將上面建立的XmlWebApplicationContext進行初始化操作。

if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
	// The application context id is still set to its original default value
	// -> assign a more useful id based on available information
	String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
	if (idParam != null) {
		wac.setId(idParam);
	}
	else {
		// Generate default id...
		wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
				ObjectUtils.getDisplayString(sc.getContextPath()));
	}
}複製程式碼

主要建立一個預設id,org.springframework.web.context.WebApplicationContext:+專案的ContextPath

//設定sc到wac中,便於Spring獲得ServletContext
wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
	wac.setConfigLocation(configLocationParam);
}
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
	((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
customizeContext(sc, wac);
wac.refresh();複製程式碼

建立好id後,把servletContext放入建立好的XmlWebApplicationContext中,便於spring後續獲得servletContext上下文。

緊接著會去讀取web.xml中<param-name>contextConfigLocation</param-name>的配置

Spring啟動過程(一)

如果在web.xml中未配置,則會去讀取WEB-INF下面的applicationContext.xml配置檔案,

即XmlWebApplicationContext中的屬性 DEFAULT_CONFIG_LOCATION的預設值

/** Default config location for the root context */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";複製程式碼

讀取到contextConfigLocation相關檔案配置路徑後,設定到XmlWebApplicationContext中,用於載入指定路徑的配置檔案。

下一步 customizeContext(sc, wac);

主要用於自定義context相關配置,比如定義bean是否可以迴圈引用,bean定義是否可以被覆蓋等,通常情況下不做任何操作。

最後一步:

wac.refresh();複製程式碼

整個容器啟動的最核心方法,在這個refresh()方法中,會完成資原始檔的載入、配置檔案解析、Bean定義的註冊、元件的初始化等核心工作。

refresh()方法的相關邏輯,下一篇繼續深入學習。

參考連結:https://www.jianshu.com/p/375ef7095139


相關文章