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、参考
- [1]spring 官方文档 5.2.3.RELEASE:Aliasing a Bean outside the Bean Definition https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-beanname-alias
- [2]spring 官方文档 5.2.3.RELEASE:Bean Aliasing https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-java-bean-aliasing
- [3]Composing XML-based Configuration Metadata https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-factory-xml-import
- [4]1.13.4. Placeholder Resolution in Statements https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-placeholder-resolution-in-statements
- [5]Spring源码深度解析(第2版),郝佳,P72-P76
- [6]相关注释可参考笔者 github 链接:https://github.com/wpbxin/spring-framework