文章轉自 http://www.it165.net/pro/html/201406/15205.htmlhtml
有朋友最近問到了 spring 加載類的過程,尤爲是基於 annotation 註解的加載過程,有些時候若是因爲某些系統部署的問題,加載不到,非常不解!就針對這個問題,我這篇博客說說spring啓動過程,用源碼來講明,這部份內容也會在書中出現,只是表達方式會稍微有些區別,我將使用spring 3.0的版原本說明(雖然版本有所區別,可是變化並非特別大),另外,這裏會從WEB中使用spring開始,中途會穿插本身經過newClassPathXmlApplicationContext 的區別和聯繫。java
要看這部分源碼,其實在spring 3.0以上你們都 通常 會配置一個Servelet,以下所示:web
1.
<servlet>
2.
<servlet-name>spring</servlet-name>
3.
<servlet-
class
>org.springframework.web.servlet.DispatcherServlet</servlet-
class
>
4.
<load-on-startup>
1
</load-on-startup>
5.
</servlet>
固然 servlet 的名字決定了,你本身獲取 SpringContext 的方式,在前面文章:《spring裏頭各類獲取ApplicationContext的方法 》有詳細的說明,這裏就不細說了,咱們就經過DispatcherServlet來講明和跟蹤(注意咱們這裏不說請求轉發,就說bean的加載過程),咱們知道servlet的規範中,若是load-on-startup被設定了,那麼就會被初始化的時候裝載,而servlet裝載時會調用其 init ()方法,那麼天然是調用 DispatcherServlet 的 init 方法,經過源碼一看,居然沒有,可是並不帶表真的沒有,你會發如今父類的父類中:org.springframework.web.servlet.HttpServletBean有這個方法,以下圖所示:spring
01.
public
final
void
init()
throws
ServletException {
02.
if
(logger.isDebugEnabled()) {
03.
logger.debug(
"Initializing servlet '"
+ getServletName() +
"'"
);
04.
}
05.
06.
// Set bean properties from init parameters.
07.
try
{
08.
PropertyValues pvs =
new
ServletConfigPropertyValues(getServletConfig(),
this
.requiredProperties);
09.
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(
this
);
10.
ResourceLoader resourceLoader =
new
ServletContextResourceLoader(getServletContext());
11.
bw.registerCustomEditor(Resource.
class
,
new
ResourceEditor(resourceLoader));
12.
initBeanWrapper(bw);
13.
bw.setPropertyValues(pvs,
true
);
14.
}
15.
catch
(BeansException ex) {
16.
logger.error(
"Failed to set bean properties on servlet '"
+ getServletName() +
"'"
, ex);
17.
throw
ex;
18.
}
19.
20.
// Let subclasses do whatever initialization they like.
21.
initServletBean();
22.
23.
if
(logger.isDebugEnabled()) {
24.
logger.debug(
"Servlet '"
+ getServletName() +
"' configured successfully"
);
25.
}
26.
}
注意代碼: initServletBean(); 其他的都和加載bean關係並非特別大,跟蹤進去會發I發現這個方法是在類:org.springframework.web.servlet. FrameworkServlet類中(是 DispatcherServlet 的父類、 HttpServletBean 的子類 ),內部經過調用 initWebApplicationContext ()來初始化一個 WebApplicationContext ,源碼片斷(篇幅所限,不拷貝全部源碼,僅僅截取片斷)數組
接下來須要知道的是如何初始化這個context的(按照使用習慣,其實只要獲得了ApplicationContext,就獲得了bean的信息,因此在初始化ApplicationCotext的時候,就已經初始化好了bean的信息,至少至少,它初始化好了bean的路徑,以及描述信息),因此咱們一旦知道 ApplicationCotext 是怎麼初始化的,就基本知道bean 是如何加載的了。tomcat
這裏的parent基本不用管,由於Root的 ApplicationContext 的信息還根本沒建立,因此主要是看createWebApplicationContext這個方法,進去後,該方法前面部分,都是在設置一些相關的參數,例如咱們須要將WEB容器、以及容器的配置信息設置進去,而後會調用一個 refresh() 方法,這個方法表面上是用來刷新的,其實也是用來作初始化bean用的,也就是配置修改後,若是你能調用它的這個方法,就能夠從新裝載 spring 的信息,咱們看看源碼中的片斷以下(一樣,不相關的部分,咱們就不貼太多了):app
其實這個方法,不管是經過 ClassPathXmlApplicationContext 仍是WEB裝載都會調用這裏,咱們看下 ClassPathXmlApplicationContext 中調用的部分:框架
他們的區別在於, web 容器中,用 servlet 裝載了, servlet 中包裝了一個XmlWebApplicationContext 而已,而 ClassPathXmlApplicationContext 是直接調用的,他們共同點是,不管是 XmlWebApplicationContext 、仍是ClassPathXmlApplicationContext 都繼承了類(間接繼承):ide
AbstractApplicationContext ,這個類中的 refresh() 方法是共用的,也就是他們都調用的這個方法來加載 bean 的,在這個方法中,經過obtainFreshBeanFactory方法來構造 beanFactory 的,以下圖所示:測試
是否是看到一層調用一層很煩人,其實回過頭來想想,它沒一層都有本身的處理動做,畢竟spring不是簡單的作一個bean加載,即便是這樣,咱們最少也須要作xml解析、類裝載和實例化的過程,每一個步驟可能都有不少需求,所以分離設計,使得代碼更加具備擴展性,咱們繼續來看 obtainFreshBeanFactory 方法的描述:
這裏不少人可能會不太注意 refreshBeanFactory ()這個方法,尤爲是第一遍看這個代碼的,若是你忽略掉,你可能會找不到bean在哪裏加載的,前面提到了refresh 其實能夠用以初始化,這裏也是這樣, refreshBeanFactory 若是沒有初始化 beanFactory 就是初始化它了,後面你看到的都是 getBeanFactory 的代碼,也就是已經初始化好了,這個refreshBeanFactory方法類AbstractRefreshableApplicationContext 中的方法,它是AbstractApplicationContext 的子類,一樣 不管是 XmlWebApplicationContext、仍是 ClassPathXmlApplicationContext 都繼承了它,所以都能調用到這個同樣的初始化方法,來看看body部分的代碼:
注意第一個紅圈圈住的地方,是建立了一個beanFactory,而後下面的方法能夠經過名稱就能看出是「 加載bean的定義 」,將beanFactory傳入,天然要加載到beanFactory中了,createBeanFactory就是實例化一個beanFactory沒別的,咱們要看的是bean在哪裏加載的,如今貌似還沒看到重點,繼續跟蹤
loadBeanDefinitions (DefaultListableBeanFactory)方法
它由 AbstractXmlApplicationContext 類中的方法實現,web項目中將會由類:XmlWebApplicationContext 來實現,其實差很少,主要是看啓動文件是在那裏而已,若是在非web類項目中沒有自定義的XmlApplicationContext,那麼其實功能能夠參考 XmlWebApplicationContext ,能夠認爲是同樣的功能。那麼看看loadBeanDefinitions方法以下:
這裏有一個XmlBeanDefineitionReader,是讀取XML中spring的相關信息(也就是解析SpringContext.xml的),這裏經過 getConfigLocations() 獲取到的就是這個或多個文件的路徑,會循環,經過 XmlBeanDefineitionReader 來解析,跟蹤到loadBeanDefinitions方法裏面,會發現方法實現體在 XmlBeanDefineitionReader的父類:AbstractBeanDefinitionReader中,代碼以下:
這裏你們會疑惑,爲啥裏面還有一個 loadBeanDefinitions ,你們要知道,咱們目前只解析到咱們的springContext.xml在哪裏,可是還沒解析到 springContext.xml的內容是什麼,可能有多個spring的配置文件,這裏會出現多個Resource,因此是一個數組(這裏如何經過location找到文件部分,在咱們找class的時候天然明瞭,你們先不糾結這個問題)。
接下來有不少層調用,會以此調用:
AbstractBeanDefinitionReader.loadBeanDefinitions(Resources []) 循環Resource數組,調用方法:
XmlBeanDefinitionReader.loadBeanDefinitions(Resource ) 和上面這個類是父子關係,接下來會作: doLoadBeanDefinitions、registerBeanDefinitions 的操做,在註冊beanDefinitions的時候,其實就是要真正開始解析XML了
它調用了 DefaultBeanDefinitionDocumentReader 類的registerBeanDefinitions方法,以下圖所示:
中間有解析XML的過程,可是貌似咱們不是很關心,咱們就關係類是怎麼加載的,雖然已經到XML解析部分了,因此主要看parseBeanDefinitions這個方法,裏面會調用到BeanDefinitionParserDelegate類的parseCustomElement方法,用來解析bean的信息:
z
這裏解析了XML的信息,跟蹤進去,會發現用了 NamespaceHandlerSupport 的parse方法,它會根據節點的類型,找到一種合適的解析BeanDefinitionParser(接口) ,他們預先被spring註冊好了,放在一個HashMap中,例如咱們在spring 的annotation掃描中,一般會配置:
1.
<context:component-scan base-
package
=
"com.xxx"
/>
此時根據名稱「 component-scan 」就會找到對應的解析器來解析,而與之對應的就是 ComponentScanBeanDefinitionParser 的 parse 方法,這地方已經很明顯有掃描bean的概念在裏面了,這裏的parse獲取到後,中間有一個很是很是關鍵的步驟那就是定義了 ClassPathBeanDefinitionScanner 來掃描類的信息,它掃描的是什麼?是加載的類仍是class文件呢?答案是後者,爲什麼,由於有些類在初始化化時根本還沒被加載,ClassLoader根本還沒加載,只是ClassLoader能夠找到這些class的路徑而已:
注意這裏的scanner建立後,最關鍵的是 doScan 的功能,解析XML我想來看這個的不是問題,若是還不熟悉能夠先看看,那麼咱們獲得了相似: com.xxx 這樣的信息,就要開始掃描類的列表,那麼再哪裏掃描呢?這裏的doScan返回了一個Set<BeanDefinitionHolder> 咱們感到但願就在不遠處,進去看看 doScan 方法。
咱們看到這麼大一坨代碼,其實咱們目前不關心的代碼,暫時能夠無論,咱們就看怎麼掃描出來的,能夠看出最關鍵的掃描代碼是:findCandidateComponents(String basePackage) 方法,也就是經過每一個basePackage 去找到有那些類是匹配的,咱們這裏假如配置了 com.abc ,或配置了 * 兩種狀況說明。
主要看紅線部分,下面非紅線部分,是已經拿到了類的定義,紅線部分,會組裝信息,若是咱們配置了 com.abc會組裝爲: classpath*:com/abc/**/*.class ,若是配置是 * ,那麼將會被組裝爲 classpath*:*/**/*.class ,可是這個好像和咱們用的東西不太同樣,java中也沒見這種URL能夠獲取到,spring究竟是怎麼搞的呢?就要看第二個紅線部分的代碼:
1.
Resource[] resources =
this
.resourcePatternResolver.getResources(packageSearchPath);
它居然神奇般的經過這個路徑獲取到了URL,你一旦跟蹤你會發現,獲取出來的全是.class的路徑,包括jar包中的相關class路徑,這裏有些細節,咱們先不說,先看下這個resourcePatternResolover是什麼類型的,看到定義部分是:
1.
private
ResourcePatternResolver resourcePatternResolver =
new
PathMatchingResourcePatternResolver();
爲此胖哥還將其作了一個測試,用一個簡單main方法寫了一段:
1.
ResourcePatternResolver resourcePatternResolver =
new
PathMatchingResourcePatternResolver();
2.
3.
Resource[] resources = resourcePatternResolver.getResources(
"classpath*:com/abc/**/*.class"
);
獲取出來的果真是那樣,胖哥開始猜想,這個和ClassLoader的getResource方法有關係了,由於太相似了,咱們跟蹤進去看下:
這個 CLASSPATH_ALL_URL_PREFIX 就是字符串 classpath*: , 咱們傳遞參數進來的時候,天然會走第一個紅圈圈住部分的代碼,可是第二個紅圈圈住部分的代碼是幹嗎的呢,胖哥告訴你先知道有這個,而後回頭會有用,繼續找findPathMatchingResources 方法,好了,愈來愈接近真相了。
這裏有一個 rootDirPath ,這個地方有個容易出錯的,是若是你配置的是 com.abc,那麼 rootDirPath 部分應該是: classpath*:com/abc/ 而若是配置是 * 那麼 classpath*: 只有這個結果,而不是 classpath*:* (這裏我就不說截取字符串的源碼了),回到上一段代碼,這裏再次調用了getResources(String)方法,又回到前面一個方法,這一次,依然是以classpath*:開頭,因此第一層 if 語句會進去,而第二層不會,爲何?在裏面的isPattern() 的實現中是這樣寫的:
1.
public
boolean
isPattern(String path) {
2.
return
(path.indexOf(
'*'
) != -
1
|| path.indexOf(
'?'
) != -
1
);
3.
}
在匹配前,作了一個 substring 的操做,會將「 classpath*: 」這個字符串去掉,若是是配置的是com.abc就變成了"com/abc/",而若是配置爲*,那麼獲得的就是「」 ,也就是長度爲0的字符串,所以在咱們的這條路上,這個方法返回的是false,就會走到代碼段 findAllClassPathResources 中,這就是爲何上面提到會有用途的緣由,好了,最最最最關鍵的地方來了哦。例如咱們知道了一個com/abc/爲前綴,此時要知道相關的classpath下面有哪些class是匹配的,如何作?天然用ClassLoader,咱們看看Spring是否是這樣作的:
果真不出所料,它也是用ClassLoader,只是它本身提供的getClassLoader()方法,也就是和spring的類使用同一個加載器範圍內的,以保證能夠識別到同樣的classpath,本身模擬的時候,能夠用一個類
類名.class.getClassLoader().getResources("")
若是放爲空,那麼就是獲取classpath的相關的根路徑(classpath可能有不少,可是根路徑,能夠被合併),也就是若是你配置的*,獲取到的將是這個,也許你在web項目中,你會獲取到項目的根路徑(classes下面,以及tomcat的lib目錄)。
若是寫入一個: com/abc/ 那麼獲得的將是掃描相關classpath下面全部的class和jar包中與之匹配的類名(前綴部分)的路徑信息,可是須要注意的是,若是有 兩層jar包 ,而你想要掃描的類或者說想要經過spring加載的類在 第二層jar包中 ,這個方法是獲取不到的,這不是spring沒有去作這個事情,而是,java提供的getResources方法就是這樣的,有朋友問個人時候,正好遇到了相似的事情,另外須要注意的是, getResources 這個方法是包含當前路徑的一個遞歸文件查找(通常環境變量中都會配置 . ),因此若是是一個jar包,你要運行的話,切記放在某個根目錄來跑,由於當前目錄,就是根目錄也會被遞歸下去,你的程序會被莫名奇怪地慢。
這裏你們還能夠經過如下簡單的方式來測試調用路徑的問題:
1.
ClassPathScanningCandidateComponentProvider provider =
new
ClassPathScanningCandidateComponentProvider(
true
);
2.
Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents(
"com/abc"
);
3.
for
(BeanDefinition beanDefinition : beanDefinitions) {
4.
System.out.println(beanDefinition.getBeanClassName()
5.
+
" "
+ beanDefinition.getResourceDescription()
6.
+
" "
+ beanDefinition.getClass());
7.
}
這是直接引用spring的源碼部分的內容,若是這裏能夠獲取到, 且路徑是正確的,通常狀況下,都是能夠加載到類的。
看了這麼多,是否是有點暈,不要緊,誰第一回看都這樣,當你下一次看的時候,有個思路就行了,我這裏並無像UML同樣理出他們的層次關係,和調用關係,僅僅針對代碼調用逐層來講明,你們若是初步看就是,由Servlet初始化來建立ApplicationContext,在設置了Servelt相關參數後,獲取servlet的配置文件路徑或本身指定的配置文件路徑(applicationContext.xml或其餘的名字,能夠一個或多個),而後經過系列的XML解析,以及針對每種不一樣的節點類型使用不一樣的加載方式,其中 component-scan 用於指定掃描類的對應有一個Scanner,它會經過ClassLoader的getResources方法來獲取到class的路徑信息,那麼class的路徑都能獲取到,類的什麼還拿不到呢?呵呵!
好,本文基本內容就說到這裏,接下來我會提到 spring MVC 的中的簡單跳轉的解析,其中有部分源碼是這裏看過的,只是還不是這裏的重點而已。
而我想說的也是這點,其實本文雖然在說啓動,其實有不少代碼也沒說,由於那樣的話我就是一個複製咱貼機了;
其實看源碼,要帶着目的,你們要知道主體狀況或實現的功能,不要就看源碼而看源碼,一個是根本記不下來,另外一個這樣看代碼沒有太大的意義。
當你有了疑問,遇到了難題不知道緣由,或發現了新大陸,頗有興趣,那麼去看看,也許看以前你會思考下若是我來實現會怎麼作?再看看別人是怎麼作的,有何區別,不斷吸收這些開源框架中優秀的品質,包括代碼的設計層次,瞭解它用到了什麼,爲什麼要這樣設計,那麼你的代碼相信會愈來愈漂亮,你對開源界的代碼也會愈來愈熟悉,熟悉得像本身親人同樣,呵呵。