专栏首页BAT的乌托邦【小家Spring】分享Spring中一个小巧而优雅的类SimpleAliasRegistry源码分析(别名注册、管理器)

【小家Spring】分享Spring中一个小巧而优雅的类SimpleAliasRegistry源码分析(别名注册、管理器)

前言

上一篇分享了:【小家Spring】一文读懂Spring中的BeanFactory和FactoryBean的区别。为了缓解疲劳,本文来个小插曲~~不用费太多脑力的

Spring是一个非常优秀且流行的框架,里面不乏有很多优秀的设计模式、设计思想。

本文主要针对其中一个非常小巧的类:SimpleAliasRegistry做一个源码解读。顺便也分享给大家,若有分析得不到位的地方,非常欢迎指正,毕竟我也是第一次看。

分析此类的源码是因为此类很具有代表性,可以部分代表Spring的代码功底,优雅~~~因为群里有好几次提到过说此类虽然很小巧,但是代码设计得很优雅

初识

首先看到,这个类实现了接口AliasRegistry,而这个接口顾名思义:它就是别名管理器。Spring提供了一个默认实现:SimpleAliasRegistry。内部会缓存这些别名和真实名称的对应关系

在Spring环境下,我们很容易的为一个Bean定义一个或者多个别名:

 <bean id="app:dataSource" class="...">
    <alias name="app:dataSoure" alias="user:dataSoure"/>
    <alias name="app:dataSoure" alias="device:dataSoure"/>
  </bean>
或者:
直接使用bean标签的name属性,就是别名
<bean id="aaa",name="bbb,ccc,ddd"/>

到这个时候,可能很多人就会问了,当前都SpringBoot环境了,哪还会用到xml了啊,所以别名这个在Boot这就不会再用了。

其实不然,SpringBoot中我们配置一个Bean一般会这么来写:

@Configuration
public class ApplicationConfig {

    @Bean
    public Object object() {
        return new Object();
    }

}

这个时候我打断点,发现还真的没有注册别名。而@Bean注解里也并没有alias等相关属性,是不是Boot就真的不支持了呢?

其实,只支持的。@Bean虽然没有alias属性,但是它的名称可以是数组,可以写多个名称,而经过我实现发现。当只写一个值的时候,只有名称没有别名。但是当你写多个值的时候,除了第一个是名称,后面的全都是别名。

    @Bean(value = {"aaa", "bbb", "ccc"})
    public Object object() {
        return new Object();
    }

断点启动:

证实了我上面的结论。虽然别名我们用得挺少,特别是在当下的Boot环境了。但是强大的Spring还是支持的。

当然这并不是本文讨论的重点,重点还是看“优雅的”代码:

public interface AliasRegistry {
	//增  给name新增一个别名alias
	void registerAlias(String name, String alias);
	//删  删除一个别名
	void removeAlias(String alias);
	//此name是否含有别名
	boolean isAlias(String name);
	//获取此name对应的所有的别名
	String[] getAliases(String name);

}

它的实现类有不少,此处我们只看SimpleAliasRegistry :

public class SimpleAliasRegistry implements AliasRegistry {

	/** Logger available to subclasses. */
	protected final Log logger = LogFactory.getLog(getClass());

	/** Map from alias to canonical name. */
	private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);


	@Override
	public void registerAlias(String name, String alias) {
		Assert.hasText(name, "'name' must not be empty");
		Assert.hasText(alias, "'alias' must not be empty");
		synchronized (this.aliasMap) {
			if (alias.equals(name)) {
				this.aliasMap.remove(alias);
				if (logger.isDebugEnabled()) {
					logger.debug("Alias definition '" + alias + "' ignored since it points to same name");
				}
			}
			else {
				String registeredName = this.aliasMap.get(alias);
				if (registeredName != null) {
					if (registeredName.equals(name)) {
						// An existing alias - no need to re-register
						return;
					}
					if (!allowAliasOverriding()) {
						throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
								name + "': It is already registered for name '" + registeredName + "'.");
					}
					if (logger.isDebugEnabled()) {
						logger.debug("Overriding alias '" + alias + "' definition for registered name '" +
								registeredName + "' with new target name '" + name + "'");
					}
				}
				checkForAliasCircle(name, alias);
				this.aliasMap.put(alias, name);
				if (logger.isTraceEnabled()) {
					logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'");
				}
			}
		}
	}

	/**
	 * Return whether alias overriding is allowed.
	 * Default is {@code true}.
	 */
	protected boolean allowAliasOverriding() {
		return true;
	}

	/**
	 * Determine whether the given name has the given alias registered.
	 * @param name the name to check
	 * @param alias the alias to look for
	 * @since 4.2.1
	 */
	public boolean hasAlias(String name, String alias) {
		for (Map.Entry<String, String> entry : this.aliasMap.entrySet()) {
			String registeredName = entry.getValue();
			if (registeredName.equals(name)) {
				String registeredAlias = entry.getKey();
				if (registeredAlias.equals(alias) || hasAlias(registeredAlias, alias)) {
					return true;
				}
			}
		}
		return false;
	}

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

	@Override
	public boolean isAlias(String name) {
		return this.aliasMap.containsKey(name);
	}

	@Override
	public String[] getAliases(String name) {
		List<String> result = new ArrayList<>();
		synchronized (this.aliasMap) {
			retrieveAliases(name, result);
		}
		return StringUtils.toStringArray(result);
	}

	/**
	 * Transitively retrieve all aliases for the given name.
	 * @param name the target name to find aliases for
	 * @param result the resulting aliases list
	 */
	private void retrieveAliases(String name, List<String> result) {
		this.aliasMap.forEach((alias, registeredName) -> {
			if (registeredName.equals(name)) {
				result.add(alias);
				retrieveAliases(alias, result);
			}
		});
	}

	/**
	 * Resolve all alias target names and aliases registered in this
	 * factory, applying the given StringValueResolver to them.
	 * <p>The value resolver may for example resolve placeholders
	 * in target bean names and even in alias names.
	 * @param valueResolver the StringValueResolver to apply
	 */
	public void resolveAliases(StringValueResolver valueResolver) {
		Assert.notNull(valueResolver, "StringValueResolver must not be null");
		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)) {
							// Pointing to existing alias - just remove placeholder
							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);
				}
			});
		}
	}

	/**
	 * Check whether the given name points back to the given alias as an alias
	 * in the other direction already, catching a circular reference upfront
	 * and throwing a corresponding IllegalStateException.
	 * @param name the candidate name
	 * @param alias the candidate alias
	 * @see #registerAlias
	 * @see #hasAlias
	 */
	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");
		}
	}

	/**
	 * Determine the raw name, resolving aliases to canonical names.
	 * @param name the user-specified name
	 * @return the transformed name
	 */
	public String canonicalName(String name) {
		String canonicalName = name;
		// Handle aliasing...
		String resolvedName;
		do {
			resolvedName = this.aliasMap.get(canonicalName);
			if (resolvedName != null) {
				canonicalName = resolvedName;
			}
		}
		while (resolvedName != null);
		return canonicalName;
	}

}

本文就是重点分析此类,代码行数若除去注释,100行左右,但是里面的写法确实是有不少可圈可点的地方。

Tips:此类在Spring中都是被Bean定义、创建的时候继承使用,和Bean的定义相关联

为了便于理解,我这里把最重要的一个方法:registerAlias(String name, String alias)画出逻辑流程图如下:

源码步骤分析

首先,使用了一个线程安全的Map:ConcurrentHashMap当做注册表来缓存alias和name的对应关系。key为alias别名,value为name真实值。可以通过别名获取到真实值,进而获取到Bean实例

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

为了方便大家下面阅读源码更好的理解为什么这么做?我在这里先说明两点: 1、需要避免循环别名和name之间循环引用的问题。比如a->b b->c c->a这就循环引用了,是需要避免的,否则很容易出问题 2、不能出现并发问题直接出现如下情况。a -> b b->a (其实也是一种循环引用嘛)

首先我们看一下registerAlias方法,也是最为复杂点的方法:

	@Override
	public void registerAlias(String name, String alias) {
		Assert.hasText(name, "'name' must not be empty");
		Assert.hasText(alias, "'alias' must not be empty");
	
		//此处注意:很多人疑问的地方,用了ConcurrentHashMap,为何此处还要加锁呢?有必要吗?
		//答:非常有必要的。因为ConcurrentHashMap只能保证单个put、remove方法的原子性。而不能保证多个操作同时的原子性。比如我一边添加、一边删除  显然这是不被允许的
		synchronized (this.aliasMap) {
			//若发现别名和name是相同的,就不需要做啥了。而且顺手把这个key给移除掉
			if (alias.equals(name)) {
				this.aliasMap.remove(alias);
				if (logger.isDebugEnabled()) {
					logger.debug("Alias definition '" + alias + "' ignored since it points to same name");
				}
			} else {
				//拿到这个别名对应的name,看看该别名是否已经存在对应的name了
				String registeredName = this.aliasMap.get(alias);
				if (registeredName != null) {
					//若已经存在对应的name了,而且还和传进俩的name相同,那啥都不做就行
					if (registeredName.equals(name)) {
						return;
					}
			
					//若存在对应的name了,切还不让复写此别名(让其指向别的name),那就跑错吧
					if (!allowAliasOverriding()) {
						throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
								name + "': It is already registered for name '" + registeredName + "'.");
					}
					if (logger.isDebugEnabled()) {
						logger.debug("Overriding alias '" + alias + "' definition for registered name '" +
								registeredName + "' with new target name '" + name + "'");
					}
				}
				
				checkForAliasCircle(name, alias);
				this.aliasMap.put(alias, name);
				if (logger.isTraceEnabled()) {
					logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'");
				}
			}
		}
	}

当alias对应的name不存在的时候,还需要去检查是否可能存在循环引用现象:checkForAliasCirclee如下:

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

对应的hasAlias方法如下:

	public boolean hasAlias(String name, String alias) {
		for (Map.Entry<String, String> entry : this.aliasMap.entrySet()) {
			String registeredName = entry.getValue();
			
			//若找到了此name 然后就拿出其对应的alias(可能会有多次哦)
			if (registeredName.equals(name)) {
				String registeredAlias = entry.getKey();
				
				//如果此alias和传入的alias相同,返回true  证明name有这个alias
				//一般人可能上面那一步就算了直接return了,但是,但是,但是还有一种情况也必须考虑到:倘若这个已经注册过的registeredAlias和传入的alias不相等。
				//但是把他作为name去找是否有alias的时候,如果有也得判断是true,表示有。 防止了a -> b  b->c  c->a的循环的情况  此处处理可以说是非常的优雅和谨慎了~
				if (registeredAlias.equals(alias) || hasAlias(registeredAlias, alias)) {
					return true;
				}
			}
		}
		return false;
	}

该方法旨在判断:给定的name,是否已经有了对应的alias别名呢?

注册的难点在于如何防止重现名称和别名之间的重复引用。

最后用一张图形象的解释一下,为什么需要加锁?

在注册别名时,检查别名是否注册过名称这一步,如果不对注册表加锁,会导致检查出现问题,最终导致出现重复引用

两个注册过程并发进行,在检查时两个注册过程均未发现问题,但是注册过后便会出现问题

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 异步事务?关于异步@Async + 事务@Transactional的结合使用问题分析【享学Spring MVC】

    如题,如果把这两者拆开来看的话,两者你应该都不陌生:@Async你不陌生,@Transactional你应该更不陌生,若单独拿来使用,理论上应该木有不会用的吧。...

    YourBatman
  • 【小家Spring】源码分析Spring的事务拦截器:TransactionInterceptor和事务管理器:PlatformTransactionManager

    接着上一篇博文: 【小家Spring】从基于@Transactional全注解方式的声明式事务入手,彻底掌握Spring事务管理的原理

    YourBatman
  • Spring中PropertySource属性源配置文件的优先级、顺序问题大解析(加载流程)【享学Spring】

    关于Spring的配置文件的优先级、加载顺序一直是个老生常谈的问题。但即使经常被提起,却还是经常被忘记或者弄混。有一种听了很多道理但仍过不好这一生的赶脚有木有。

    YourBatman
  • vue路由跳转传参数

    1. router-link <router-link :to="{ path: 'yourPath', param...

    庞小明
  • 错误日志告警实战

    如果不差钱,更系统更完善的解决方案,我首先想到的是CAT,它不但能实现错误告警,且更加智能,告警的错误间隔,错误告警内容,QPS告警等等方式更多样化,还能查看接...

    老梁
  • UVA - 1152 --- 4 Values whose Sum is 0(二分)

    首先枚举a和b, 把所有a+b记录下来放在一个有序数组,然后枚举c和d, 在有序数组中查一查-c-d共有多少个。注意这里不可以直接用二分算法的那个模板,因为那个...

    _DIY
  • EditPlus中有用的快捷键

    点击到该标记(全选或光标落在里面即可)按下ctrl+[就可以找到匹配的标签了。权支持HTML标签的匹配。

    meteoric
  • 私有云中Kubernetes Clu

    Kubernetes Master HA架构图 ? 配置与说明 所有组件可以通过kubelet static pod的方式启动和管理,由kubelet st...

    Walton
  • JS进阶 你真的掌握变量和类型了吗

    原文链接:https://mp.weixin.qq.com/s/Z0jnNJlfOrXHdNDb8CM-ng

    mafeifan
  • 【JS进阶】你真的掌握变量和类型了吗

    变量和类型是学习JavaScript最先接触到的东西,但是往往看起来最简单的东西往往还隐藏着很多你不了解、或者容易犯错的知识,比如下面几个问题:

    ConardLi

扫码关注云+社区

领取腾讯云代金券