【死磕 Spring】—— IoC 之解析Bean:解析 import 标签

前言

在前面的博客中分析到,Spring 中有两种解析Bean 的方式:

  • 如果根节点或者子节点采用默认命名空间的话,则调用 parseDefaultElement(...) 方法,进行默认的标签解析
  • 否则,调用 BeanDefinitionParseDelefate#parseCustomElement(...) 方法,进行自定义解析。

所以,以下博客就这两个方法进行详细分析说明。而本文,先从默认标签解析过程开始。代码如下:

// DefaultBeanDefinitionDocumentReader.java

public static final String IMPORT_ELEMENT = "import";
public static final String ALIAS_ATTRIBUTE = "alias";
public static final String BEAN_ELEMENT = BeanDefinitionParserDelegate.BEAN_ELEMENT;
public static final String NESTED_BEANS_ELEMENT = "beans";

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);
	}
}

​ 该方法的功能一目了然,分别是对四中不同的标签进行解析,分别是 importaliasbeanbeans。咱们从第一个标签import 开始,

1. import 示例

​ 经历过spring 配置文件的小伙伴都知道,如果工程比较大,配置文件的维护会让人觉得恐怖,文件太多了,想象静所有的配置都放在spring.xml 配置文件中,那种后怕是不是很明显?

​ 所有针对这种情况 Spring 提供了一个分模块的思路,利用 import 标签,例如我们可以构造一个这样的 spring.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="spring-student.xml"/>

    <import resource="spring-student-dtd.xml"/>

</beans>

​ spring.xml 配置文件中,使用 import 标签的方式导入其他模块的配置文件。

  • 如果有配置需要修改,直接修改相应配置文件即可
  • 如果有新的模块需要引入直接增加import 即可,

这样大大简化了配置后期维护的复杂度,同时也易于管理。

2 importBeanDefinitionResource

​ spring 使用#importBeanDefinitionResource(Element ele) 方法,完成对 import 标签的解析。

// DefaultBeanDefinitionDocumentReader.java

/**
 * Parse an "import" element and load the bean definitions
 * from the given resource into the bean factory.
 */
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); // 使用 problemReporter 报错
        return;
    }

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

    // 实际 Resource 集合,即 import 的地址,有哪些 Resource 资源
    Set<Resource> actualResources = new LinkedHashSet<>(4);

    // <3> 判断 location 是相对路径还是绝对路径
    // 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?
    // <4> 绝对路径
    if (absoluteLocation) {
        try {
            // 添加配置文件地址的 Resource 到 actualResources 中,并加载相应的 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 们
                importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
                // 添加到 actualResources 中
                actualResources.add(relativeResource);
            // 不存在
            } else {
                // 获得根路径地址
                String baseLocation = getReaderContext().getResource().getURL().toString();
                // 添加配置文件地址的 Resource 到 actualResources 中,并加载相应的 BeanDefinition 们
                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> 解析成功后,进行监听器激活处理
    Resource[] actResArray = actualResources.toArray(new Resource[0]);
    getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}

解析 import 标签的过程较为清晰,整个过程如下:

  • <1>处,获取source 属性的值,改值表示资源的路径。
  • <2>处,解析路径中的系统属性,如:“${user.dir}”
  • <3>处,判断资源路径location 是绝对路径还是相对路径,详细解析见【2.1】
  • <4>处,如果是绝对路径,则递归调用 Bean 的解析过程,进行另一次解析,详细解析见【2.2】
  • <5>处,如果是相对路径,则先计算出绝对路径得到 Resource, 然后进行解析。详细解析见【2.3】
  • <6>处,通知监听器,完成解析。

2.1 判断路径

​ 通过如下代码,来判断 location 是为相对路径,还是绝对路径:

absoluteLocation = ResourcePatternUtils.isUrl(location) // <1>
    || ResourceUtils.toURI(location).isAbsolute(); // <2>

判断绝对路径的规则如下:

  • <1> 以 classpath*: 或者 classpath: 开头的为绝对路径。
  • 能够通过该 location 构建处 java.net.URI 为绝对路径。
  • <2> 根据location 构造 java.net.URI 判断调用 #isAbsolute() 方法,判断是否为绝对路径。

2.2 处理绝对路径

如果,location 为绝对路径,则调用 loadBeanDefinitions(String location, Set<Resource> actualResources) 方法。该方法在,org.springframework.beans.factory.support.AbstractBeanDefinitionReader 中定义,代码如下:

/**
 * 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 数组,因为 Pattern 模式匹配下,可能有多个 Resource 。例如说,Ant 风格的 location
            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 对象,
        Resource resource = resourceLoader.getResource(location);
        // 加载 BeanDefinition 们
        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;
    }
}

整个逻辑比较简单:

  • 首先,获取ResourceLoader 对象
  • 然后,根据不同的 ResourceLoader 执行不同的逻辑,主要是可能存在多个Resource
  • 最终,都会回归到 XmlBeanDefinitionReader#loadBeanDefinitions(Resource... resources) 方法,所以这是一个递归的过程。
  • 另外,获得到的Resource 的对象获数组,都会添加到 actualResources

2.3 处理相对路径,

如果location 是相对路径,则会根据相应的Resource 计算出相应的相对路径的Resource 对象,然后:

  • 若该Resource 存在,则调用 XmlBeanDefinitionReader#loadBeanDefinitions() 方法,进行 BeanDefinition 加载,
  • 否则,构造一个绝对 location 即StringUtils.applyRelativePath(baseLocation, location) 处的代码),并调用 #loadBeanDefinitions(String location, Set<Resource> actualResources) 方法,与绝对路径过程一样

小结

至此, import 标签的解析完毕了,真个过程也比较清晰明了了: 获取 source 属性值,得到正确的资源路径,然后调用 XmlBeanDefinitionReader#loadBeanDefinitions(Resource... resources) 方法,进行递归的 BeanDeanDefinition 加载

posted @ 2022-04-19 14:58  雷姆饲养员  阅读(108)  评论(0)    收藏  举报