天天看點

【Spring】IOC核心源碼學習(三):bean标簽和自定義标簽實作原理

[size=large]本文将解析spring bean定義标簽和自定義标簽的解析實作原理。

這裡說的标簽僅限于以xml作為bean定義描述符的spring容器,繼承AbstractXmlApplicationContext的一些子 容器,如XmlApplicationContext、ClassPathXmlApplicationContext、 FileSystemXmlApplicationContext等。同時也僅限于描述schema作為标簽定義的情況。

[b]Spring xml ioc 容器常用标簽和自定義标簽[/b]

以 Xml 資源定義的容器配置是我們最常見的一種方式。

Spring 容器需要解析 xml 的标簽,并把 xml 裡 bean 的定義轉化為内部的結構 BeanDifinition 。

Spring 的标簽有很多種,其支援的常見的标簽有:

标簽 說明 例子

<bean> 最常用的,定義一個普通 bean。

<tx> 如<tx: advice> 等,提供事務配置通用支援。

<tx:advice id="txAdvice" transaction-manager="transactionManager">  
    <tx:attributes>  
        <tx:method name="save*"/>  
        <tx:method name="remove*"/>  
        <tx:method name="*" read-only="true"/>  
   </tx:attributes>  
</tx:advice> 
           

<aop> <aop:config>,<aop: aspectj-autoproxy> 等提供代理 bean 通用配置支援。

<aop:config proxy-target-class="true">    
    <aop:advisor pointcut="..." advice-ref="txAdvice"/>    
    <aop:advisor pointcut="..." advice-ref="fooAdvice"/>    
</aop:config> 
           

<util> 提供在容器内配置一些JDK自帶的工具類、集合類和常量的支援。

<util:list id="list" list-class="java.util.ArrayList">  
  <value>listValue1</value>  
  <value>listValue2</value>  
</util:list>  

<util:map id="map">  
  <entry key="key1"  value="mapValue1"></entry>  
  <entry key="key12" value="mapValue2"></entry>  
</util:map>
           

<p> 屬性的簡單通路

<bean id="loginAction" class="com.test.LoginAction" p:name="test"></bean>
           

<lang> <lang:groovy><lang:jruby>等,提供對動态腳本的支援。

<lang:groovy id="test"  
             refresh-check-delay="5000"  
             script-source="classpath:com/test/groovy/test.groovy">  
</lang:groovy>  
           

<jee > <jee:jndi-lookup/>等,對一些javaEE規範的bean配置的簡化,如jndi等。

<jee:jndi-lookup id="simple"    
             jndi-name="jdbc/MyDataSource"    
             cache="true"    
             resource-ref="true"    
             lookup-on-startup="false"    
             expected-type="com.myapp.DefaultFoo"    
             proxy-inter/>  
           

基本上每一種标簽都是用來定義一類 bean 的(P标簽除外)。以上都是 spring 自帶的一些标簽,當然 spring 也支援自定義标簽。其實 <tx><aop> 這些也可以認為是自定義标簽,不過是由 spring 擴充的而已。

其實所有的bean定義都可以用bean标簽來實作定義的。而衍生這種自定義标簽來定義 bean 有幾個好處:

1. 見名知意。

2. 對于同一類的通用 bean 。封裝不必要的配置,隻給外部暴露一個簡單易用的标簽和一些需要配置的屬性。很多時候對于一個架構通用的 bean ,我們不需要把 bean 的所有配置都暴露出來,甚至像類名、預設值等我們都想直接封裝,這個時候就可以使用自定義标簽了,如: <services:property-placeholder /> 可能這個标簽就預設代表配置了一個支援 property placeholder 的通用 bean ,我們都不需要去知道配這樣一個 bean 的類路徑是什麼。

可以說自定義标簽是 spring 的 xml 容器的一個擴充點,本身 spring 自己的很多标簽也是基于這個設計上面來構造出來的。

[b]Spring 對于自定義(聲明式)bean标簽解析如何設計[/b]

Bean 的定義方式有千千萬萬種,無論是何種标簽,無論是何種資源定義,無論是何種容器,最終的 bean 定義内部表示都将轉換為内部的唯一結構: BeanDefinition 。外部的各種定義說白了就是為了友善配置。

Spring 提供對其支援的标簽解析的天然支援。是以隻要按照 spring 的規範編寫 xml 配置檔案。所有的配置,在啟動時都會正常的被解析成 BeanDefinition 。但是如果我們要實作一個自定義标簽,則需要提供對自定義标簽的全套支援。

我們知道要去完成一個自定義标簽,需要完成的事情有:

1. 編寫自定義标簽 schema 定義檔案,放在某個 classpath 下。

2. 在 classpath 的在 META-INF 下面增加 spring.schemas 配置檔案,指定 schema 虛拟路徑和實際 xsd 的映射。我們在 xml 裡的都是虛拟路徑,如:

頭部的

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           

就是一個虛拟路徑,其對應的真實路徑在spring jar包裡的META-INF/spring.schemas裡面有映射到classpath定義:

http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans-2.5.xsd 
           

3. 增加一個 NamespaceHandler 和 BeanDefinitionParser ,用于解析自定義的标簽,将自定義标簽的 bean 解析成一個 BeanDefinition 傳回。

4. 在 classpath 的在 META-INF 下面增加 spring.handlers 配置檔案,指定标簽命名空間和 handlers 的映射。

為什麼要做以上幾個事情?我們來看看設計:

Spring 對标簽解析的設計的過程如下:

[img]http://dl.iteye.com/upload/attachment/0068/0759/6bba1114-0652-399d-87a6-4c7e94a44b03.jpg[/img]

[b]解釋:[/b]

[b]Step 1[/b]: 将 xml 檔案解析成 Dom 樹。将 xml 檔案解析成 dom 樹的時候,需要 xml 标簽定義 schema 來驗證檔案的文法結構。 Spring 約定将所有的 shema 的虛拟路徑和真是檔案路徑映射定義在 classpath 的在 META-INF/spring.schemas 下面。在容器啟動時 Spring 會掃描所有的 META-INF/spring.schemas 并将映射維護到一個 map 裡。

如 spring jar 包裡會有自帶的标簽的 schemas 映射,可以看一下部配置設定置:

等号左邊是虛拟路徑,右邊是真是路徑(classpath下的)。

虛拟路徑用在我們的bean定義配置檔案裡,如:

<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"  
    xmlns:p="http://www.springframework.org/schema/p"  
    xsi:schemaLocation="  
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd>  
<bean>  
</beans> 
           

beans裡面的

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 
           

就是個虛拟路徑。

[b]Step 2[/b]: 将 dom 樹解析成 BeanDifinition 。将定義 bean 的标簽和 xml 定義解析成 BeanDefinition 的過程。如果是預設的 bean 标簽, spring 會直接進行解析。而如果不是預設的 bean 标簽,包括自定義和 spring 擴充的 <aop> 、 <p> 、 <util> 等标簽,則需要提供專門的 xmlparser 來處理。 paorser由自己定義和編寫,并通過handler注冊到容器。Spring 約定了 META-INF/spring.handlers 檔案,在這裡面定義了标簽命名空間和 handler 的映射。容器起來的時候會加載 handler , handler 會向容器注冊該命名空間下的标簽和解析器。在解析的自定義标簽的時候, spring 會根據标簽的命名空間和标簽名找到一個解析器。由該解析器來完成對該标簽内容的解析,并傳回一個 BeanDefinition 。

以下是 spring jar 包自帶的一些自定義标簽擴充的 spring.handlers 檔案,可以看到定義了 aop\p 等其擴充标簽的 handlers 。

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler  
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler  
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler  
http\://www.springframework.org/schema/jms=org.springframework.jms.config.JmsNamespaceHandler  
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler  
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler  
http\://www.springframework.org/schema/tx=org.springframework.transaction.config.TxNamespaceHandler  
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler 
           

META-INF/spring.handlers

看看UtilNamespaceHandler的代碼實作

public void init() {  
    registerBeanDefinitionParser("constant", new ConstantBeanDefinitionParser());  
    registerBeanDefinitionParser("property-path", new PropertyPathBeanDefinitionParser());  
    registerBeanDefinitionParser("list", new ListBeanDefinitionParser());  
    registerBeanDefinitionParser("set", new SetBeanDefinitionParser());  
    registerBeanDefinitionParser("map", new MapBeanDefinitionParser());  
    registerBeanDefinitionParser("properties", new PropertiesBeanDefinitionParser());  
}  
           

實作了标簽和對應parser的映射注冊。

ListBeanDefinitionParser的實作如下:

這裡父類代碼不貼了,主要完成的是beanDifinition的生成。

源碼實作

Spring 對于自定義(聲明式)bean标簽源碼實作大概的源碼結構如下:

[img]http://dl.iteye.com/upload/attachment/0068/0761/bfb88394-cdc8-32dd-bf05-49f4fd03859e.jpg[/img]

XmlBeanDefinitionReader 是核心類,它接收 spring 容器傳給它的資源 resource 檔案,由它負責完成整個轉換。它調用 DefaultDocumentLoader 來完成将 Resource 到 Dom 樹的轉換。調用 DefaultBeanDefinitionDocumentReader 完成将 Dom 樹到 BeanDefinition 的轉換。

具體的代碼流程細節完全可以基于這個結構去閱讀,下面就貼幾個核心源碼段:

源碼段 1 : 加載 spring.shemas,在PluggableSchemaResolver.java裡實作:

源碼段 2 : 加載 spring.handlers,在 DefaultNamespaceHandlerResolver裡實作:

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {  
    /** 
     * The location to look for the mapping files. Can be present in multiple JAR files. 
     */  
    public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";  

    /** Logger available to subclasses */  
    protected final Log logger = LogFactory.getLog(getClass());  
    /** ClassLoader to use for NamespaceHandler classes */  
    private final ClassLoader classLoader;  
    /** Resource location to search for */  
    private final String handlerMappingsLocation;  
    /** Stores the mappings from namespace URI to NamespaceHandler class name / instance */  
    private Map handlerMappings;  

    public DefaultNamespaceHandlerResolver() {  
        this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION);  
    }  

    public DefaultNamespaceHandlerResolver(ClassLoader classLoader) {  
        this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION);  
    }  

    public DefaultNamespaceHandlerResolver(ClassLoader classLoader, String handlerMappingsLocation) {  
        Assert.notNull(handlerMappingsLocation, "Handler mappings location must not be null");  
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());  
        this.handlerMappingsLocation = handlerMappingsLocation;  
    }  
/**==========中間省略部分代碼=========**/  


       /************************ 
     * Load the specified NamespaceHandler mappings lazily. 
        *  此處加載延遲加載spring.handlers,隻有第一次自定義标簽被解析到,才會被加載。 
        ****************************/  
    private Map getHandlerMappings() {  
        if (this.handlerMappings == null) {  
            try {  
                Properties mappings =  
                        PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);  
                if (logger.isDebugEnabled()) {  
                    logger.debug("Loaded mappings [" + mappings + "]");  
                }  
                this.handlerMappings = new HashMap(mappings);  
            }  
            catch (IOException ex) {  
                IllegalStateException ise = new IllegalStateException(  
                        "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]");  
                ise.initCause(ex);  
                throw ise;  
            }  
        }  
        return this.handlerMappings;  
    }  

} 
           

源碼段3 : xml 到 dom 樹的解析。

在 XmlBeanDefinitionReader .java 的 doLoadBeanDefinitions 方法裡,調用 DefaultDocumentLoader 完成。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)  
            throws BeanDefinitionStoreException {  
        try {  
            int validationMode = getValidationModeForResource(resource);  
            Document doc = this.documentLoader.loadDocument(  
                    inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());  
            return registerBeanDefinitions(doc, resource);  
        }  
        catch (BeanDefinitionStoreException ex) {  
            throw ex;  
        }  
        catch (SAXParseException ex) {  
            throw new XmlBeanDefinitionStoreException(resource.getDescription(),  
                    "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);  
        }  
        catch (SAXException ex) {  
            throw new XmlBeanDefinitionStoreException(resource.getDescription(),  
                    "XML document from " + resource + " is invalid", ex);  
        }  
        catch (ParserConfigurationException ex) {  
            throw new BeanDefinitionStoreException(resource.getDescription(),  
                    "Parser configuration exception parsing XML from " + resource, ex);  
        }  
        catch (IOException ex) {  
            throw new BeanDefinitionStoreException(resource.getDescription(),  
                    "IOException parsing XML document from " + resource, ex);  
        }  
        catch (Throwable ex) {  
            throw new BeanDefinitionStoreException(resource.getDescription(),  
                    "Unexpected exception parsing XML document from " + resource, ex);  
        }  
    } 
           

其中的

getEntityResolver()  
           

會完成spring.schemas的裝載,裡面會間接調用源碼段1。穿進去的entityResolver作為标簽解析使用。

源碼段4 : dom 樹到 Beandifinition:

在 XmlBeanDefinitionReader .java 的 doLoadBeanDefinitions 方法裡,調用 BeanDefinitionDocumentReader 完成。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {  
        // Support old XmlBeanDefinitionParser SPI for backwards-compatibility.  
        if (this.parserClass != null) {  
            XmlBeanDefinitionParser parser =  
                    (XmlBeanDefinitionParser) BeanUtils.instantiateClass(this.parserClass);  
            return parser.registerBeanDefinitions(this, doc, resource);  
        }  
        // Read document based on new BeanDefinitionDocumentReader SPI.  
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();  
        int countBefore = getRegistry().getBeanDefinitionCount();  
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));  
        return getRegistry().getBeanDefinitionCount() - countBefore;  
    }
           

具體細節這裡不在累述。

總結

spring标簽的擴充性做得還是不錯的。在我們公司很多架構的一些通用配置都基于spring的聲明式标簽來實作。中間的一些約定和設計思想值得學習。 本文很多代碼細節沒辦法累述,有興趣可以去看看

感謝 :http://singleant.iteye.com/blog/1179948

自己看完源碼後不善于總結,看到如此好的文章果斷收藏,分享。再次感謝!

[/size]