深入理解Spring別名機制

前言

在 spring 容器中,允許通過名稱或別名來獲取 bean ,這個能力來自於頂層接口 AliasRegistry,分析類下屬的關係圖,可以看到,幾乎所有主要容器都直接或間接的實現了 AliasRegistry 接口。

image-20220621134756391

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 -> ac -> b 的引用關係。

此時,若想要確定 c 是否是 a 的別名,就需要在通過 c 取出 b 後,再向上遞歸根據 b 找到 a,把每一級的關係卻判斷一遍。

爲什麼要加鎖

理解了循環引用的檢查,也就不難理解加鎖的必要性了。實際使用中,SimpleAliasRegistry 基本是作爲單例使用,此時不可避免的可能會存在多線程調用 registerAlias 的可能性,即便 aliasMap 使用的 ConcurrentHashMap 本身是線程安全的,但是這不能阻止出現 a -> bb -> a 兩個操作的同時發生,此時 ab 構成了循環引用,而每個線程中的 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,則從 abcd 任意一個名稱開始調用該方法,則最終都會返回 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 一致,本質上就是將原有的別名的與名稱轉換後再次建立映射關係,然後移除舊的映射關係。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章