天天看點

看看Spring的源碼(一)——Bean加載過程

最近幾天跟同僚聊起spring的一些問題,對一些地方有些疑問,趁這兩天有點空,看看spring的源碼,了解下具體的實作細節。本文基于spring 4.0.5版本。

首先web項目使用spring是通過在web.xml裡面配置

<code>org.springframework.web.context.contextloaderlistener</code>初始化ioc容器的。

那就以此為切入點順藤摸瓜。

<code>contextloaderlistener</code>繼承了<code>contextloader</code>,并且實作<code>servletcontextlistener</code>接口。當server容器(一般指tomcat)啟動時,會收到事件初始化。<code></code>

<code></code>

<code>initwebapplicationcontext</code>方法是在<code>org.springframework.web.context.contextloader</code>類裡面。方法太長,分段讀一下。<code></code>

首先是判斷<code>servletcontext</code>中是否已經注冊了<code>webapplicationcontext</code>,如果有則抛出異常,避免重複注冊。然後就是啟用log,啟動計時。本方法的關鍵就在于try代碼塊裡的内容<code></code>

這裡面有幾個關鍵的方法。首先看一下<code>createwebapplicationcontext()</code><code></code>

首先<code>determinecontextclass()</code>方法查明具體的<code>context</code>類,他會讀取<code>servletcontext</code>的初始化參數<code>contextclass</code>,此參數我們一半不配置,是以<code>spring</code>就會讀取跟<code>org.springframework.web.context.webapplicationcontext</code>同一個包下面的<code>contextloader.properties</code>檔案讀取預設設定,反射出<code>org.springframework.web.context.support.xmlwebapplicationcontext</code>類來。接下來就是在<code>configureandrefreshwebapplicationcontext()</code>方法裡将新建立的<code>xmlwebapplicationcontext</code>進行初始化。首先會設定一個預設id,即<code>org.springframework.web.context.webapplicationcontext:</code>+你項目的<code>contextpath</code>。<code></code>

緊接着就是将<code>servletcontext</code>設定成<code>xmlwebapplicationcontext</code>的屬性,這樣<code>spring</code>就能在上下文裡輕松拿到<code>servletcontext</code>了。<code></code>

接下來就是讀取<code>web.xml</code>檔案中的<code>contextconfiglocation</code>參數。如果沒有配置就會去讀<code>web-inf下的applicationcontext.xml</code>檔案。<code></code>

并将值設定(就是我們的spring配置檔案的路徑)進<code>xmlwebapplicationcontext</code>中。然後就會在指定的路徑加載配置檔案。<code></code>

接下來就是<code>customizecontext(sc, wac)</code>方法,此方法會根據使用者配置的<code>globalinitializerclasses</code>參數來初始化一些使用者自定義的屬性,一般我們不配置,是以這裡什麼也不做。

最後登場的就是最核心的方法了,<code></code>

在這個方法裡,會完成資源檔案的加載、配置檔案解析、bean定義的注冊、元件的初始化等核心工作,我們一探究竟。<code></code>

次方法是同步的,避免重複重新整理,每個步驟都放在單獨的方法内,流程清晰,是值得學習的地方。這裡面有個重要的方法是<code>finishbeanfactoryinitialization(beanfactory);</code>,裡面的内容是spring如何執行個體化bean,并注入依賴的,這個内容下一節講,本節隻說明spring是如何加載class檔案的。

首先就是<code>preparerefresh()</code>方法。<code></code>

此方法做一些準備工作,如記錄開始時間,輸出日志,<code>initpropertysources();</code>和<code>getenvironment().validaterequiredproperties();</code>一般沒幹什麼事。

接下來就是初始化<code>beanfactory</code>,是整個<code>refresh()</code>方法的核心,其中完成了配置檔案的加載、解析、注冊

看看它裡面都做了些什麼?<code></code>

首先<code>refreshbeanfactory()</code>:<code></code>

我們看到會建立一個<code>defaultlistablebeanfactory</code>執行個體<code></code>

再設定一個id<code></code>

然後設定一些自定義參數:<code></code>

這裡面最重要的就是<code>loadbeandefinitions(beanfactory);</code>方法了。<code></code>

此方法會通過<code>xmlbeandefinitionreader</code>加載bean定義。具體的實作方法是在<code>org.springframework.beans.factory.xml.xmlbeandefinitionreader.loadbeandefinitions</code>方法中定義的。這裡設計了層層調用,有好多重載方法,主要就是加載spring所有的配置檔案(可能會有多個),以備後面解析,注冊之用。我一路追蹤到<code>org.springframework.beans.factory.xml.defaultbeandefinitiondocumentreader.doregisterbeandefinitions(element root)</code><code></code>

這裡建立了一個<code>beandefinitionparserdelegate</code>示例,解析xml的過程就是委托它完成的,我們不關心它是怎樣解析xml的,我們隻關心是怎麼加載類的,是以就要看<code>parsebeandefinitions(root, this.delegate)</code>方法了。<code></code>

我們看到最終解析xml元素的是<code>delegate.parsecustomelement(ele)</code>方法,最終會走到一下方法.<code></code>

這裡會根據不同的xml節點,會委托<code>namespacehandlersupport</code>找出合适的<code>beandefinitionparser</code>,如果我們配置了<code></code>

那麼對應<code>beandefinitionparser</code>就是<code>org.springframework.context.annotation.componentscanbeandefinitionparser</code>,來看看它的<code>parse</code>方法。<code></code>

不難看出這裡定義了一個<code>classpathbeandefinitionscanner</code>,通過它去掃描包中的類檔案,注意:這裡是類檔案而不是類,因為現在這些類還沒有被加載,隻是classloader能找到這些class的路徑而已。到目前為止,感覺真想距離我們越來越近了。順着繼續往下摸。進入<code>dosacn</code>方法裡,映入眼簾的又是一大坨代碼,但是我們隻關心觀點的部分。<code></code>

一眼就能看出是通過<code></code>

有時候不得不佩服這些外國人起名字的功力,把掃描出來的類叫做candidates(候選人);真是不服不行啊,這種名字真的很容易了解有不有?哈哈,貌似扯遠了。繼續往下看。這裡隻列出方法的主題部分。<code></code>

先看這兩句:<code></code>

假設我們配置的需要掃描的包名為<code>com.geeekr.service</code>,那麼<code>packagesearchpath</code>的值就是<code>classpath*:com.geeekr.service/**/*.class</code>,意思就是com.geeekr.service包(包括子包)下所有class檔案;如果配置的是<code>*</code>,那麼<code>packagesearchpath</code>的值就是<code>classpath*:*/**/*.class</code>。這裡的表達式是spring自己定義的。spring會根據這種表達式找出相關的class檔案。<code></code>

這一句就把相關class檔案加載出來了,那我們就要看看,spring究竟是如何把class檔案找到的了。首先看看<code>resourcepatternresolver</code>的定義:<code></code>

進入<code>getresources</code>方法<code></code>

這裡會先判斷表達式是否以<code>classpath*:</code>開頭。前面我們看到spring已經給我們添加了這個頭,這裡當然符合條件了。接着會進入<code>findpathmatchingresources</code>方法。在這裡又把<code>**/*.class</code>去掉了,然後在調用<code>getresources</code>方法,然後在進入<code>findallclasspathresources</code>方法。這裡的參數隻剩下包名了例如<code>com/geeekr/service/</code>。<code></code>

真相大白了,spring也是用的<code>classloader</code>加載的class檔案。一路追蹤,原始的classloader是<code>thread.currentthread().getcontextclassloader();</code>。到此為止,就拿到class檔案了。

spring會将class資訊封裝成<code>beandefinition</code>,然後再放進<code>defaultlistablebeanfactory</code>的<code>beandefinitionmap</code>中。

拿到了class檔案後,就要看看spring是如何裝配bean的了,下一節,繼續看。