背景简介
出现的原因
我们在上面介绍了AliasRegistry
定义了从 alias-->id
的种种增删改查操作。在 Spring 框架的运行中使用场景往往是从配置文件或者打着注解的代码中加载、解析成 BDH(BeanDefinitionHolder
)然后注册,所以我们对AliasRegistry
的实现也应该是内存数据结构——我们本节介绍的SimpleAliasRegistry
就是使用Java的集合类对AliasRegistry
做了基本实现。
职责
SimpleAliasRegistry
为他的子类提供了线程安全的管理别名映射的功能。
注意点
这个类使用了一些线程安全机制,根据之前学习的多线程知识进行甄别。
源码
实例属性
private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);
使用 ConcurrentHashMap
以保证多线程操作存取时的线程安全。
从这里看是将线程安全委托给ConcurrentHashMap
了。
注意:这个 Map
的key
是别名,value
是id或者另一个别名【这个可以传递的】
实现接口方法【增、删、查】
增
@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) {
// 如果别名就是真实名称,直接去掉缓存即可,没必要通过 SimpleAliasRegistry来走弯路
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)) {
// 已经存在了,不用再加了
return;
}
if (!allowAliasOverriding()) { // 看是否允许别名覆盖
throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
name + "': It is already registered for name '" + registeredName + "'.");
}
if (logger.isInfoEnabled()) {
logger.info("Overriding alias '" + alias + "' definition for registered name '" +
registeredName + "' with new target name '" + name + "'");
}
}
// 到这里,有两种情况:
// 或者这个 alias 没在 map 中当 key
// 或者 alias 存在,但是 value 和入参不同且可覆盖 【当然,不确定是不是间接的】
//
// 因为这里是递归的,防止出现循环依赖
checkForAliasCircle(name, alias); // 见工具函数
this.aliasMap.put(alias, name);
if (logger.isDebugEnabled()) {
logger.debug("Alias definition '" + alias + "' registered for name '" + name + "'");
}
}
}
}
protected boolean allowAliasOverriding() {
return true;
}
这里,你会发现:整个操作都是加锁的,因为逻辑比较复杂,设计多次的存取判断,仅靠委托给ConcurrentHashMap
是不行的。
既然这样,如果我们使用了锁来操作,是不是ConcurrentHashMap
又是多余的了呢?
删
@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");
}
}
}
删除操作也进行了加锁。ConcurrentHashMap
照样没用。
不允许随便传值,删除不存在的key
会报错
查
@Override
// 判断入参别名是否存在
public boolean isAlias(String name) {
return this.aliasMap.containsKey(name);
}
@Override
// 获得直接/间接指向 name 的 alias
public String[] getAliases(String name) {
List<String> result = new ArrayList<>();
synchronized (this.aliasMap) {
retrieveAliases(name, result);//
}
return StringUtils.toStringArray(result);
}
// 思路很简单,因为是根据 value 找 key ,所以需要遍历 Map。
// 递归操作,涉及树遍历【深度遍历】
private void retrieveAliases(String name, List<String> result) {
this.aliasMap.forEach((alias, registeredName) -> {
if (registeredName.equals(name)) {
result.add(alias);
retrieveAliases(alias, result);
}
});
}
工具函数
/**
* 对 Map 中的所有 key、value进行解析操作,因为有的里面是有 Spring 表达式的,需要去掉 Spring 的
* 那些占位符
*/
public void resolveAliases(StringValueResolver valueResolver) {
Assert.notNull(valueResolver, "StringValueResolver must not be null");
synchronized (this.aliasMap) {
// 复制 Map,方便找一个不动的参考来进行遍历
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)) {
// 解析后的key value存在空,或者解析后key value相同
// 也就是说这个映射是多余的。直接删除
this.aliasMap.remove(alias);
} else if (!resolvedAlias.equals(alias)) {
// 完成解析,且key、value看起来合理
String existingName = this.aliasMap.get(resolvedAlias);
// 解析后的 key 发现已经在map中存在了
if (existingName != null) {
if (existingName.equals(resolvedName)) {
// 解析后的 value 和解析后的 key 映射的是一个值,所以不用专门把解析后的key value放进去了
// 直接删了未解析的映射即可
// Pointing to existing alias - just remove placeholder
this.aliasMap.remove(alias);
return;
}
// 解析后的 key 已经存在,但是指向的 value 不是我们解析出来的 value ,这个坑已经被占了,存在冲突!
throw new IllegalStateException(
"Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias +
"') for name '" + resolvedName + "': It is already registered for name '" +
registeredName + "'.");
}
// 解析后的 key 在 map 中不存在,看看没有啥循环引用啥的,就把未解析的键值对换成解析后的键值对即可
checkForAliasCircle(resolvedName, resolvedAlias);
this.aliasMap.remove(alias);
this.aliasMap.put(resolvedAlias, resolvedName);
} else if (!registeredName.equals(resolvedName)) {
// 解析后的 key 不变,但是 value 变了,直接做值覆写即可
// TODO:key 不变 value 变了 不用 check 一下循环引用吗?
this.aliasMap.put(alias, resolvedName);
}
});
}
}
/**
* 检测循环依赖,即存在 A--->B--->*--->A
*
* @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");
}
}
/**
* 判断是否存在 alias--->name 的直接或者间接的映射
**/
public boolean hasAlias(String name, String alias) { // 存在多线程访问时的问题
// 递归函数,从真实名称往上走。
// TODO: 这里存疑,为什么不能直接运用map的特点,从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;
}
/**
* 找到别名的最终映射的 id
*/
// 通过从 map 中反复查找,找到 "name" 对应的 alias 在 map 中最终指向的名字
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;
}
总结记录
就是一个基本的实现。重点熟悉一下那几个工具函数即可。
问题
滥用线程安全类
使用ConcurrentHashMap
在操作中没有任何帮助,原因如下:
- 实例属性为
private
且没有getter
,不存在泄漏可能 - 所有方法均为复杂操作,需要自行加锁,无法直接委托给
ConcurrentHashMap
综上,使用ConcurrentHashMap
除了拖慢存取速度,没有其他用处。
工具函数存在线程安全问题
在前面几个函数中明显顾及了线程安全问题,但是在最后几个函数中:例如checkForAliasCircle
,hasAlias
,canonicalName
均并没有顾及线程安全,在执行这些方法的递归/循环时,如果Map
被其他线程修改,会返回错误的结果。
扩展
多线程基础知识。回头看。
网友评论