Spring 对占位符的处理(一):XML 中的 Bean

Spring 对占位符的处理(一):XML 中的 Bean

最近有小伙伴在开发时,遇到了一个 Spring 占位符,例如 ${token}, 在不同环境下处理不一致的问题,正好对 Spring 对占位符的处理也有一些不清楚的地方,趁此机会,把 Spring 对占位符的处理机制深入了解一下,方便后续排查问题。

经常阅读D瓜哥博客的朋友可能知道,D瓜哥在 Spring 扩展点实践:整合 Apache Dubbo(一): Spring 插件机制简介 中已经介绍了 Spring 的插件机制。在阅读以下内容之前,建议大家先去阅读一下这篇文章中“Spring 插件机制简介”章节的内容,以便于无缝衔接。

在分析的过程中发现, Spring 对占位符有两种截然不同的出来阶段:① XML 配置文件中的占位符;② Java 源代码中 @Value 注解中的占位符。由于内容较多,一篇讲解完有些过长,所以分三篇文章来分别介绍这两种处理过程。

本篇首先来介绍一下对 XML 配置文件中的占位符的处理。

示例代码

在正式开始之前,先来看一下示例代码:

UserRpc.java
/**
 * @author D瓜哥 · https://www.diguage.com
 * @since 2023-05-02 10:23:49
 */
public class UserRpc {

  @Value("${user.appId}")
  private String appId;

  // 这里不使用注解,而是使用 XML 配置
  // @Value("${user.token}")
  private String token;
}
token.properties
user.appId=dummyAppId
user.token=dummyToken
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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
          https://www.springframework.org/schema/context/spring-context.xsd">

  <!-- @author D瓜哥 · https://www.diguage.com -->

  <context:annotation-config/>

  <bean id="userRpc"
        class="com.diguage.truman.context.UserRpc">
    <!-- XML 配置的占位符实例在此 -->
    <property name="token" value="${user.token}"/>
  </bean>

  <context:property-placeholder location="classpath:token.properties"/>

</beans>

<bean> 标签处理

Spring 启动流程概述 中,已经介绍过,Spring 的启动过程几乎都被封装在 AbstractApplicationContext#refresh 方法中。在 refresh 方法中调用了 refreshBeanFactory 方法;在 refreshBeanFactory 方法执行过程中,调用了 loadBeanDefinitions 方法。而 BeanDefinition 的加载是由 org.springframework.context.support.AbstractRefreshableApplicationContext#loadBeanDefinitions 来完成的。通过 XML 文件配置的 Bean 是由 org.springframework.context.support.AbstractXmlApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.support.DefaultListableBeanFactory)AbstractRefreshableApplicationContext 的子类)处理完成的。处理过程的时序图如下:

AbstractApplicationContext.obtainFreshBeanFactory — XML 配置文件解析
Figure 1. AbstractApplicationContext.obtainFreshBeanFactory — XML 配置文件解析

我们来看一下 XmlBeanDefinitionReader#loadBeanDefinitions 的代码:

XmlBeanDefinitionReader.java
/**
 * Load bean definitions from the specified XML file.
 * @param encodedResource the resource descriptor for the XML file,
 * allowing to specify an encoding to use for parsing the file
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 */
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (logger.isTraceEnabled()) {
        logger.trace("Loading XML bean definitions from " + encodedResource);
    }

    // 通过属性来记录已经加载过的资源
    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();

    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
                "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }

    // 从 EncodedResource 中获取已经封装的 Resource 对象并再次从 Resource 中获取其中的 InputStream
    // 将资源文件转为 InputStream 的 IO 流
    try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
        // 从 InputStream 中得到 XML 的解析源
        InputSource inputSource = new InputSource(inputStream);
        if (encodedResource.getEncoding() != null) {
            inputSource.setEncoding(encodedResource.getEncoding());
        }
        // 这里是具体的读取过程
        // 真正开始读取的方法(读取 BeanDefinitions 的核心) TODO dgg XML 配置文件解析重点
        return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException(
                "IOException parsing XML document from " + encodedResource.getResource(), ex);
    }
    finally {
        currentResources.remove(encodedResource);
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}

XmlBeanDefinitionReader#loadBeanDefinitions 中,将 XML 文件读取出来,转化成 InputSource 对象,然后通过 doLoadBeanDefinitions 开始解析。

从上面的时序图可以看成,XML 各个标签的解析是在 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法中完成的。跳过“中间商”,直接来看看 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法的实现:

DefaultBeanDefinitionDocumentReader.java
/**
 * 使用 Spring 的 Bean 规则从 Document 的根元素开始进行 Bean 定义的 Document 对象。<p/>
 * doRegisterBeanDefinitions ->  parseBeanDefinitions -> parseDefaultElement<p/>
 *
 * Parse the elements at the root level in the document:
 * "import", "alias", "bean".
 * @param root the DOM root element of the document
 */
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // Bean 定义的 Document 对象使用了 Spring 默认的 XML 命名空间
    if (delegate.isDefaultNamespace(root)) {
        // 获取 Bean 定义的 Document 镀锌根元素的所有子节点
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            // 获取 Document 节点是 XML 元素节点
            if (node instanceof Element ele) {
                // Bean 定义的 Document 对象使用了 Spring 默认的 XML 命名空间
                if (delegate.isDefaultNamespace(ele)) {
                    // 使用 Spring 的 Bean 规则解析元素节点
                    // 解析默认元素
                    parseDefaultElement(ele, delegate);
                }
                else {
                    // 没有使用 Spring 默认的 XML 命名空间,则使用用户自定义的解析规则解析元素节点
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        // Document 的根节点没有使用 Spring 默认的命名空间,则使用用户自定义的解析规则解析元素节点
        delegate.parseCustomElement(root);
    }
}

DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法中,将 XML 标签区分为默认命名空间 beans 和其他自定义命名空间。 <bean> 在默认命名空间下,接下来看一下 DefaultBeanDefinitionDocumentReader#parseDefaultElement 方法:

DefaultBeanDefinitionDocumentReader.java
// 使用 Spring 的 Bean 规则解析 Document 元素节点
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    // 如果元素节点是 <import> 导入元素,进行导入解析
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        importBeanDefinitionResource(ele);
    }
    // 如果元素节点是 <alias> 别名元素,进行别名解析
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        processAliasRegistration(ele);
    }
    // 如果元素节点是 <bean> 元素,进行 Bean 解析
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        processBeanDefinition(ele, delegate);
    }
    // 如果是 <beans> 元素,则递归调用 `doRegisterBeanDefinitions` 方法进行处理
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // recurse
        doRegisterBeanDefinitions(ele);
    }
}

DefaultBeanDefinitionDocumentReader#parseDefaultElement 中,终于找到了处理 <bean> 标签的方法: processBeanDefinition。但是,从上面的时序图可以看出,实际的脏活累活都是由 BeanDefinitionParserDelegate#parseBeanDefinitionElement 干的,我们直接看这个方法的代码:

BeanDefinitionParserDelegate.java
/**
 * 解析 Bean 配置信息中的 <bean> 元素。这个方法中主要处理 <bean> 元素的 id、name 和 别名属性。</p>
 *
 * Parses the supplied {@code <bean>} element. May return {@code null}
 * if there were errors during parse. Errors are reported to the
 * {@link org.springframework.beans.factory.parsing.ProblemReporter}.
 */
@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
    // 获取 <bean> 元素中的 id 属性值
    String id = ele.getAttribute(ID_ATTRIBUTE);
    // 获取 <bean> 元素中的 name 属性值
    String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);

    // 获取 <bean> 元素中的 alias 属性值
    List<String> aliases = new ArrayList<>();
    // 将 <bean> 元素中的 name 属性值存放到别名中
    // 如果 bean 有别名的话,那么就将别名分割解析
    if (StringUtils.hasLength(nameAttr)) {
        String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
        aliases.addAll(Arrays.asList(nameArr));
    }

    String beanName = id;
    // 如果 <bean> 元素中没有配置 id 属性值,将别名中的第一个值赋值给 beanName
    if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
        beanName = aliases.remove(0);
        if (logger.isTraceEnabled()) {
            logger.trace("No XML 'id' specified - using '" + beanName +
                    "' as bean name and " + aliases + " as aliases");
        }
    }

    // 检查 <bean> 元素配置的 id 或者 name 的唯一性。
    // containingBean 标识 <bean> 元素中是否包含子 <bean> 元素
    if (containingBean == null) {
        checkNameUniqueness(beanName, aliases, ele);
    }

    // 详细对 <bean> 元素中配置的 bean 定义进行解析的地方
    AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
    if (beanDefinition != null) {
        if (!StringUtils.hasText(beanName)) {
            try {
                // 如果不存在 beanName,那么根据 Spring 中提供的命名规则为当前 bean 生成对应的 beanName
                if (containingBean != null) {
                    // 如果 <bean> 元素总没有配置 id、alias 或者 name,且没有包含子元素
                    // <bean> 元素为解析的 Bean 生成一个唯一 beanName 并注册
                    beanName = BeanDefinitionReaderUtils.generateBeanName(
                            beanDefinition, this.readerContext.getRegistry(), true);
                }
                else {
                    // 如果 <bean> 元素总没有配置 id、alias 或者 name,且包含子元素
                    // <bean> 元素,为解析的 Bean 使用别名向 IoC 容器注册
                    beanName = this.readerContext.generateBeanName(beanDefinition);
                    // Register an alias for the plain bean class name, if still possible,
                    // if the generator returned the class name plus a suffix.
                    // This is expected for Spring 1.2/2.0 backwards compatibility.
                    // 为解析的 Bean 使用别名注册时,为了向后兼容 Spring 1.2/2.0,给别名添加类名后缀
                    String beanClassName = beanDefinition.getBeanClassName();
                    if (beanClassName != null &&
                            beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
                            !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
                        aliases.add(beanClassName);
                    }
                }
                if (logger.isTraceEnabled()) {
                    logger.trace("Neither XML 'id' nor 'name' specified - " +
                            "using generated bean name [" + beanName + "]");
                }
            }
            catch (Exception ex) {
                error(ex.getMessage(), ele);
                return null;
            }
        }
        String[] aliasesArray = StringUtils.toStringArray(aliases);
        return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
    }
    // 当解析出错时,返回 null
    return null;
}

BeanDefinitionParserDelegate#parseBeanDefinitionElement 只是对 <bean> 标签的定义做了解析,对于定义 Bean 属性的 <property> 标签的解析,则是放在 parseBeanDefinitionElement 方法中,我们来看一下这个方法:

BeanDefinitionParserDelegate.java
/**
 * 详细对 <bean> 元素中配置的 bean 定义其他属性进行解析。<br/>
 * 由于上面的方法中已经对 bean 的 id、name 和 alias 等属性进行了处理,
 * 该方法中主要处理除了这三个以外的其他属性。
 *
 * Parse the bean definition itself, without regard to name or aliases. May return
 * {@code null} if problems occurred during the parsing of the bean definition.
 */
@Nullable
public AbstractBeanDefinition parseBeanDefinitionElement(
        Element ele, String beanName, @Nullable BeanDefinition containingBean) {
    // 记录解析的 <bean>
    this.parseState.push(new BeanEntry(beanName));

    // 这里只读取 <bean> 元素中配置的 class 名字,然后载入到 BeanDefinition 中区。
    // 只记录配置的 class 名字,不做实例化。对象的实例化在依赖注入时完成。
    String className = null;
    if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
        className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
    }
    // 解析parent属性
    String parent = null;
    // 如果 <bean> 元素中配置了 parent 属性,则获取 parent 属性值
    if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
        parent = ele.getAttribute(PARENT_ATTRIBUTE);
    }

    try {
        // 根据 <bean> 元素配置的 class 名称和 parent 属性值创建 BeanDefinition。
        // 为载入 Bean 定义信息做准备
        // 创建装在 bean 信息的 AbstractBeanDefinition 对象,实际的实现是 GenericBeanDefinition
        AbstractBeanDefinition bd = createBeanDefinition(className, parent);

        // 对当前的 <bean> 元素中配置的一些属性进行解析和设置,如配置的单例(singleton)属性等
        parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
        // 为 <bean> 元素解析的 bean 设置 description 信息
        bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
        // 对 <bean> 元素的 meta(元信息)属性解析
        parseMetaElements(ele, bd);
        // 为 <bean> 元素的 lookup-method 属性解析
        parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
        // 为 <bean> 元素的 replaced-method 属性解析
        parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

        // 解析 <bean> 元素的构造方法设置
        parseConstructorArgElements(ele, bd);
        // 解析 <bean> 元素的 <property> 值
        parsePropertyElements(ele, bd);
        // 解析 <bean> 元素的 qualifier 属性
        parseQualifierElements(ele, bd);

        // 为当前解析的 bean 设置所需的资源和依赖对象
        bd.setResource(this.readerContext.getResource());
        bd.setSource(extractSource(ele));

        return bd;
    }
    catch (ClassNotFoundException ex) {
        error("Bean class [" + className + "] not found", ele, ex);
    }
    catch (NoClassDefFoundError err) {
        error("Class that bean class [" + className + "] depends on not found", ele, err);
    }
    catch (Throwable ex) {
        error("Unexpected failure during bean definition parsing", ele, ex);
    }
    finally {
        this.parseState.pop();
    }

    return null;
}

由于,可能存在多个 <property>,所以 BeanDefinitionParserDelegate#parsePropertyElements 是通过遍历来解析多个 <property>。单个 <property> 是通过 BeanDefinitionParserDelegate#parsePropertyElement 来解析的,来看一下这个方法:

BeanDefinitionParserDelegate.java
/**
 * 解析 <property> 子元素。
 * Parse a property element.
 */
public void parsePropertyElement(Element ele, BeanDefinition bd) {
    // 获取解析 <property> 子元素的名字
    String propertyName = ele.getAttribute(NAME_ATTRIBUTE);
    if (!StringUtils.hasLength(propertyName)) {
        error("Tag 'property' must have a 'name' attribute", ele);
        return;
    }
    this.parseState.push(new PropertyEntry(propertyName));
    try {
        // 如果一个 Bean 中已经有同名的 property 存在,则不进行解析,直接返回。
        // 即如果在同一个 Bean 中配置同名的 property,则只有第一个起作用。
        if (bd.getPropertyValues().contains(propertyName)) {
            error("Multiple 'property' definitions for property '" + propertyName + "'", ele);
            return;
        }
        // 解析获取 property 的值
        Object val = parsePropertyValue(ele, bd, propertyName);
        // 根据 property 的名字和值创建 property 实例
        PropertyValue pv = new PropertyValue(propertyName, val);
        // 解析 <property> 子元素中的属性
        parseMetaElements(ele, pv);
        pv.setSource(extractSource(ele));
        bd.getPropertyValues().addPropertyValue(pv);
    }
    finally {
        this.parseState.pop();
    }
}

从此方法可以看出:每个 <property> 都转化成了 PropertyValue 对象。

暂停一下,做个总结:到目前为止,通过 <bean> 配置的 Bean 被转化为一个 BeanDefinition 对象,该对象中,还包含了由 <property> 转化成了的 PropertyValue 对象对象集合,而这些集合元素中,就包含了占位符信息。

本篇到此为止。下一篇文章中,作为对比,我们看一下如果不使用 XML 配置,而只使用注解配置,