现在的位置: 首页 > 综合 > 正文

(精)Spring IOC核心源码学习III:bean标签和自定义标签实现原理

2013年02月09日 ⁄ 综合 ⁄ 共 17041字 ⁄ 字号 评论关闭

本文将解析spring bean定义标签和自定义标签的解析实现原理。

这里说的标签仅限于以xml作为bean定义描述符的spring容器,继承AbstractXmlApplicationContext的一些子 容器,如XmlApplicationContext、ClassPathXmlApplicationContext、 FileSystemXmlApplicationContext等。同时也仅限于描述schema作为标签定义的情况。

 

 

Spring  xml ioc 容器常用标签和自定义标签

以 Xml 资源定义的容器配置是我们最常见的一种方式。

Spring 容器需要解析 xml 的标签,并把 xml 里 bean 的定义转化为内部的结构 BeanDifinition 。

Spring 的标签有很多种,其支持的常见的标签有:

 

标签 说明 例子
<bean> 最常用的,定义一个普通bean。
Java代码 
  1. <bean id="myBean"          class="com.test.MyBean" lazy-init="true"/>  

 

<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 通用配置支持。
Java代码 
  1. <aop:config proxy-target-class="true">    
  2.     <aop:advisor pointcut="..." advice-ref="txAdvice"/>    
  3.     <aop:advisor pointcut="..." advice-ref="fooAdvice"/>    
  4. </aop:config>  

 

<util> 提供在容器内配置一些JDK自带的工具类、集合类和常量的支持。
Java代码 
  1. <util:list id="list" list-class="java.util.ArrayList">  
  2.   <value>listValue1</value>  
  3.   <value>listValue2</value>  
  4. </util:list>  
  5.   
  6. <util:map id="map">  
  7.   <entry key="key1"  value="mapValue1"></entry>  
  8.   <entry key="key12" value="mapValue2"></entry>  
  9. </util:map>  

 

<p> 属性的简单访问
Java代码 
  1. <bean id="loginAction" class="com.test.LoginAction" p:name="test"></bean>  

 

<lang> <lang:groovy><lang:jruby>等,提供对动态脚本的支持。
Java代码 
  1. <lang:groovy id="test"  
  2.              refresh-check-delay="5000"  
  3.              script-source="classpath:com/test/groovy/test.groovy">  
  4. </lang:groovy>  

 

<jee > <jee:jndi-lookup/>等,对一些javaEE规范的bean配置的简化,如jndi等。
Java代码 
  1. <jee:jndi-lookup id="simple"    
  2.              jndi-name="jdbc/MyDataSource"    
  3.              cache="true"    
  4.              resource-ref="true"    
  5.              lookup-on-startup="false"    
  6.              expected-type="com.myapp.DefaultFoo"    
  7.              proxy-interface="com.myapp.Foo"/>    

 

基本上每一种标签都是用来定义一类 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 自己的很多标签也是基于这个设计上面来构造出来的。

 

 

 

Spring 对于自定义(声明式)bean标签解析如何设计

 

Bean 的定义方式有千千万万种,无论是何种标签,无论是何种资源定义,无论是何种容器,最终的 bean 定义内部表示都将转换为内部的唯一结构: BeanDefinition 。外部的各种定义说白了就是为了方便配置。

Spring 提供对其支持的标签解析的天然支持。所以只要按照 spring 的规范编写 xml 配置文件。所有的配置,在启动时都会正常的被解析成 BeanDefinition 。但是如果我们要实现一个自定义标签,则需要提供对自定义标签的全套支持。

我们知道要去完成一个自定义标签,需要完成的事情有:

1.       编写自定义标签 schema 定义文件,放在某个 classpath 下。

2.       在 classpath 的在 META-INF 下面增加 spring.schemas 配置文件,指定 schema 虚拟路径和实际 xsd 的映射。我们在 xml 里的都是虚拟路径,如:

Xml代码 
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"  
  4.     xmlns:p="http://www.springframework.org/schema/p"  
  5.     xsi:schemaLocation="  
  6.             http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
  7.             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd  
  8.             ">  
  9.   
  10.         <bean id="otherBean"          class="com.test.OtherBean" scope="prototype"/>  
  11.     <bean id="myBean"          class="com.test.MyBean" lazy-init="true"/>  
  12.     <bean id="singletonBean"          class="com.test.SingletonBean"/>  
  13. </beans>  

 

头部的

Java代码  收藏代码
  1. http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  

 就是一个虚拟路径,其对应的真实路径在spring jar包里的META-INF/spring.schemas里面有映射到classpath定义:

Xml代码  收藏代码
  1. 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 对标签解析的设计的过程如下:

解释:

 

Step 1: 将 xml 文件解析成 Dom 树。将 xml 文件解析成 dom 树的时候,需要 xml 标签定义 schema 来验证文件的语法结构。 Spring 约定将所有的 shema 的虚拟路径和真是文件路径映射定义在 classpath 的在 META-INF/spring.schemas 下面。在容器启动时 Spring 会扫描所有的 META-INF/spring.schemas 并将映射维护到一个 map 里。

      如 spring jar 包里会有自带的标签的 schemas 映射,可以看一下部分配置:

 

Xml代码 
  1. http\://www.springframework.org/schema/aop/spring-aop-2.0.xsd  =  org/springframework/aop/config/spring-aop-2.0.xsd  
  2. http\://www.springframework.org/schema/aop/spring-aop-2.5.xsd  =  org/springframework/aop/config/spring-aop-2.5.xsd  
  3. http\://www.springframework.org/schema/aop/spring-aop.xsd  =  org/springframework/aop/config/spring-aop-2.5.xsd  
  4. http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd  =  org/springframework/beans/factory/xml/spring-beans-2.0.xsd  
  5. http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd  =  org/springframework/beans/factory/xml/spring-beans-2.5.xsd  
  6. http\://www.springframework.org/schema/beans/spring-beans.xsd  =  org/springframework/beans/factory/xml/spring-beans-2.5.xsd  
  7. http\://www.springframework.org/schema/context/spring-context-2.5.xsd  =  org/springframework/context/config/spring-context-2.5.xsd  
  8. http\://www.springframework.org/schema/context/spring-context.xsd  =  org/springframework/context/config/spring-context-2.5.xsd  
  9. http\://www.springframework.org/schema/jee/spring-jee-2.0.xsd  =  org/springframework/ejb/config/spring-jee-2.0.xsd  
  10. http\://www.springframework.org/schema/jee/spring-jee-2.5.xsd  =  org/springframework/ejb/config/spring-jee-2.5.xsd  
  11. ......   

等号左边是虚拟路径,右边是真是路径(classpath下的)。
虚拟路径用在我们的bean定义配置文件里,如:

Xml代码  收藏代码
  1. <beans xmlns="http://www.springframework.org/schema/beans"  
  2.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"  
  3.     xmlns:p="http://www.springframework.org/schema/p"  
  4.     xsi:schemaLocation="  
  5.             http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
  6.             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd>  
  7. <bean>  
  8. </beans>  

 

beans里面的

Xml代码  收藏代码
  1. http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  

就是个虚拟路径。

Step 2: 将 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 。

Xml代码 
  1. http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler  
  2. http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler  
  3. http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler  
  4. http\://www.springframework.org/schema/jms=org.springframework.jms.config.JmsNamespaceHandler  
  5. http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler  
  6. http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler  
  7. http\://www.springframework.org/schema/tx=org.springframework.transaction.config.TxNamespaceHandler  
  8. http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler  

 

META-INF/spring.handlers

看看UtilNamespaceHandler的代码实现

 

Java代码  收藏代码
  1. public void init() {  
  2.     registerBeanDefinitionParser("constant"new ConstantBeanDefinitionParser());  
  3.     registerBeanDefinitionParser("property-path"new PropertyPathBeanDefinitionParser());  
  4.     registerBeanDefinitionParser("list"new ListBeanDefinitionParser());  
  5.     registerBeanDefinitionParser("set"new SetBeanDefinitionParser());  
  6.     registerBeanDefinitionParser("map"new MapBeanDefinitionParser());  
  7.     registerBeanDefinitionParser("properties"new PropertiesBeanDefinitionParser());  
  8. }  

 实现了标签和对应parser的映射注册。

ListBeanDefinitionParser的实现如下:

Java代码  收藏代码
  1. private static class ListBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {  
  2.   
  3.     protected Class getBeanClass(Element element) {  
  4.         return ListFactoryBean.class;  
  5.     }  
  6.   
  7.     protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {  
  8.         String listClass = element.getAttribute("list-class");  
  9.         List parsedList = parserContext.getDelegate().parseListElement(element, builder.getRawBeanDefinition());  
  10.         builder.addPropertyValue("sourceList", parsedList);  
  11.         if (StringUtils.hasText(listClass)) {  
  12.             builder.addPropertyValue("targetListClass", listClass);  
  13.         }  
  14.         String scope = element.getAttribute(SCOPE_ATTRIBUTE);  
  15.         if (StringUtils.hasLength(scope)) {  
  16.             builder.setScope(scope);  
  17.         }  
  18.     }  
  19. }  

 

这里父类代码不贴了,主要完成的是beanDifinition的生成。

源码实现

Spring 对于自定义(声明式)bean标签源码实现大概的源码结构如下:

 

 

 

 

XmlBeanDefinitionReader 是核心类,它接收 spring 容器传给它的资源 resource 文件,由它负责完成整个转换。它调用 DefaultDocumentLoader 来完成将 Resource 到 Dom 树的转换。调用DefaultBeanDefinitionDocumentReader 完成将 Dom 树到 BeanDefinition 的转换。

 

具体的代码流程细节完全可以基于这个结构去阅读,下面就贴几个核心源码段:

 

源码段 1 : 加载 spring.shemas,在PluggableSchemaResolver.java里实现:

 

Java代码 
  1. public class PluggableSchemaResolver implements EntityResolver {  
  2. /***定义schema location的映射文件路径***/  
  3.     public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";  
  4.     private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);  
  5.     private final ClassLoader classLoader;  
  6.     private final String schemaMappingsLocation;  
  7.     /** Stores the mapping of schema URL -> local schema path */  
  8.     private Properties schemaMappings;  
  9.     public PluggableSchemaResolver(ClassLoader classLoader) {  
  10.         this.classLoader = classLoader;  
  11.         this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;  
  12.     }  
  13.       
  14.     public PluggableSchemaResolver(ClassLoader classLoader, String schemaMappingsLocation) {  
  15.         Assert.hasText(schemaMappingsLocation, "'schemaMappingsLocation' must not be empty");  
  16.         this.classLoader = classLoader;  
  17.         this.schemaMappingsLocation = schemaMappingsLocation;  
  18.     }  
  19.   
  20. /**==========中间省略部分代码=========**/  
  21.   
  22. /***此处完成schema的加载***/  
  23.     protected String getSchemaMapping(String systemId) {  
  24.         if (this.schemaMappings == null) {  
  25.             if (logger.isDebugEnabled()) {  
  26.                 logger.debug("Loading schema mappings from [" + this.schemaMappingsLocation + "]");  
  27.             }  
  28.             try {  
  29.                 this.schemaMappings =  
  30.                         PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);  
  31.                 if (logger.isDebugEnabled()) {  
  32.                     logger.debug("Loaded schema mappings: " + this.schemaMappings);  
  33.                 }  
  34.             }  
  35.             catch (IOException ex) {  
  36.                 throw new FatalBeanException(  
  37.                         "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);  
  38.             }  
  39.         }  
  40.         return this.schemaMappings.getProperty(systemId);  
  41.     }  
  42.   
  43. }  

 

源码段 2 : 加载 spring.handlers,在 DefaultNamespaceHandlerResolver里实现:

Java代码 
  1. public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {  
  2.     /** 
  3.      * The location to look for the mapping files. Can be present in multiple JAR files. 
  4.      */  
  5.     public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";  
  6.   
  7.     /** Logger available to subclasses */  
  8.     protected final Log logger = LogFactory.getLog(getClass());  
  9.     /** ClassLoader to use for NamespaceHandler classes */  
  10.     private final ClassLoader classLoader;  
  11.     /** Resource location to search for */  
  12.     private final String handlerMappingsLocation;  
  13.     /** Stores the mappings from namespace URI to NamespaceHandler class name / instance */  
  14.     private Map handlerMappings;  
  15.   
  16.     public DefaultNamespaceHandlerResolver() {  
  17.         this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION);  
  18.     }  
  19.   
  20.     public DefaultNamespaceHandlerResolver(ClassLoader classLoader) {  
  21.         this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION);  
  22.     }  
  23.   
  24.     public DefaultNamespaceHandlerResolver(ClassLoader classLoader, String handlerMappingsLocation) {  
  25.         Assert.notNull(handlerMappingsLocation, "Handler mappings location must not be null");  
  26.         this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());  
  27.         this.handlerMappingsLocation = handlerMappingsLocation;  
  28.     }  
  29. /**==========中间省略部分代码=========**/  
  30.   
  31.         
  32.        /************************ 
  33.      * Load the specified NamespaceHandler mappings lazily. 
  34.         *  此处加载延迟加载spring.handlers,只有第一次自定义标签被解析到,才会被加载。 
  35.         ****************************/  
  36.     private Map getHandlerMappings() {  
  37.         if (this.handlerMappings == null) {  
  38.             try {  
  39.                 Properties mappings =  
  40.                         PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);  
  41.                 if (logger.isDebugEnabled()) {  
  42.                     logger.debug("Loaded mappings [" + mappings + "]");  
  43.                 }  
  44.                 this.handlerMappings = new HashMap(mappings);  
  45.             }  
  46.             catch (IOException ex) {  
  47.                 IllegalStateException ise = new IllegalStateException(  
  48.                         "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]");  
  49.                 ise.initCause(ex);  
  50.                 throw ise;  
  51.             }  
  52.         }  
  53.         return this.handlerMappings;  
  54.     }  
  55.   
  56. }  

 

源码段3 : xml 到 dom 树的解析。

在 XmlBeanDefinitionReader .java 的 doLoadBeanDefinitions 方法里,调用 DefaultDocumentLoader 完成。

 

Java代码 
  1. protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)  
  2.             throws BeanDefinitionStoreException {  
  3.         try {  
  4.             int validationMode = getValidationModeForResource(resource);  
  5.             Document doc = this.documentLoader.loadDocument(  
  6.                     inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());  
  7.             return registerBeanDefinitions(doc, resource);  
  8.         }  
  9.         catch (BeanDefinitionStoreException ex) {  
  10.             throw ex;  
  11.         }  
  12.         catch (SAXParseException ex) {  
  13.             throw new XmlBeanDefinitionStoreException(resource.getDescription(),  
  14.                     "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);  
  15.         }  
  16.         catch (SAXException ex) {  
  17.             throw new XmlBeanDefinitionStoreException(resource.getDescription(),  
  18.                     "XML document from " + resource + " is invalid", ex);  
  19.         }  
  20.         catch (ParserConfigurationException ex) {  
  21.             throw new BeanDefinitionStoreException(resource.getDescription(),  
  22.                     "Parser configuration exception parsing XML from " + resource, ex);  
  23.         }  
  24.         catch (IOException ex) {  
  25.             throw new BeanDefinitionStoreException(resource.getDescription(),  
  26.                     "IOException parsing XML document from " + resource, ex);  
  27.         }  
  28.         catch (Throwable ex) {  
  29.             throw new BeanDefinitionStoreException(resource.getDescription(),  
  30.                     "Unexpected exception parsing XML document from " + resource, ex);  
  31.         }  
  32.     }  

 其中的

Java代码 
  1. getEntityResolver()  

  会完成spring.schemas的装载,里面会间接调用源码段1。穿进去的entityResolver作为标签解析使用。

 

源码段4 : dom 树到 Beandifinition:

 

在 XmlBeanDefinitionReader .java 的 doLoadBeanDefinitions 方法里,调用 BeanDefinitionDocumentReader完成。

 

 

Java代码 
  1. public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {  
  2.         // Support old XmlBeanDefinitionParser SPI for backwards-compatibility.  
  3.         if (this.parserClass != null) {  
  4.             XmlBeanDefinitionParser parser =  
  5.                     (XmlBeanDefinitionParser) BeanUtils.instantiateClass(this.parserClass);  
  6.             return parser.registerBeanDefinitions(this, doc, resource);  
  7.         }  
  8.         // Read document based on new BeanDefinitionDocumentReader SPI.  
  9.         BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();  
  10.         int countBefore = getRegistry().getBeanDefinitionCount();  
  11.         documentReader.registerBeanDefinitions(doc, createReaderContext(resource));  
  12.         return getRegistry().getBeanDefinitionCount() - countBefore;  
  13.     }  

 

具体细节这里不在累述。

 

总结

spring标签的扩展性做得还是不错的。在我们公司很多框架的一些通用配置都基于spring的声明式标签来实现。中间的一些约定和设计思想值得学习。 本文很多代码细节没办法累述,有兴趣可以去看看。文章中若有不对的地方请大家指出,一起探讨。

后续的spring源码学习可能会是代理的具体实现,欢迎一起探讨!

抱歉!评论已关闭.