spring啟動component-scan類掃描載入過程
有朋友最近問到了 spring 載入類的過程,尤其是基於 annotation 註解的載入過程,有些時候如果由於某些系統部署的問題,載入不到,很是不解!就針對這個問題,我這篇部落格說說spring啟動過程,用原始碼來說明,這部分內容也會在書中出現,只是表達方式會稍微有些區別,我將使用spring 3.0的版本來說明(雖然版本有所區別,但是變化並不是特別大),另外,這裡會從WEB中使用spring開始,中途會穿插自己通過newClassPathXmlApplicationContext 的區別和聯絡。
要看這部分原始碼,其實在spring 3.0以上大家都 一般 會配置一個Servelet,如下所示:
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有這個方法,如下圖所示:
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 是如何載入的了。
這裡的parent基本不用管,因為Root的 ApplicationContext 的資訊還根本沒建立,所以主要是看createWebApplicationContext這個方法,進去後,該方法前面部分,都是在設定一些相關的引數,例如我們需要將WEB容器、以及容器的配置資訊設定進去,然後會呼叫一個 refresh() 方法,這個方法表面上是用來重新整理的,其實也是用來做初始化bean用的,也就是配置修改後,如果你能呼叫它的這個方法,就可以重新裝載 spring 的資訊,我們看看原始碼中的片段如下(同樣,不相關的部分,我們就不貼太多了):
其實這個方法,不論是通過 ClassPathXmlApplicationContext 還是WEB裝載都會呼叫這裡,我們看下 ClassPathXmlApplicationContext 中呼叫的部分:
他們的區別在於, web 容器中,用 servlet 裝載了, servlet 中包裝了一個XmlWebApplicationContext 而已,而 ClassPathXmlApplicationContext 是直接呼叫的,他們共同點是,不論是 XmlWebApplicationContext 、還是ClassPathXmlApplicationContext 都繼承了類(間接繼承):
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的原始碼部分的內容,如果這裡可以獲取到, 且路徑是正確的,一般情況下,都是可以載入到類的。