Spring component-scan類掃描加載過程

 https://github.com/javahongxihtml

      有朋友最近問到了spring加載類的過程,尤爲是基於annotation註解的加載過程,有些時候若是因爲某些系統部署的問題,加載不到,非常不解!就針對這個問題,我這篇博客說說spring啓動過程,用源碼來講明,這部份內容也會在書中出現,只是表達方式會稍微有些區別,我將使用spring 3.0的版原本說明(雖然版本有所區別,可是變化並非特別大),另外,這裏會從WEB中使用spring開始,中途會穿插本身經過newClassPathXmlApplicationContext的區別和聯繫。java

 

要看這部分源碼,其實在spring 3.0以上你們都通常會配置一個Servelet,以下所示:git

 

[html]  view plain  copy
 
  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()方法,那麼天然是調用DispatcherServletinit方法,經過源碼一看,居然沒有,可是並不帶表真的沒有,你會發如今父類的父類中:org.springframework.web.servlet.HttpServletBean有這個方法,以下圖所示:github

 

 

[java]  view plain  copy
 
  1.     public final void init() throws ServletException {  
  2.     if (logger.isDebugEnabled()) {  
  3.         logger.debug("Initializing servlet '" + getServletName() + "'");  
  4.     }  
  5.   
  6.     // Set bean properties from init parameters.  
  7.     try {  
  8.         PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);  
  9.         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,源碼片斷(篇幅所限,不拷貝全部源碼,僅僅截取片斷)web

 


接下來須要知道的是如何初始化這個context的(按照使用習慣,其實只要獲得了ApplicationContext,就獲得了bean的信息,因此在初始化ApplicationCotext的時候,就已經初始化好了bean的信息,至少至少,它初始化好了bean的路徑,以及描述信息),因此咱們一旦知道ApplicationCotext是怎麼初始化的,就基本知道bean是如何加載的了。spring


這裏的parent基本不用管,由於Root的ApplicationContext的信息還根本沒建立,因此主要是看createWebApplicationContext這個方法,進去後,該方法前面部分,都是在設置一些相關的參數,例如咱們須要將WEB容器、以及容器的配置信息設置進去,而後會調用一個refresh()方法,這個方法表面上是用來刷新的,其實也是用來作初始化bean用的,也就是配置修改後,若是你能調用它的這個方法,就能夠從新裝載spring的信息,咱們看看源碼中的片斷以下(一樣,不相關的部分,咱們就不貼太多了):數組


其實這個方法,不管是經過ClassPathXmlApplicationContext仍是WEB裝載都會調用這裏,咱們看下ClassPathXmlApplicationContext中調用的部分:tomcat


他們的區別在於,web容器中,用servlet裝載了,servlet中包裝了一個XmlWebApplicationContext而已,而ClassPathXmlApplicationContext是直接調用的,他們共同點是,不管是XmlWebApplicationContext、仍是ClassPathXmlApplicationContext都繼承了類(間接繼承):app

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掃描中,一般會配置:

 

[html]  view plain  copy
 
  1. <context:component-scan base-package="com.xxx" />  

 

 

此時根據名稱「component-scan」就會找到對應的解析器來解析,而與之對應的就是ComponentScanBeanDefinitionParserparse方法,這地方已經很明顯有掃描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究竟是怎麼搞的呢?就要看第二個紅線部分的代碼:

 

[java]  view plain  copy
 
  1. Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);  

 

 

它居然神奇般的經過這個路徑獲取到了URL,你一旦跟蹤你會發現,獲取出來的全是.class的路徑,包括jar包中的相關class路徑,這裏有些細節,咱們先不說,先看下這個resourcePatternResolover是什麼類型的,看到定義部分是:

 

[java]  view plain  copy
 
  1. private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();  

 

 

爲此胖哥還將其作了一個測試,用一個簡單main方法寫了一段:

 

[java]  view plain  copy
 
  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() 的實現中是這樣寫的:

 

[java]  view plain  copy
 
  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包,你要運行的話,切記放在某個根目錄來跑,由於當前目錄,就是根目錄也會被遞歸下去,你的程序會被莫名奇怪地慢。

回到上面的代碼中,在findPathMatchingResources中咱們這裏剛剛獲取到base的路徑列表,也就是全部包含相似com/abc/爲前綴的路徑,或classpath合併後的目錄根路徑;此時咱們須要下面全部的class,那麼就須要的是遞歸,這裏我就再也不跟蹤了,你們能夠本身去跟蹤裏面的幾個方法調用:doFindPathMatchingJarResources、doFindPathMatchingFileResources 。

幾乎不會用到:VfsResourceMatchingDelegate.findMatchingResources,因此主要是上面兩個,分別是jar包中的和工程裏面的class,跟蹤進去會發現,代碼會不斷遞歸循環調用目錄路徑下的class文件的路徑信息,最終會拿到相關的class列表信息,可是這些class還並無作檢測是否有annotation,那是下一步作的事情,可是下一個步驟已經很簡單了,由於要檢測一個類的annotation,在前面的文章中:《 java之annotation與框架的那些祕密》中已經提到了。

 

這裏你們還能夠經過如下簡單的方式來測試調用路徑的問題:

 

[java]  view plain  copy
 
  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.                     + "\t" + beanDefinition.getResourceDescription()  
  6.                     + "\t" + beanDefinition.getClass());  
  7. }  

 

 

這是直接引用spring的源碼部分的內容,若是這裏能夠獲取到,且路徑是正確的,通常狀況下,都是能夠加載到類的。

 

看了這麼多,是否是有點暈,不要緊,誰第一回看都這樣,當你下一次看的時候,有個思路就行了,我這裏並無像UML同樣理出他們的層次關係,和調用關係,僅僅針對代碼調用逐層來講明,你們若是初步看就是,由Servlet初始化來建立ApplicationContext,在設置了Servelt相關參數後,獲取servlet的配置文件路徑或本身指定的配置文件路徑(applicationContext.xml或其餘的名字,能夠一個或多個),而後經過系列的XML解析,以及針對每種不一樣的節點類型使用不一樣的加載方式,其中component-scan用於指定掃描類的對應有一個Scanner,它會經過ClassLoader的getResources方法來獲取到class的路徑信息,那麼class的路徑都能獲取到,類的什麼還拿不到呢?呵呵!

相關文章
相關標籤/搜索