spring啟動component-scan類掃描加載過程---源碼分析
最近因為寫書的事情,一段時間沒有寫博客了,有朋友最近問到了spring加載類的過程,尤其是基於annotation注解的加載過程,有些時候如果由於某些係統部署的問題,加載不到,很是不解!就針對這個問題,我這篇博客說說spring啟動過程,用源碼來說明,這部分內容也會在書中出現,隻是表達方式會稍微有些區別,我將使用spring 3.0的版本來說明(雖然版本有所區別,但是變化並不是特別大),另外,這裏會從WEB中使用spring開始,中途會穿插自己通過new ClassPathXmlApplicationContext的區別和聯係。
要看這部分源碼,其實在spring 3.0以上大家都一般會配置一個Servelet,如下所示:
<servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>當然servlet的名字決定了,你自己獲取SpringContext的方式,在前麵文章:《spring裏頭各種獲取ApplicationContext的方法》有詳細的說明,這裏就不細說了,我們就通過DispatcherServlet來說明和跟蹤(注意我們這裏不說請求轉發,就說bean的加載過程),我們知道servlet的規範中,如果load-on-startup被設定了,那麼就會被初始化的時候裝載,而servlet裝載時會調用其init()方法,那麼自然是調用DispatcherServlet的init方法,通過源碼一看,竟然沒有,但是並不帶表真的沒有,你會發現在父類的父類中:org.springframework.web.servlet.HttpServletBean有這個方法,如下圖所示:
public final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Initializing servlet '" + getServletName() + "'"); } // Set bean properties from init parameters. try { PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader)); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); throw ex; } // Let subclasses do whatever initialization they like. initServletBean(); if (logger.isDebugEnabled()) { logger.debug("Servlet '" + getServletName() + "' configured successfully"); } }注意代碼: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掃描中,通常會配置:
<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到底是怎麼搞的呢?就要看第二個紅線部分的代碼:
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
它竟然神奇般的通過這個路徑獲取到了URL,你一旦跟蹤你會發現,獲取出來的全是.class的路徑,包括jar包中的相關class路徑,這裏有些細節,我們先不說,先看下這個resourcePatternResolover是什麼類型的,看到定義部分是:
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
為此胖哥還將其做了一個測試,用一個簡單main方法寫了一段:
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); 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() 的實現中是這樣寫的:
public boolean isPattern(String path) { return (path.indexOf('*') != -1 || path.indexOf('?') != -1); }
在匹配前,做了一個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與框架的那些秘密》中已經提到了。
這裏大家還可以通過以下簡單的方式來測試調用路徑的問題:
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents("com/abc"); for(BeanDefinition beanDefinition : beanDefinitions) { System.out.println(beanDefinition.getBeanClassName() + "\t" + beanDefinition.getResourceDescription() + "\t" + beanDefinition.getClass()); }
這是直接引用spring的源碼部分的內容,如果這裏可以獲取到,且路徑是正確的,一般情況下,都是可以加載到類的。
看了這麼多,是不是有點暈,沒關係,誰第一回看都這樣,當你下一次看的時候,有個思路就好了,我這裏並沒有像UML一樣理出他們的層次關係,和調用關係,僅僅針對代碼調用逐層來說明,大家如果初步看就是,由Servlet初始化來創建ApplicationContext,在設置了Servelt相關參數後,獲取servlet的配置文件路徑或自己指定的配置文件路徑(applicationContext.xml或其他的名字,可以一個或多個),然後通過係列的XML解析,以及針對每種不同的節點類型使用不同的加載方式,其中component-scan用於指定掃描類的對應有一個Scanner,它會通過ClassLoader的getResources方法來獲取到class的路徑信息,那麼class的路徑都能獲取到,類的什麼還拿不到呢?嗬嗬!
好,本文基本內容就說到這裏,接下來我會提到spring MVC的中的簡單跳轉的解析,其中有部分源碼是這裏看過的,隻是還不是這裏的重點而已。
而我想說的也是這點,其實本文雖然在說啟動,其實有很多代碼也沒說,因為那樣的話我就是一個複製咱貼機了;
其實看源碼,要帶著目的,大家要知道主體情況或實現的功能,不要就看源碼而看源碼,一個是根本記不下來,另一個這樣看代碼沒有太大的意義。
當你有了疑問,遇到了難題不知道原因,或發現了新大陸,很有興趣,那麼去看看,也許看之前你會思考下如果我來實現會怎麼做?再看看別人是怎麼做的,有何區別,不斷吸取這些開源框架中優秀的品質,包括代碼的設計層次,了解它用到了什麼,為何要這樣設計,那麼你的代碼相信會越來越漂亮,你對開源界的代碼也會越來越熟悉,熟悉得像自己親人一樣,嗬嗬。
【對於5樓的回複(CSDN發神經,我回複的內容提示我鏈接過多,其實一個鏈接都沒有,神奇,所以回複在正文)】:
你能看到isCandidateComponent(MetadataReader metadataReader)方法,其實呢,你再跟下應該就有結果了!要細寫可以寫一篇文章,簡單寫下如下:
這個方法裏先循環excludeFilters,再循環includeFilters,excludeFilters默認情況下沒有啥內容,includeFilters默認情況下最少會有一個new AnnotationTypeFilter(Component.class);
也就是默認情況下excludeFilters排除內容不會循環,includeFilters包含內容最少會匹配到AnnotationTypeFilter,調用AnnotationTypeFilter.match方法是其父類AbstractTypeHierarchyTraversingFilter.math()方法,其內部調用matchSelf()調回子類的AnnotationTypeFilter.matchSelf()方法。該方法中用||連接兩個判定分別是hasAnnotation、hasMetaAnnotation,前者判定注解名稱本身是否匹配因此Component肯定能匹配上,後者會判定注解的meta注解是否包含,Service、Controller、Repository注解都注解了Component因此它們會在後者匹配上。這樣match就肯定成立了。
此時類還沒有被裝載,Resource中僅僅是類的目錄信息,Spring也沒有通過ClassLoader將類加載後通過反射讀取類的Annotation信息(這條路也是通的),而是通過自己的asm對類的class字節碼的解析來完成的,這部分是字節碼相關的知識,在我的書中和其它博客有所介紹。spring我想這樣做的目的是方便自己做AOP相關的字節碼增強一帶搞定,也不會多加載不需要的類,因為本文提到的訪問如果寫了目錄,可以訪問到jar包中的內容,可能類信息會比較多。
最後更新:2017-04-03 18:52:12