• Spring Bean 别名处理原理分析


    今天来和小伙伴们聊一聊 Spring 中关于 Bean 别名的处理逻辑。

    1. Alias

    别名,顾名思义就是给一个 Bean 去两个甚至多个名字。整体上来说,在 Spring 中,有两种不同的别名定义方式:

    1. 定义 Bean 的 name 属性,name 属性在真正的处理过程中,实际上就是按照别名来处理的。
    2. 通过 alias 标签定义专门的别名,通过 alias 定义出来的别名和 name 属性定义的别名最终都是合并在一起处理的,所以这两种定义别名的方式最终是殊途同归

    那么定义的别名是保存在哪里呢?

    大家知道,Bean 解析出来之后被保存在容器中,别名其实也是一样的,容器中存在一个 aliasMap 专门用来保存 Bean 的别名,保存的格式是 alias->name,例如有一个 Bean 的名字是 user,别名是 userAlias,那么保存在 aliasMap 中就是 userAlias->user。

    举个简单例子:

    <bean class="org.javaboy.demo.User" id="user" name="user4,user5,user6"/>
    <alias name="user" alias="user2"/>
    <alias name="user2" alias="user3"/>
    
    • 1
    • 2
    • 3

    在上面这段定义中,user2、user3、user4、user5、user6 都是别名。

    2. AliasRegistry

    2.1 AliasRegistry

    Spring 中为别名的处理提供了 AliasRegistry 接口,这个接口中提供了别名处理的主要方法:

    public interface AliasRegistry {
    	void registerAlias(String name, String alias);
    	void removeAlias(String alias);
    	boolean isAlias(String name);
    	String[] getAliases(String name);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • registerAlias:这个方法用来添加别名,核心逻辑就是向 aliasMap 中添加数据。
    • removeAlias:这个方法用来从 aliasMap 中移除一个别名。
    • isAlias:判断给定的 name 是否是一个别名。
    • getAliases:根据给定的名字去获取所有的别名。

    方法就这四个,看一下这个接口的实现类有哪些。

    大家看到,AliasRegistry 的实现类其实还是蛮多的,但是大部分都是容器,真正实现了 AliasRegistry 中四个方法的只有 SimpleAliasRegistry,其他的容器大部分其实都是为了具备别名管理的能力,继承了 SimpleAliasRegistry。

    所以真正给我们整活的其实是 SimpleAliasRegistry。

    2.2 SimpleAliasRegistry

    SimpleAliasRegistry 类中的内容比较多,为了讲解方便,我就挨个贴属性和方法出来,贴出来后和大家分享。

    private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);
    
    • 1

    首先,SimpleAliasRegistry 中定义了一个 aliasMap,这个就是用来保存别名的,这是一个 Map 集合,接下来所有的操作都是围绕这个集合展开。

    @Override
    public void removeAlias(String alias) {
    	synchronized (this.aliasMap) {
    		String name = this.aliasMap.remove(alias);
    		if (name == null) {
    			throw new IllegalStateException("No alias '" + alias + "' registered");
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个方法用来移除别名,移除的思路很简单,就是从 aliasMap 中移除数据即可,如果 remove 方法返回值为 null 那就说明要移除的别名不存在,那么直接抛出异常

    @Override
    public boolean isAlias(String name) {
    	return this.aliasMap.containsKey(name);
    }
    
    • 1
    • 2
    • 3
    • 4

    这个是判断是否包含某一个别名,这个判断简单。有一个跟它容易产生歧义的方法,如下:

    public boolean hasAlias(String name, String alias) {
    	String registeredName = this.aliasMap.get(alias);
    	return ObjectUtils.nullSafeEquals(registeredName, name) ||
    			(registeredName != null && hasAlias(name, registeredName));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个方法是判断给定的 name 和 alias 之间是否具备关联关系。判断的逻辑就是先去 aliasMap 中,根据 alias 查出来这个 alias 所对应的真实 beanName,即 registeredName,然后判断 registeredName 和 name 是否相等,如果相等就直接返回,如果不相等就继续递归调用,为什么要递归呢?因为 aliasMap 中存在的别名可能是这样的:

    • a->b
    • b->c
    • c->d

    即 a 是 b 的别名,b 是 c 的别名,c 是 d 的别名,现在如果想要判断 a 和 d 之间的关系,那么根据 a 查出来的 b 显然不等于 d,所以要继续递归,再根据 b 查 c,根据 c 查到 d,这样就能确定 a 和 d 是否有关系了。

    @Override
    public String[] getAliases(String name) {
    	List<String> result = new ArrayList<>();
    	synchronized (this.aliasMap) {
    		retrieveAliases(name, result);
    	}
    	return StringUtils.toStringArray(result);
    }
    private void retrieveAliases(String name, List<String> result) {
    	this.aliasMap.forEach((alias, registeredName) -> {
    		if (registeredName.equals(name)) {
    			result.add(alias);
    			retrieveAliases(alias, result);
    		}
    	});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    getAliases 方法是根据传入的 name 找到其对应的别名,但是由于别名可能存在多个,所以调用 retrieveAliases 方法递归去查找所有的别名,将找到的别名都存入到一个集合中,最终将集合转为数组返回。

    protected void checkForAliasCircle(String name, String alias) {
    	if (hasAlias(alias, name)) {
    		throw new IllegalStateException("Cannot register alias '" + alias +
    				"' for name '" + name + "': Circular reference - '" +
    				name + "' is a direct or indirect alias for '" + alias + "' already");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个方法用来检查别名是否存在死结,即 a 是 b 的别名,b 是 a 的别名这种情况。检查的方式很简单,就是调用 hasAlias 方法,但是将传入的两个参数颠倒过来就可以了。

    public void resolveAliases(StringValueResolver valueResolver) {
    	synchronized (this.aliasMap) {
    		Map<String, String> aliasCopy = new HashMap<>(this.aliasMap);
    		aliasCopy.forEach((alias, registeredName) -> {
    			String resolvedAlias = valueResolver.resolveStringValue(alias);
    			String resolvedName = valueResolver.resolveStringValue(registeredName);
    			if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) {
    				this.aliasMap.remove(alias);
    			}
    			else if (!resolvedAlias.equals(alias)) {
    				String existingName = this.aliasMap.get(resolvedAlias);
    				if (existingName != null) {
    					if (existingName.equals(resolvedName)) {
    						this.aliasMap.remove(alias);
    						return;
    					}
    					throw new IllegalStateException(
    							"Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias +
    							"') for name '" + resolvedName + "': It is already registered for name '" +
    							registeredName + "'.");
    				}
    				checkForAliasCircle(resolvedName, resolvedAlias);
    				this.aliasMap.remove(alias);
    				this.aliasMap.put(resolvedAlias, resolvedName);
    			}
    			else if (!registeredName.equals(resolvedName)) {
    				this.aliasMap.put(alias, resolvedName);
    			}
    		});
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这个方法是处理别名是占位符的情况,例如当引入了一个 .properties 文件之后,那么在配置别名的时候就可以引用 .properties 中的变量,那么上面这个方法就是用来解析变量的。

    例如下面这种情况,我有一个 alias.properties,如下:

    name=user
    alias=javaboy
    
    • 1
    • 2

    然后在 XML 文件中使用这个 properties 文件,如下:

    <context:property-placeholder location="classpath:alias.properties"/>
    <alias name="${name}" alias="${alias}"/>
    
    • 1
    • 2

    对于这种情况,一开始存入到 aliasMap 中的就是占位符了,resolveAliases 方法就是要将这些占位符解析为具体的字符串。

    大家看到,首先这里将 aliasMap 复制一份,生成一个 aliasCopy,然后进行遍历。在遍历时,根据 valueResolver 将引用使用的占位符解析为真正的字符,如果解析出来的。如果解析出来的 name 和别名是相同的,那么显然是有问题的,就需要把这个别名移除掉。

    继续判断,如果解析出来的别名和原本的别名不相等(说明别名使用了占位符),那么就去检查一下这个别名对应的 name,如果这个 name 已经存在,且等于占位符解析出来的 name,说明这个别名已经被定义过了,即重复定义,那么就把别名移除掉即可。如果这个别名指向的 name 和占位符解析出来的 name 不相等,说明试图让一个别名指向两个 bean,那么就直接抛出异常了。

    如果解析出来的别名还没有指向 name 属性的话,那么就正常处理,检查是否存在死结、移除带占位符的别名,存入解析之后的别名。

    最后,如果原本的名称和解析之后的属性名称不相等,那么就直接保存这个别名即可。

    @Override
    public void registerAlias(String name, String alias) {
    	synchronized (this.aliasMap) {
    		if (alias.equals(name)) {
    			this.aliasMap.remove(alias);
    		}
    		else {
    			String registeredName = this.aliasMap.get(alias);
    			if (registeredName != null) {
    				if (registeredName.equals(name)) {
    					return;
    				}
    				if (!allowAliasOverriding()) {
    					throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
    							name + "': It is already registered for name '" + registeredName + "'.");
    				}
    			}
    			checkForAliasCircle(name, alias);
    			this.aliasMap.put(alias, name);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这个就是使用最多的别名注册了,传入的参数分别是 bean 的 name 和 alias,如果 alias 跟 name 相等,二话不说直接移除,这个 alias 有问题。

    否则就去查询这个 alias,检查这个 alias 是否已经有对应的 name 了,如果有,且等于传入的 name,那么直接返回就行了,不用注册,因为已经注册过了;如果有且不等于传入的 name,那么就抛出异常,因为一个 alias 不能指向两个 name。最后就是检查和保存了。

    public String canonicalName(String name) {
    	String canonicalName = name;
    	String resolvedName;
    	do {
    		resolvedName = this.aliasMap.get(canonicalName);
    		if (resolvedName != null) {
    			canonicalName = resolvedName;
    		}
    	}
    	while (resolvedName != null);
    	return canonicalName;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这个方法用来解析出来别名里边顶格的名字,例如有一个 bean 有很多别名,a->b,b->c,c->d,那么这个方法的目的就是传入 a、b、c 中任意一个,返回 d 即可。因为 Spring 容器在处理的时候,并不用管这么多别名问题,容器只需要专注一个名字即可,因为最后一个别名实际上就是指向真实的 beanId 了,所以最终拿到的 bean 名称其实相当于 bean 的 ID 了。

    别名的处理主要就是这些方法。

    3. 原理分析

    前面我们说了,别名的来源主要是两个地方:name 属性和 alias 标签,我们分别来看。

    3.1 name 处理

    对于 name 属性的处理,有两个地方,一个是在 bean 定义解析的时候,将 name 属性解析为 alias,具体在 BeanDefinitionParserDelegate#parseBeanDefinitionElement 方法中(这个方法在之前跟大家讲 bean 的默认名称生成策略的时候,见过):

    @Nullable
    public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
    	String id = ele.getAttribute(ID_ATTRIBUTE);
    	String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
    	List<String> aliases = new ArrayList<>();
    	if (StringUtils.hasLength(nameAttr)) {
    		String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
    		aliases.addAll(Arrays.asList(nameArr));
    	}
        //省略其他
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到,这里就从 XML 节点中提取出来 name 属性,然后切分为一个数组,并将之存入到 aliases 属性中。接下来在后续的 BeanDefinitionReaderUtils#registerBeanDefinition 方法中,再把 aliases 中的值注册一下,如下:

    public static void registerBeanDefinition(
    		BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
    		throws BeanDefinitionStoreException {
    	String beanName = definitionHolder.getBeanName();
    	registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
    	String[] aliases = definitionHolder.getAliases();
    	if (aliases != null) {
    		for (String alias : aliases) {
    			registry.registerAlias(beanName, alias);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这就是 XML 中的 name 属性是如何变为别名的。

    3.2 别名标签处理

    别名的另一个来源是别名标签,在 Spring 解析 XML 标签的时候,有针对别名标签的专门处理,具体位置是在 DefaultBeanDefinitionDocumentReader#parseDefaultElement 方法中:

    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    	if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
    		importBeanDefinitionResource(ele);
    	}
    	else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
    		processAliasRegistration(ele);
    	}
    	else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
    		processBeanDefinition(ele, delegate);
    	}
    	else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
    		// recurse
    		doRegisterBeanDefinitions(ele);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里会去判断标签的类型,如果是别名,就调用 processAliasRegistration 方法进行处理:

    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 {
    			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));
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可以看到,这里也是从 XML 文件中的别名标签上,提取出来 name 和 alias 属性值,最后调用 registerAlias 方法进行注册。

    好啦,这就是 Spring 中关于别名的处理流程啦~

  • 相关阅读:
    URL 编码和解码工具
    手写能任务窃取的线程池
    [Machine Learning]pytorch手搓一个神经网络模型
    第十四届蓝桥杯省赛 C/C++ A 组 H 题——异或和之和(AC)
    【源码系列】MyBatis与Spring整合原理源码
    28335之GPIO输出
    概率论得学习整理--番外3:二项式定理和 二项式系数
    Science经典:植物基因组的同线性与共线性分析思路
    BMS电池管理系统理论基础
    【MongoDB】索引 - 复合索引
  • 原文地址:https://blog.csdn.net/u012702547/article/details/132687461