spring web容器加載始末

// 緩慢更新ing

這是很早以前就想搞明白的一個問題,但回回看,回回暈。最開始連從哪一個類進都不知道,後來雖然曉得是從org.springframework.web.context.ContextLoaderListener這個監聽器開始的,但點進去後,立馬就陷入了方法調用的汪洋大海…… ……

爲何看的會頭暈呢,或者換種問法,什麼狀況下腦子會不夠用?
一,線索與線索之間的關聯度過低;二,線索太多,致使遺忘,很難串成一條線。
其實對於spring來說,也不存在什麼過高深的代碼,命名嚴謹,封裝規範,看上去簡潔易懂。因此根本上講,仍是由於沒找到一條清晰的鏈路進行跟蹤。

直到某天,我瞄了一眼idea的調用棧。
驚了!
從入口到斷點處,通過了哪些類、哪一個方法都明明白白,另外一個窗口還能夠看有哪些線程。

屌誒

另外我碰巧打在一個BeanPostProcessor接口的實現類上,這個調用棧又恰好走過了容器加載的過程。
而,這不就是我想要的,清晰的鏈路嗎

:-)





記錄的意義

9102年了,spring源碼解讀類的書都出到…… em…… ……
都不知道出到第幾版了。

那還寫博客幹嗎?
於我的研究終有意義,但記錄好像略顯矯情。因此最後,應該仍是印證了那句話:
我的博客是寫給本身看的,於他人,更像是展示一種成長的痕跡,而不是技術的原理。





主流程

web環境下,容器加載主流程基本被org.springframework.context.support.AbstractApplicationContext#refresh()方法給歸納了。可是在着重研究它以前,仍是要簡單講下進入這個方法以前的代碼。

首先,只要是java web項目, 第一都是tomcat啓動,而後servlet容器啓動,加載並讀取web.xml,建立ServletContext,啓動listener,啓動filter,啓動servlet
爲了讓spring在這個流程中啓動,咱們都會在web.xml裏配置spring的一個監聽器(上下文加載器監聽器)。而後經過這個監聽器,調用spring的初始化方法。之因此使用監聽器,是爲了在filter與servlet啓動以前把容器加載完畢,由於這倆有可能用到容器中的內容。
<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...         ↓

能夠說是很長了

:-(




prepareRefresh()

這個方法沒什麼好說的,初始化一些參數,並校驗。由於這這篇文章是從ContextLoaderListener#contextInitialized()方法一路看下來的,因此初始化參數這段代碼,以前就出現過了。但下面這個語句仍是值得留心,雖然如今也不知道這有什麼用……
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
複製代碼

ok,這個方法結束。

緩存

obtainFreshBeanFactory()

加載xml並讀取bean,一個長的要死的流程的開始……

1.refreshBeanFactory()

首先獲取新的BeanFactory。
給誰獲取?org.springframework.web.context.support.**XmlWebApplicationContext**對象。XmlWebApplicationContext的父類包含一個DefaultListableBeanFactory的私有成員變量。
/** Bean factory for this context */
private DefaultListableBeanFactory beanFactory;
複製代碼

在獲取以前會先檢查是否已經存在BeanFactory,檢查的依據,就是看對象是否是null。有意思的是,這個方法如何作到線程同步。
tomcat

1.1鎖

在類內new一個Object類型的私有成員變量,做爲一把鎖。要注意的是,這把鎖 不是靜態的。所以,僅當多個線程想要調用 這個XmlWebApplicationContext對象的hasBeanFactory()方法時,纔會等待。
/** 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();
複製代碼

而後進入又一個重頭戲:

2.loadBeanDefinitions(beanFactory)

從spring的配置文件里加載定義好的bean。所以具體實如今這個位置org.springframework.web.context.support. XmlWebApplicationContext#loadBeanDefinitions()

關於如何加載的,本身也能夠猜一下。先確認xml的路徑,而後用dom4j之類的東西讀取xml,讀取完之後,按必定的規則,靠反射建立對象。
spring也基本是這個邏輯,但中間的過程卻很複雜,因此下面要搞懂的,就是他爲何這麼複雜。

2.1 找到xml的所在位置

解析xml的工具,再怎麼強悍,也要先知道xml到底保存在哪裏才行。咱們配置的spring xml路徑都是相對的(以下),關鍵是怎麼根據這個相對路徑找到絕對路徑。
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對象下找到了文件目錄

文件絕對路徑以URLResource的方式包裹,並向上轉型爲Resource接口。

2.2 loadBeanDefinitions

即使得到了絕對路徑,離解析xml並提取BeanDefinition的代碼,還差得遠。
以上那一連串獲取xml文件絕對路徑的代碼,都只是XmlBeanDefinitionReader類中的一部分。爲了避免讓思惟陷入混沌或是跑偏,繼續用表格的方式羅列一下過程

類名 簡單描述
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以及屬性的方法

but,最後返回的不是類對象、不是BeanDefinition,是org.springframework.beans.factory.config.BeanDefinitionHolder。

其實,我是能理解不返回類對象,只返回BeanDefinition。由於spring做爲一個龐大的框架,在初始化類對象前,一定還有一些其它的操做,若是直接返回一個類對象,那麼解析這一環的操做會變得更加複雜(自己解析就已經通過了不少方法了)。
經驗告訴咱們,超複雜代碼的維護成本是極其昂貴的。因此在解析這一環,作的就是 純解析而不是「解釋」。不過我沒搞明白的是,幹嗎還要用BeanDefinitionHolder把BeanDefinition包裝一層再返回?

雖然返回的是BeanDefinitionHolder,但解析成BeanDefinition的過程仍是實打實存在的,因此先收回來,看看怎麼解析BeanDefinition的。可是解析的代碼過長,閱讀源碼也不可能說一行行的解釋過去,所以這裏只能記錄一些我以爲有趣,有借鑑意義的代碼。

2.2.1 遞歸解析

要先從這個方法開始,前面有些代碼我可能就省略了。

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……

2.2.2 ParseState堆棧

以上文貼的,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此節點的類型與名稱,如圖。

起初也沒搞懂,設計這個堆棧是幹嗎的。後來反應過來了,解析節點以及屬性時,普遍應用了遞歸。遞歸這個東西出問題時是很那排查的,debug都看的暈。所以搞一個堆棧,解析前push,解析後pop,出異常了,就把當前堆棧信息提如今異常中。 基於先進後出的特性,立馬就能看出是在解析哪一個節點時出的錯。

最後的最後,終於見證了一個簡單的bean,成爲了BeanDefinition,又被封裝成BeanDefinitionHolder,並保存到map中:
org.springframework.beans.factory.support.DefaultListableBeanFactory#beanDefinitionMap

2.2.3 customElement

to be continued…… ……
相關文章
相關標籤/搜索