摘要:本文結合《Spring源碼深度解析》來分析Spring 5.0.6版本的源代碼。若有描述錯誤之處,歡迎指正。
當把文件轉換為Document后,接下來的提取及注冊bean就是我們的重頭戲。繼續上一篇的分析,當程序已經擁有XML文檔文件的Document實例對象時,就會被引入XmlBeanDefinitionReader的這個方法。
/** * Register the bean definitions contained in the given DOM document. * Called by {@code loadBeanDefinitions}. * <p>Creates a new instance of the parser class and invokes * {@code registerBeanDefinitions} on it. * @param doc the DOM document * @param resource the resource descriptor (for context information) * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of parsing errors * @see #loadBeanDefinitions * @see #setDocumentReaderClass * @see BeanDefinitionDocumentReader#registerBeanDefinitions */ public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { // 使用DefaultBeanDefinitionDocumentReader實例化BeanDefinitionDocumentReader BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); // 實例化BeanDefinitionReader時候會將BeanDefinitionRegistry傳入,默認使用繼承自DefaultListableBeanFactory的子類 int countBefore = getRegistry().getBeanDefinitionCount(); // 加載及注冊bean documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); // 記錄本次加載的BeanDefinition個數 return getRegistry().getBeanDefinitionCount() - countBefore; }
其中的參數doc是通過上一節loadDocument加載轉換出來的。在這個方法中很好地應用了面向對象單一職責的原則,將邏輯處理委托給單一的類進行處理,而這個邏輯處理類就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一個接口,而實例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通過此方法, BeanDefinitionDocumentReader真正的類型其實已經是DefaultBeanDefinitionDocumentReader了,進入DefaultBeanDefinitionDocumentReader后,發現這個方法的重要目的之一就是提取root,以便再次將root作為參數繼續BeanDefinition的注冊。
/** * This implementation parses bean definitions according to the "spring-beans" XSD * (or DTD, historically). * <p>Opens a DOM Document; then initializes the default settings * specified at the {@code <beans/>} level; then parses the contained bean definitions. */ @Override public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; logger.debug("Loading bean definitions"); Element root = doc.getDocumentElement(); doRegisterBeanDefinitions(root); }
經過艱難險阻,磕磕絆絆,我們終於到了核心邏輯的底部doRegisterBeanDefinitions(root),至少我們在這個方法中看到了希望。
如果說以前一直是XML加載解析的准備階段,那么doRegisterBeanDefinitions算是真正地開始進行解析了,我們期待的核心部分真正開始了。
/** * Register each bean definition within the given root {@code <beans/>} element. */ @SuppressWarnings("deprecation") // for Environment.acceptsProfiles(String...) protected void doRegisterBeanDefinitions(Element root) { // Any nested <beans> elements will cause recursion in this method. In // order to propagate and preserve <beans> default-* attributes correctly, // keep track of the current (parent) delegate, which may be null. Create // the new (child) delegate with a reference to the parent for fallback purposes, // then ultimately reset this.delegate back to its original (parent) reference. // this behavior emulates a stack of delegates without actually necessitating one. // 專門處理解析 BeanDefinitionParserDelegate parent = this.delegate; this.delegate = createDelegate(getReaderContext(), root, parent); if (this.delegate.isDefaultNamespace(root)) { // 處理profile屬性 String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); // We cannot use Profiles.of(...) since profile expressions are not supported // in XML config. See SPR-12458 for details. if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { if (logger.isDebugEnabled()) { logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + getReaderContext().getResource()); } return; } } } /* 這里是模板方法模式 */ // 解析前處理,留給子類實現 preProcessXml(root); parseBeanDefinitions(root, this.delegate); // 解析后處理,留給子類實現 postProcessXml(root); this.delegate = parent; }
通過上面的代碼我們看到了處理流程,首先是對profile的處理,然后開始進行解析,可以當我們跟進preProcessXml(root)和postProcessXml(root)發現代碼是空的,既然是空的寫着還有什么用呢?就像面向對象設計方法學中常說的一句話,一個類要么是面向繼承設計的,要么就用final修飾。在DefaultBeanDefinitionDocumentReader中並沒有用final修飾,所以它是面向繼承設計的。這兩個方法正是為子類而設計的,如果讀者有了解過設計模式,可以很快速地反應出這是模板方法模式,如果繼承自DefaultBeanDefinitionDocumentReader的子類需要在Bean解析前后做一些處理的話,那么只需要重寫這兩個方法就可以了。
1. profile屬性的使用
我們注意到在注冊bean的最開始是對PROFILE_ATTRIBUTE屬性的解析,可能對於我們來說,profile屬性並不是很常用。讓我們先了解一下這個屬性。
分析profile前我們先了解下profile的用法,官方實例代碼片段如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <beans profile="dev"> ...... </beans> <beans profile="production"> ...... </beans> </beans>
集成到Web環境中時,在web.xml中加入以下代碼:
<context-param> <param-name>Spring.profiles.active</param-name> <param-value>dev</param-value> </context-param>
有了這個特性我們就可以在配置文件中部署兩套配置來適用於生產環境和開發環境,這樣可以方便的進行切換開發、部署環境,最常用的就是更換不同的數據庫。
了解了profile的使用再來分析代碼會清晰很多,首先程序會獲取beans節點是否定義了profile屬性,如果定義了則會需要到環境變量中去尋找,所以這里首先斷言environment不可能為空,因為profile是可以同時指定多個的,需要程序對其拆分,並解析每個profile是都符合環境變量中所定義的,不定義則不會浪費性能去解析。
2. 解析並注冊BeanDefinition
處理了profile后就可以進行XML的讀取了,跟蹤代碼進入parseBeanDefinitions(root, this.delegate)。
/** * Parse the elements at the root level in the document: * "import", "alias", "bean". * @param root the DOM root element of the document */ protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { // 對beans的處理 if (delegate.isDefaultNamespace(root)) { NodeList nl = root.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { Element ele = (Element) node; if (delegate.isDefaultNamespace(ele)) { // 對bean的處理 parseDefaultElement(ele, delegate); } else { // 對bean的處理 delegate.parseCustomElement(ele); } } } } else { delegate.parseCustomElement(root); } }
上面的代碼看起來邏輯還是蠻清晰的,因為在Spring的XML配置里面有兩大類Bean聲明,一個是默認的,如:
<bean id="mySpringBean" class="org.cellphone.uc.MySpringBean"/>
另一類就是自定義的,如:
<tx:annotation-driven/>
而這兩種的讀取及解析差別是非常大的,如果采用Spring默認的配置,Spring當然知道該怎么做,但是如果是自定義的,那么就需要用戶實現一些接口和配置了。對於根節點或者子節點如果是默認命名空間的話則采用parseDefaultElement方法進行解析,否則使用delegate.parseCustomElement(ele)方法對自定義命名空間進行解析。而判斷是否默認命名空間還是自定義命名空間的辦法是使用node.getNamespaceURI()獲取命名空間,並與Spring中固定的命名空間http://www.springframework.org/schema/beans進行比對。如果一致則認為是默認,否則就認為是自定義。而對於默認標簽解析與自定義標簽解析我們將會在下一篇中進行討論。