<listener>
<description>Spring容器加載監聽器</description>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
複製代碼
由於監聽器的緣故,ContextLoaderListener#contextInitialized()方法會被自動調用,在contextInitialized()方法內,又會調用org.springframework.web.context.ContextLoader#initWebApplicationContext()方法。
java
initWebApplicationContext()
初始化web應用上下文,首先要建立一個上下文對象。建立時,優先從web.xml裏尋找,有沒有名爲「contextClass」參數,有的話根據名字獲取Class對象,而後以反射的方式將上下文對象new出來,最後new出來的必定是ConfigurableWebApplicationContext類型。若是web.xml裏不存在「contextClass」,那麼默認使用org.springframework.web.context.support.XmlWebApplicationContext類來建立上下文。
node
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
Class<?> contextClass = determineContextClass(sc);// 這裏選擇上下文類的Class對象
// 此處省略……
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
複製代碼
root上下文建立完後,還會從web.xml裏查詢是否配置了parent上下文,有的話就加載並指定,可是這個操做,我工做這幾年也沒見過哪一個項目配置過,就暫不理會了。下一步:
web
configureAndRefreshWebApplicationContext()
配置並刷新上下文,這個方法內,比較重要的邏輯是從web.xml裏讀取spring各項配置文件的路徑,具體什麼文件因項目而異,我我的的習慣是定義application-main.xml,而後在main.xml裏,import數據庫,緩存,隊列等的xml。spring
<context-param><!---->
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/applicationContext-main.xml</param-value>
</context-param>
複製代碼
讀取完配置文件路徑後,spring會再從web.xml中初始化一些參數,以及新建一個ConfigurableEnvironment對象。
初始化的參數這幾年項目中也沒見過,暫不詳述。主要是ConfigurableEnvironment,這個類的對象接下來出現的頻率還比較高,經過這個接口能夠獲取到不少spring須要的配置參數,而後記錄一下類之間的包含關係。mongodb
AbstractApplicationContext==包含==>ConfigurableEnvironment==包含==>MutablePropertySources
複製代碼
再接下來初始化一些實現了org.springframework.context.ApplicationContextInitializer接口的類,詳情在那些實現了Spring接口的類,都是怎麼被加載的文章中有記錄。
最後終於到org.springframework.context.support.AbstractApplicationContext#refresh()。
從ContextLoaderListener#contextInitialized()方法到AbstractApplicationContext#refresh()方法,中間沒有特別深奧的東西,就是A方法調B方法,B方法調C方法,等等等等。
但,起名嚴謹直觀,看一眼就知道類和方法的做用,這是寫代碼要學習的地方。
由於直接貼代碼看的不是很清晰,因此用表格列一下refresh()中的幾個方法:數據庫
正常流程 | 官方註釋 | |
---|---|---|
prepareRefresh() | // Prepare this context for refreshing. | ↓ |
obtainFreshBeanFactory() | // Tell the subclass to refresh the internal bean factory. | ↓ |
prepareBeanFactory(beanFactory) | // Prepare the bean factory for use in this context. | ↓ |
postProcessBeanFactory(beanFactory) | // Allows post-processing of the bean factory in context subclasses. | ↓ |
invokeBeanFactoryPostProcessors(beanFactory) | // Invoke factory processors registered as beans in the context. | ↓ |
registerBeanPostProcessors(beanFactory) | // Register bean processors that intercept bean creation. | ↓ |
initMessageSource() | // Initialize message source for this context. | ↓ |
initApplicationEventMulticaster() | // Initialize event multicaster for this context. | ↓ |
onRefresh() | // Initialize other special beans in specific context subclasses. | ↓ |
registerListeners() | // Check for listener beans and register them. | ↓ |
finishBeanFactoryInitialization(beanFactory) | // Instantiate all remaining (non-lazy-init) singletons. | ↓ |
finishRefresh() | // Last step: publish corresponding event. | ↓ |
拋異常執行的方法 | ||
destroyBeans() | // Destroy already created singletons to avoid dangling resources. | ↓ |
cancelRefresh(ex) | // Reset 'active' flag. | ↓ |
finally塊的方法 | ||
resetCommonCaches() | // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... | ↓ |
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
複製代碼
ok,這個方法結束。
緩存
/** Bean factory for this context */
private DefaultListableBeanFactory beanFactory;
複製代碼
在獲取以前會先檢查是否已經存在BeanFactory,檢查的依據,就是看對象是否是null。有意思的是,這個方法如何作到線程同步。
tomcat
/** Synchronization monitor for the internal BeanFactory */
private final Object beanFactoryMonitor = new Object();
protected final boolean hasBeanFactory() {
synchronized (this.beanFactoryMonitor) {
return (this.beanFactory != null);
}
}
複製代碼
若是,當前真的已經存在了一個BeanFactory,那麼會銷燬以前建立出來的所有bean,以及這個已經存在了的BeanFactory。因爲spring是靠Map、List、Set來持有bean的,因此銷燬建立的bean,就是把集合清空,而後銷燬BeanFactory,就是將引用置爲null。
判斷完之後,開始真正的建立。默認new一個org.springframework.beans.factory.support.DefaultListableBeanFactory類。這段代碼研究不深,略過。bash
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}
複製代碼
DefaultListableBeanFactory類須要重點關注,由於這個對象是容器的核心,比方說要根據名字或類型從容器拿對象,最後靠的就是它的方法。好比:app
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean();
org.springframework.beans.factory.support.AbstractBeanFactory#containsBean();
複製代碼
而後進入又一個重頭戲:
classpath*:config/applicationContext-main.xml
複製代碼
咱們順着loadBeanDefinitions()方法一路往下走,在org.springframework.beans.factory.support.AbstractBeanDefinitionReader#loadBeanDefinitions()方法內有這麼一行代碼,這就是獲取絕對路徑的關鍵。
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
複製代碼
這方法再往下走也很深,debug讓人頭暈。能確認的是,這一整套邏輯和CLassLoader相關。起效的方法以下:sun.misc.URLClassPath#findResources
public Enumeration<URL> findResources(final String var1, final boolean var2) {
return new Enumeration<URL>() {
private int index = 0;
private int[] cache = URLClassPath.this.getLookupCache(var1);
private URL url = null;
private boolean next() {
if (this.url != null) {
return true;
} else {
do {
URLClassPath.Loader var1x;
if ((var1x = URLClassPath.this.getNextLoader(this.cache, this.index++)) == null) {
return false;
}
// 雙眼盯準這一句
this.url = var1x.findResource(var1, var2);
} while(this.url == null);
return true;
}
}
public boolean hasMoreElements() {
return this.next();
}
public URL nextElement() {
if (!this.next()) {
throw new NoSuchElementException();
} else {
URL var1x = this.url;
this.url = null;
return var1x;
}
}
};
}
複製代碼
最後在這個URLClassPath對象下找到了文件目錄
類名 | 簡單描述 | |
---|---|---|
org.springframework.web.context.support.XmlWebApplicationContext | 準備XmlBeanDefinitionReader對象,提供xml文件的相對路徑 | ↓ |
org.springframework.beans.factory.xml.XmlBeanDefinitionReader | 根據相對路徑獲取xml的絕對路徑,讀取並解析成org.w3c.dom.Document對象,準備BeanDefinitionDocumentReader對象 | ↓ |
org.springframework.beans.factory.xml.BeanDefinitionDocumentReader | 提取root節點,爲解析作一些判斷幷包含遞歸解析之類的操做。建立BeanDefinitionParserDelegate對象 | ↓ |
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate | 裏面都是實打實的,從document、element解析bean以及屬性的方法 | ↓ |
要先從這個方法開始,前面有些代碼我可能就省略了。
org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions
複製代碼
第一,判斷這個document的root節點是否是<beans>,判斷依據是提取節點的namespaceUri。
public boolean isDefaultNamespace(Node node) {
return isDefaultNamespace(getNamespaceURI(node));
}
public boolean isDefaultNamespace(String namespaceUri) {
return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
}
複製代碼
就看這個代碼,是否是也挺簡單的?因此原則上任何一個問題,被拆分紅數個小模塊以後,都是簡單的。
而後,提取beans節點的profile屬性,若是profile屬性爲空,直接解析。若是不爲空,那麼判斷profile是否被激活,未激活則放棄解析。(篇幅問題只放一點代碼)
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
複製代碼
那麼判斷激活的邏輯呢?
代碼是死的,數據是活的,因此一定是先在某個地方配置了激活的profile的名字,而後才能以此爲依據來判斷beans節點的profile屬性是否激活。spring有個參數名爲spring.active.profiles,此參數的值,便表明着激活的profile名。
這個變量,默認從java.lang.System類中獲取。想必你也發現了,這裏有個加載前後的注意點。必須先讀取並加載spring.active.profiles的值,而後才能開始判斷beans節點中的profile屬性是否激活。
所以操做順序是這樣的,一,讀取spring.active.profiles;二,加載spring.active.profiles;三,根據spring.active.profiles去判斷是否激活
容器啓動的代碼已經很靠前了,要怎麼更靠前一點,去讀取參數呢?
ApplicationContextInitializer接口,咱們能夠自定義一個ApplicationContextInitializer接口,在這個類內讀取參數,詳情點擊查看另外一篇博客。
關於加載的過程就一圖以蔽之了。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}
複製代碼
首先解釋一下customElement與defaultElement的區別。
<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" lazy-init="false">
<!-- 線程池維護線程的最少數量 -->
<property name="corePoolSize" value="5"/>
<!-- 容許的空閒時間 -->
<property name="keepAliveSeconds" value="200"/>
<!-- 線程池維護線程的最大數量 -->
<property name="maxPoolSize" value="20"/>
<!-- 緩存隊列 -->
<property name="queueCapacity" value="20"/>
<!-- 對拒絕task的處理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</property>
</bean>
<mongo:db-factory id="mongoFactory" client-uri="mongodb://root:root@127.0.0.1:27017/test"/>
<mongo:mapping-converter id="mongoConverter" db-factory-ref="mongoFactory"/>
<mongo:template id="mongoTemplate" db-factory-ref="mongoFactory" converter-ref="mongoConverter"/>
<mongo:gridFsTemplate id="gridFsTemplate" bucket="allot_image" db-factory-ref="mongoFactory" converter-ref="mongoConverter"/>
複製代碼
最簡單的解釋,<beans>、<bean>、<import>、<alias>這四種節點就是defaultElement,其它全部節點,都屬於customElement。好比<mongo:>、<context:>等等等等。
由於<beans>節點是能夠嵌套的,因此一旦在<beans>節點內發現了<beans>節點,就會遞歸調用
org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions
繼續判斷是否須要根據profile來加載,子節點屬於defaultElement仍是customElement……bla……bla……bla……
以上文貼的,threadPool bean舉例。這是defaultElement中的beanElement,下一步終於能夠開始實打實的解析了。
第一步,提取bean節點相關屬性,id,alias,lazyInit,singleton,scope,abstract,parent等等等等,而後建立一個BeanDefinition對象
第二步,提取嵌套在bean節點內的節點,好比最多見的<property>、<meta>、<look-up>、<constructor-arg>等等。另外,由於節點直接能夠互相嵌套,因此像這樣解析一種節點就是一個方法,而後在方法內組合調用,天然而然造成遞歸。
模塊化,提升的不只僅是可讀性,還有效率!
parseMetaElements(ele, bd);
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
parseConstructorArgElements(ele, bd);
parsePropertyElements(ele, bd);
parseQualifierElements(ele, bd);
複製代碼
看過spring文檔再來看這部分代碼,如見故人的感受會很強烈,咱們寫文檔也該這樣,與代碼邏輯高度同步,言簡意賅。
這些簡單的,按規則解析屬性的代碼就不看了,重點關注一下ParseState。
this.parseState.push(new QualifierEntry(typeName));
this.parseState.pop();
複製代碼
一個普普統統的類,把堆棧的方法給包了一層,而後額外提供了一個複製當前堆棧值的方法。除了封裝,要達到一樣的效果,還可使用繼承,就是直接繼承Stack類,各有利弊。總之,就把ParseState理解成一個堆棧就行了。
而後,再解析每一種節點類型前,都會往這個堆棧內push此節點的類型與名稱,如圖。