zoukankan      html  css  js  c++  java
  • 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、参考

  • 相关阅读:
    《C++ Primer》学习笔记第2章 变量和基本类型
    Java学习笔记类的继承与多态特性
    Java的冒泡排序问题
    新起点,分享,进步
    MVC2中Area的路由注册实现
    了解一下new关键字实现阻断继承的原理
    利用Bing API开发的搜索工具(MVC+WCF)
    ASP.NET MVC中错误处理方式
    const和readonly内部区别
    WCF中校验参数的实现方式(一)
  • 原文地址:https://www.cnblogs.com/wpbxin/p/14204132.html
Copyright © 2011-2022 走看看