Spring5源码分析(019)——IoC篇之解析alias标签、import标签和beans标签

注:《Spring5源码分析》汇总可参考:Spring5源码分析(002)——博客汇总


  还是之前提到过,配置文件中的默认标签的解析包括 import 标签、alias 标签、bean 标签、beans 标签的处理,前面优先花了较多的篇幅分析了 bean 标签的解析,这是最复杂但也是最重要最核心的功能,其他几个标签的解析也都是围绕这来的,接下来将对其他几个标签的解析进行分析:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {    // import 标签
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {    // alias 标签
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {    // bean 标签
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {        // beans 标签,需要递归解析
        // recurse
        doRegisterBeanDefinitions(ele);
    }
}

   本文的目录结构如下:


1、别名alias

1.1、别名alias的使用

  在介绍别名 alias 前,我们先来看下 Spring 中怎么使用别名 alias (这里的例子都是使用 XML 的配置方式,注解的使用方式可以参考 spring 官方文档[2])。就目前来看,alias的使用主要有两种方式:

  • <1> 通过 bean 标签中的 name 属性来配置(这些配置会被当成 alias 进行注册),需要注意的是,没有配置 id 属性的话,则使用 name 属性的第一个名称作为 id ,其他的则是 alias,例子如下:
// 只有一个 alias
<bean id='fromName' name='toName' class='cn.wpbxin.AliasExample' />
// , 分隔
<bean id='fromName' name='toNameA,toNameB' class='cn.wpbxin.AliasExample' />
// ; 分隔
<bean id='fromName' name='toNameA;toNameB' class='cn.wpbxin.AliasExample' />
// 空格分隔
<bean id='fromName' name='toNameA toNameB' class='cn.wpbxin.AliasExample' />
// 没有配置 id 属性,则使用 name 属性的第一个名称作为 id ,其他的则是 alias
<bean name='fromName,toName' class='cn.wpbxin.AliasExample' />
  • <2> 通过 alias 标签来指定:
<bean id='fromName' class='cn.wpbxin.AliasExample' />
<alias name="fromName" alias="toName"/>
// , 分隔
<alias name="fromName" alias="toNameA,toNameB"/>
// ; 分隔
<alias name="fromName" alias="toNameA;toNameB"/>
// 空格分隔
<alias name="fromName" alias="toNameA toNameB"/>

  

1.2、什么是别名 alias

  spring 官方文档[1]中提到,每个bean都可以有一个或者多个标识符,这些标识符 identifiers 在 IoC 容器中都是唯一的。一个 bean 通常只有一个标识符(即所谓的 beanName ,一般通过 bean 标签中的 id 属性来指定)。然而如果需要配置多个标识符的话,则其他的这些标识符就被称为 别名 alias 。他们都等价指向同一个 bean 。

 

1.3、id/beanName 的确定顺序

  前面分析 bean 标签时提到过,id/beanName 的确定顺序如下,而其他的标识符则作为别名 alias

  • <1> 如果 bean 标签中有配置 id 属性,则使用此属性作为 beanName ;
  • <2> 如果 id 属性没有配置,而 name 属性有指定,则使用 name 属性指定的第一个标识符(使用英文逗号,英文分号;或者空格作为多个名称的分隔符)来作为 beanName ,而其他的则作为别名 alias 进行注册。
  • <3> 如果都没有,则 Spring 会根据自定义的规则提供一个默认的 beanName 作为 id 。

 

1.4、别名alias的配置来源

  如 1.1 中提到的,别名alias的配置来源(XML的方式)有2个:bean 标签的name配置和alias标签。

 

1.5、为什么需要 alias

  通过前面的分析,相信大家对alias已经不陌生了。不过这里有个疑问,既然已经有了 id/beanName,为什么还需要 alias ?

  还是来看下 Spring 官方文档给出来的一些说明,Spring 官方文档[2]中提到:

it is sometimes desirable to give a single bean multiple names, otherwise known as bean aliasing. [2]

   可以看出是因为有需要,但是这里并没有详细说明,相关的说明其实在前面 Spring 官方文档[1] 中有提到(这里就不贴出具体的文档内容了,感兴趣的可以直接参考链接中的官方英文文档,这里笔者根据理解大致翻译总结下):

  bean 可以有多个标识符定义(多余的称为 alias),但是在bean定义时(bean 标签中的bean定义)就指明所有的别名并不可行,然而有时候又需要为其他地方已定义的bean来引入一个alias,尤其是在大型系统中,配置信息分散在各个子系统中,而每个子系统都有各自的定义,而你又可能无法对这些进行修改(笔者注:其他模块中已定义好的,一般不会轻易被改动),于是就有 alias 标签,来做这些配置指定,配置如下:

<alias name="fromName" alias="toName"/>

  在这个配置中, fromName 和 toName 其实都是指向同一个 bean ,它们是等价的。

  文档中提到这样一个例子:子系统A需要通过 subsystemA-dataSource 来引用 DataSource ,而子系统B中则需要通过 subsystemB-dataSource 来引用同一个 DataSource,这个 DataSource 是在主应用中已经定义好的 myApp-dataSource ,主应用如果需要组合子系统A和子系统B,在不能修改A、B配置的前提下想要应用正常运行,则可以通过alias来分别指定,很好地处理这种场景:

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>

 笔者注:多团队提供引用的jar包,碰上需要复用各自已经定义好的bean时可能会出现,这也算是一种定制化,根据应用本身的命名需要。

 

2、alias标签的解析

  接下来回到alias标签的解析上,这里调用的是 org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.processAliasRegistration(Element ele) 来进行处理:

/**
 * Process the given alias element, registering the alias with the registry.
 * <p>处理给定的alias元素,注册alias
 */
protected void processAliasRegistration(Element ele) {
    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));
    }
}

   可以发现,alias 标签中别名的注册,和 bean 标签中别名的注册基本是一样的,都是将别名与 beanName 一同注册到 Registry 中,这里不再赘述。另外需要说明的是,别名注册后通知监听器做相应处理 这一步,Spring当前并没有具体的实现处理。

 

3、import标签的解析

3.1、关于import标签及其使用

  对于大型项目,需要管理的 bean 可能会有很多,如果项目中使用的是 Spring 配置文件的方式,而且是单个配置文件的话,可能会出现超级大的配置文件,对于配置的维护可能存在一定的不便。这时候可能就需要根据系统的功能来进行模块划分了,拆分成多个配置文件,一方面独立的配置文件会小很多,另一方面单独维护模块相关的配置也会更加方便,而多个配置文件则可以使用 import 标签来进行导入。就如同Spring文档[2]中提到的:It can be useful to have bean definitions span multiple XML files. Often, each individual XML configuration file represents a logical layer or module in your architecture.

  例如,在常用的 applicationContext.xml 配置文件中,我们只做配置文件的管理,而具体的bean配置则由引入的各个配置文件单独维护,后期如果有新的模块需要引入,只需要简单修改 applicationContext.xml ,这样就可以简化配置后期维护的复杂度,并使得配置模块化,易于管理(案例参考[3]和[4],这里使用的都是相对路径,前导 / 加不加都一样,官方文档建议不使用 / ):

// ...其他省略...
<beans>
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>

   接下来看下 Spring 中如何解析 import 标签。

 

3.2、importBeanDefinitionResource

  import 标签的解析是通过 org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(Element ele) 方法来进行处理的,代码如下:

/**
 * Parse an "import" element and load the bean definitions
 * from the given resource into the bean factory.
 * <p>解析 import 标签并从指定的 resource 加载 beanDefinition 到 beanFactory 中
 */
protected void importBeanDefinitionResource(Element ele) {
    // 1、获取 resource 属性,即配置资源文件路径
    String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
    if (!StringUtils.hasText(location)) {
        getReaderContext().error("Resource location must not be empty", ele);
        return;
    }

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

    // 实际的资源集合,即 import 标签导入的所有资源
    Set<Resource> actualResources = new LinkedHashSet<>(4);

    // Discover whether the location is an absolute or relative URI
    // 3、判断 location 是绝对路径还是相对路径
    boolean absoluteLocation = false;
    try {
        // classpath 、classpath* 、 标准的 URL / URI
        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?
    // 4、绝对路径
    if (absoluteLocation) {
        try {
            // 加载相应路径的 beanDefinition ,并添加对应配置文件的 Resource 到 actualResources 中
            // 这里其实又是回到 ResourcePatternResolver 去解析资源路径,然后再加载对应资源的 beanDefinition
            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);
        }
    }
    // 5、相对路径,即当前文件/目录的相对路径
    else {
        // No URL -> considering resource location as relative to the current file.
        try {
            int importCount;
            // 创建相对路径的 Resource
            Resource relativeResource = getReaderContext().getResource().createRelative(location);
            if (relativeResource.exists()) {
                // 如果存在,则加载对应 relativeResource 的 beanDefinition,并将 relativeResource 添加到 actualResources 中
                importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
                actualResources.add(relativeResource);
            }
            else {
                // 如果不存在,则获取根路径地址
                String baseLocation = getReaderContext().getResource().getURL().toString();
                // 拼接路径,加载对应资源路径的 beanDefinition 并将对应的 Resource 添加到 actualResources 中
                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);
        }
    }
    // 6、解析后激活 import已解析 的事件,即通知监听器
    Resource[] actResArray = actualResources.toArray(new Resource[0]);
    getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}

import 标签的解析过程也比较清晰直接,就是确认路径然后解析,整个解析过程如下:

  • 1、获取 import 标签的 resource 属性值,即配置资源的路径。
  • 2、解析路径中的系统属性,例如常见的 ${user.dir} 、 ${user.home},Environment 中的配置都可以在这里使用。
  • 3、判断资源路径 location 是绝对路径还是相对路径,具体的路径判断逻辑后文会详细分析,参考【3.3、判断资源路径】:
    • 4、如果是绝对路径,则递归调用 bean 的解析过程,进行另一次的解析
    • 5、如果是相对路径,则先计算出绝对路径得到 resource 并进行解析
  • 6、解析后激活 import已解析 的事件,即通知监听器,解析完成。

 

3.3、判断资源路径

  判断资源路径 location 是绝对路径还是相对路径是通过以下代码来判断的:

// classpath 、classpath* 、 标准的 URL / URI
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();

判断规则如下:

  • 1、以 classpath 、classpath* 开头的伪 URL,或者是标准的 URL (可以构造成 java.net.URL ),则是绝对路径;
  • 2、根据 location 构造 java.net.URI ,然后调用其 isAbsolute() 判断是否为绝对路径

 

3.4、绝对路径的处理

  如果需要 import 的资源的 location 是绝对路径,则通过调用 org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) 方法来进行处理,这又是似曾相识的接口,具体代码如下:

/**
 * Load bean definitions from the specified resource location.
 * <p>The location can also be a location pattern, provided that the
 * ResourceLoader of this bean definition reader is a ResourcePatternResolver.
 * @param location the resource location, to be loaded with the ResourceLoader
 * (or ResourcePatternResolver) of this bean definition reader
 * @param actualResources a Set to be filled with the actual Resource objects
 * that have been resolved during the loading process. May be {@code null}
 * to indicate that the caller is not interested in those Resource objects.
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 * @see #getResourceLoader()
 * @see #loadBeanDefinitions(org.springframework.core.io.Resource)
 * @see #loadBeanDefinitions(org.springframework.core.io.Resource[])
 */
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
    // 获取 ResourceLoader 对象
    ResourceLoader resourceLoader = getResourceLoader();
    if (resourceLoader == null) {
        throw new BeanDefinitionStoreException(
                "Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
    }

    if (resourceLoader instanceof ResourcePatternResolver) {
        // Resource pattern matching available.
        try {
            // 资源的定位:获取 Resource 数组。模式匹配下可能会有多个资源文件,例如 ANT 风格的路径表达式
            Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
            // 解析加载 BeanDefinition
            int count = loadBeanDefinitions(resources);
            // 添加到 actualResources 中
            if (actualResources != null) {
                Collections.addAll(actualResources, resources);
            }
            if (logger.isTraceEnabled()) {
                logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
            }
            return count;
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "Could not resolve bean definition resource pattern [" + location + "]", ex);
        }
    }
    else {
        // Can only load single resources by absolute URL.
        // 获取 Resource 对象,加载当个资源文件的 BeanDefinition
        Resource resource = resourceLoader.getResource(location);
        int count = loadBeanDefinitions(resource);
        // 添加到 actualResources 中
        if (actualResources != null) {
            actualResources.add(resource);
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
        }
        return count;
    }
}

@Override
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
    Assert.notNull(resources, "Resource array must not be null");
    int count = 0;
    for (Resource resource : resources) {
        count += loadBeanDefinitions(resource);
    }
    return count;
}

  具体的解析逻辑如下:

  • 1、首先,获取当前的资源加载器 ResourceLoader
  • 2、然后根据资源加载器的类型来进行不同的逻辑解析,比如 Pattern 模式匹配下,一个路径可能存在多个Resource资源
  • 3、最终调用的其实都是我们一直在分析的 org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(Resource resource) ,这里就是递归调用解析了,一环套一环,直到解析完成。
  • 4、这其中解析的 Resource 同样都需要添加到 actualResources 中。

 

3.5、相对路径的处理

  如果 location 是相对路径,则先构造出对应路径的 resource 然后再进行解析

  • 先按照当前路径的相对路径的来创建 Resource ,如果存在,则调用 XmlBeanDefinitionReader.loadBeanDefinitions(Resource resource) 进行解析
    • 前面 3.1 例子中出现的都是相对路径的资源:<import resource="resources/messageSource.xml"/>
  • 否则,构造一个绝对路径 location( 即 StringUtils.applyRelativePath(baseLocation, location) ),并调用 loadBeanDefinitions(String location, Set<Resource> actualResources) 方法进行解析,这与绝对路径的解析过程一样。
    • 类似这种:<import resource="resources/*-messageSource.xml"/>

 

3.6、总结

  import 标签的解析过程分析到此就结束了。其关键点在于找到对应的资源 Resource (可能是多个),然后递归地解析。

 

4、beans标签的解析

  对于 beans 标签,其实和外层的 beans 是类似的,这里是直接递归地解析。可以看成是把另一个配置文件的内容给搬过来而已,和单独的配置文件解析没有太大差别。 beans 标签比较少用,还是按照文件划分的方式在模块化方面会比较好理解一些。

 

5、参考

posted @ 2020-12-28 23:17  心明谭  阅读(190)  评论(0编辑  收藏  举报