前言
在 spring 容器中,允許通過名稱或別名來獲取 bean ,這個能力來自於頂層接口 AliasRegistry
,分析類下屬的關係圖,可以看到,幾乎所有主要容器都直接或間接的實現了 AliasRegistry
接口。
AliasRegistry
的結構非常簡單,主要的類就是 AliasRegistry
接口與他的實現類 SimpleAliasRegistry
,後續的實現類基本都直接或間接的繼承了 SimpleAliasRegistry
。
本文將基於 spring 源碼 5.2.x
分支,圍繞 SimpleAliasRegistry
解析 spring 的別名機制是如何實現的。
一、AliasRegistry
在 spring 的容器中,一個 bean 必須至少有一個名稱,而一個名稱可以有多個別名,別名亦可以有別名,若我們把這個最原始的名稱稱爲 id
,則結構可以有:
id -> id's alias1 -> alias of id's alias1 ... ...
-> id's alias2 -> alias of id's alias2 ... ...
通過 bean 的 id
,或與該 id
直接、間接相關的別名,都可以從容器中獲取到對應的 bean。
AliasRegistry
的作用就是定義這一套規則:
public interface AliasRegistry {
// 爲指定名稱註冊別名
void registerAlias(String name, String alias);
// 移除別名
void removeAlias(String alias);
// 判斷名稱是否爲別名
boolean isAlias(String name);
// 獲取該名稱的別名
String[] getAliases(String name);
}
二、SimpleAliasRegistry
SimpleAliasRegistry
是直接基於 AliasRegistry
接口提供的簡單實現類,他在內部維護了一個 Map
集合,以別名作爲 key,別名對應的原名稱作爲 value:
public class SimpleAliasRegistry implements AliasRegistry {
/** Map from alias to canonical name. */
private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);
}
在 SimpleAliasRegistry
中,我們在上文所提到的 id
,被稱爲標準名稱 CanonicalName
。
1、註冊別名
@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) {
// 1.若別名與原名稱一致,則直接移除該別名,否則繼續後續處理
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);
// 2.獲取該別名的對應的原名稱,若該別名已有對應的原名稱,則:
if (registeredName != null) {
// a.已對應的原名稱和要對應的原名稱相同,則放棄後續處理
if (registeredName.equals(name)) {
// An existing alias - no need to re-register
return;
}
// a.若不允許重寫原名稱對應的別名,則直接拋出異常
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 + "'");
}
}
}
}
是否允許覆蓋別名對應的原名稱
在 registerAlias
方法中,調用了 allowAliasOverriding
方法,該方法在 SimpleAliasRegistry
中固定返回 true
:
protected boolean allowAliasOverriding() {
return true;
}
該方法決定一個已經有對應原名稱的別名,是否允許被重新被對應到另一個原名稱。跟 HashMap
中的 afterNodeXXX
方法一樣,這裏很明顯是留給子類重寫的鉤子方法。
檢查是否存在循環引用
在 registerAlias
方法中,調用了 checkForAliasCircle
以檢查別名是否存在 a -> b -> a
這樣的循環引用:
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");
}
}
public boolean hasAlias(String name, String alias) {
String registeredName = this.aliasMap.get(alias);
// 判斷該別名對應的原名稱是否就是要找的原名稱
return ObjectUtils.nullSafeEquals(registeredName, name)
// 遞歸檢查別名的別名對應的原名稱是否爲要找的原名稱
|| (registeredName != null && hasAlias(name, registeredName));
}
因爲在 SimpleAliasRegistry
中,一個名稱作爲另一個名稱的別名後,該名稱仍然允許有別的名稱作爲它的別名。
舉個例子,實際場景中,可能會出現 a -> b -> c
這種情況,對應在 aliasMap
中則存在 b -> a
與 c -> b
的引用關係。
此時,若想要確定 c
是否是 a
的別名,就需要在通過 c
取出 b
後,再向上遞歸根據 b
找到 a
,把每一級的關係卻判斷一遍。
爲什麼要加鎖
理解了循環引用的檢查,也就不難理解加鎖的必要性了。實際使用中,SimpleAliasRegistry
基本是作爲單例使用,此時不可避免的可能會存在多線程調用 registerAlias
的可能性,即便 aliasMap
使用的 ConcurrentHashMap
本身是線程安全的,但是這不能阻止出現 a -> b
和 b -> a
兩個操作的同時發生,此時 a
與 b
構成了循環引用,而每個線程中的 checkForAliasCircle
卻未必能夠正確的檢測出問題。
2、獲取名稱
獲取全部別名
@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);
}
});
}
獲取別名也很好理解,首先加鎖同樣的爲了避免刪除的同時註冊別名導致的一些問題,然後遞歸調用的 retrieveAliases
也是爲了防止出現“別名還有別名”的情況,getAliases
最終將返回該名稱與該名稱構成直接或間接引用關係的別名。
獲取標準名稱
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;
}
該方法的作用是通過一個別名獲得對應的標準名稱。舉個例子,假如現在存在如下別名引用關係:a -> b -> c -> d
,則從 a
、b
、c
、d
任意一個名稱開始調用該方法,則最終都會返回 a
。
3、移除別名
@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");
}
}
}
4、名稱轉換
有時候,直接註冊到 SimpleAliasRegistry
的名稱和對應別名未必就是最想要,還需要替換佔位符或者進行一些大小寫轉換的操作,這就需要通過 resolveAliases
方法來完:
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);
// a.若轉換後的別名已有對應的原名稱,且與轉換後的新原名稱相同,則移除該別名的映射關係,否則報錯
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);
}
});
}
}
其中,StringValueResolver
是一個函數式接口,它用於將一個字符串轉爲另一個字符串,也就是 resolve
的過程:
@FunctionalInterface
public interface StringValueResolver {
@Nullable
String resolveStringValue(String strVal);
}
resolveAliases
的過程基本與註冊別名的方法 registerAlias
一致,本質上就是將原有的別名的與名稱轉換後再次建立映射關係,然後移除舊的映射關係。