[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]