1. 程式人生 > >spring啟動component-scan類掃描載入過程

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.classnew 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的原始碼部分的內容,如果這裡可以獲取到, 且路徑是正確的,一般情況下,都是可以載入到類的。