Spring Source Code Analysis (四) The parsing of alias, import and beans label


默认标签的解析

之前提到过读配置文件的解析包括对 import 标签、alias 标签、bean 标签、beans 标签的处理,上一篇博客主要围绕着 bean 标签做了详细的分析,下面再看看 alias 标签。

1、alias 标签的解析

在对 bean 进行定义时,除了使用 id 属性来指定名称之外,为了提供多个名称,可以使用 alias 标签来指定。而所有的这些名称都指向同一个 bean,在某些情况下提供别名非常有用,比如为了让应用的每一个组件能更容易地对公共组件进行引用。

然而,在定义 bean 时就指定所有的别名并不总是恰当的。有时我们期望能在当前位置为那些在别处定义的 bean引入别名。在 XML 配置文件中,可用单独的 <alias/> 元素来完成 bean 别名的定义,如配置文件中定义了一个 JavaBean:

<bean id="testBean" class="com.test" />
<!-- 可以使用 name 属性定义别名 -->
<bean id="testBean" name="testBean1,testBean2" class="com.test" />

<!-- Spring 还有另外一种声明别名的方式 -->
<bean id="testBean" class="com.test" />
<alias name="testBean" alias="testBean1,testBean2" />

举个例子,组件 A 在 XML 配置文件中定义了一个名为 componentADataSource 类型的 bean,但组件 B 却想在其 XML 文件中以 componentB 命名引入此 bean。而且在主程序 MyApp 的 XML 配置文件中,希望以 myApp 的名字来引用此 bean,最后容器加载 3 个 XML 文件来完成最终的 ApplicationContext。在此情形下,可通过在配置文件中添加下列 alias 元素实现:

<alias name="componentA" alias="componentB" />
<alias name="componentA" alias="myApp" />

下面深入分析 alias 标签的解析过程:

// org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // recurse
        doRegisterBeanDefinitions(ele);
    }
}

解析方法如下:

// org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader

protected void processAliasRegistration(Element ele) {
    // 获取 beanName
    String name = ele.getAttribute(NAME_ATTRIBUTE);
    // 获取别名
    String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
    boolean valid = true;
    if (!StringUtils.hasText(name)) {
        getReaderContext().error("Name must not be empty", ele);
        valid = false;
    }
    if (!StringUtils.hasText(alias)) {
        getReaderContext().error("Alias must not be empty", ele);
        valid = false;
    }
    if (valid) {
        try {
            // 注册 alias 
            getReaderContext().getRegistry().registerAlias(name, alias);
        }
        catch (Exception ex) {
            getReaderContext().error("Failed to register alias '" + alias +
                                     "' for bean with name '" + name + "'", ele, ex);
        }
        // 别名注册后通知监听器做相应的处理
        getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
    }
}

可以看到和之前注册 BeanDefinitinon 很相似,这里也是通过注册工厂 AliasRegistry 的实例 XmlBeanFactory 进行注册(方法的实现在 SimpleAliasRegistry 中),具体不再赘述。

2、import 标签的解析

Spring 项目早期最复杂的一点就在配置文件的撰写,此时分模块是一个很好的办法,此时可以使用 import 标签,例如我们可以构造这样的 Spring 配置文件 applicationContext.xml

<?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">

    <import resource="customerContext.xml" />
    <import resource="systemContext.xml" />
    
</beans>

applicationContext.xml 文件中使用 import 的方式导入各个模块配置文件,以后若有新模块的加入,那么就可以简单修改这个文件了。这样可以大大简化配置后期维护的复杂度,并使配置模块化、易于管理。

下面看看 Spring 如何解析 import 配置文件:

// org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader

protected void importBeanDefinitionResource(Element ele) {
    // 获取 resource 属性
    String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
    // 如果不存在 resource 属性则不做任何处理
    if (!StringUtils.hasText(location)) {
        getReaderContext().error("Resource location must not be empty", ele);
        return;
    }

    // 解析系统属性,格式如 "${user.dir}"
    // Resolve system properties: e.g. "${user.dir}"
    location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);

    Set<Resource> actualResources = new LinkedHashSet<>(4);

    // 判断 location 是绝对 URI 还是相对 URI
    // Discover whether the location is an absolute or relative URI
    boolean absoluteLocation = false;
    try {
        absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
    }
    catch (URISyntaxException ex) {
        // cannot convert to an URI, considering the location relative
        // unless it is the well-known Spring prefix "classpath*:"
    }

    // Absolute or relative?
    // 如果是绝对 URI 则直接根据地址加载对应的配置文件
    if (absoluteLocation) {
        try {
            int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
            if (logger.isTraceEnabled()) {
                logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]");
            }
        }
        catch (BeanDefinitionStoreException ex) {
            getReaderContext().error(
                "Failed to import bean definitions from URL location [" + location + "]", ele, ex);
        }
    }
    else {
        // 如果是相对地址则根据相对地址计算出绝对地址
        // No URL -> considering resource location as relative to the current file.
        try {
            int importCount;
            // Resource 存在多个实现子类,如 VfsResource、FileSystemResource 等等
            // 而每个 resource 的 createRelative 方法实现方式都不一样,所以这里先使用子类的方法尝试解析
            Resource relativeResource = getReaderContext().getResource().createRelative(location);
            if (relativeResource.exists()) {
                importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
                actualResources.add(relativeResource);
            }
            else {
                // 如果解析不成功,则使用默认的解析器 ResourcePatternResolver 来解析
                String baseLocation = getReaderContext().getResource().getURL().toString();
                importCount = getReaderContext().getReader().loadBeanDefinitions(
                    StringUtils.applyRelativePath(baseLocation, location), actualResources);
            }
            if (logger.isTraceEnabled()) {
                logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]");
            }
        }
        catch (IOException ex) {
            getReaderContext().error("Failed to resolve current resource location", ele, ex);
        }
        catch (BeanDefinitionStoreException ex) {
            getReaderContext().error(
                "Failed to import bean definitions from relative location [" + location + "]", ele, ex);
        }
    }
    // 解析后进行监听器激活处理
    Resource[] actResArray = actualResources.toArray(new Resource[0]);
    getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}

流程如下:

(1)获取 resource 属性所代表的路径;

(2)解析路径中的系统属性,格式如 "${user.dir}"

(3)判定 location 是绝对路径还是相对路径;

(4)如果是绝对路径则递归调用 bean 的解析过程,进行另一次解析;

(5)如果是相对路径则计算出绝对路径并进行解析;

(6)通知监听器,解析完成。(注意此版本为空实现)

3、嵌入式 beans 标签的解析

对于嵌入式的 beans 标签,它非常类似于 import 标签所提供的功能:

<?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">

    <bean id="aa" class="test.aa" />
    
    <beans>
    	...
    </beans>
   
</beans>

嵌入式 beans 标签的解析与单独的配置文件差不多,无非就是递归调用 beans 的解析过程。


Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC