深入理解Spring註解機制(三):合併註解的合成

前言

衆所周知,spring 從 2.5 版本以後開始支持使用註解代替繁瑣的 xml 配置,到了 springboot 更是全面擁抱了註解式配置。平時在使用的時候,點開一些常見的等註解,會發現往往在一個註解上總會出現一些其他的註解,比如 @Service

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // @Component
public @interface Service {
    @AliasFor(annotation = Component.class)
    String value() default "";
}

大部分情況下,我們可以將 @Service 註解等同於 @Component 註解使用,則是因爲 spring 基於其 JDK 對元註解的機制進行了擴展。

在 java 中,元註解是指可以註解在其他註解上的註解,spring 中通過對這個機制進行了擴展,實現了一些原生 JDK 不支持的功能,比如允許在註解中讓兩個屬性互爲別名,或者將一個帶有元註解的子註解直接作爲元註解看待,或者在這個基礎上,通過 @AliasFor 或者同名策略讓子註解的值覆蓋元註解的值。

本文將基於 spring 源碼 5.2.x 分支,解析 spring 如何實現這套功能的。

這是系列的第三篇文章,將詳細介紹 Spring 是如何在經過搜索與屬性映射後,將處理後的註解合成爲合併註解的。

相關文章:

一、合併註解

我們在前文了解用於搜索註解的合併註解聚合 MergedAnnotations與用於完成註解屬性映射的 AnnotationTypeMappingsAnnotationTypeMapping,現在我們需要知道在 MergedAnnotations 這個容器中,AnnotationTypeMappingsAnnotationTypeMapping 是如何轉爲一個我們所需要的合併註解 MergedAnnotation 的。

與前文一樣,我們以 AnnotatedElementUtils.findMergedAnnotations 方法作爲入口:

public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> annotationType) {
    // 1、下述任意情況下直接獲取元素上聲明的註解:
    // a.查找的註解屬於java、javax或者org.springframework.lang包
    // b.被處理的元素屬於java包,或被java包中的對象聲明,或者就是Ordered.class
    if (AnnotationFilter.PLAIN.matches(annotationType) ||
        AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
        return element.getDeclaredAnnotation(annotationType);
    }

    // 2、將元素上的全部註解合成MergedAnnotation
    return findAnnotations(element)
        // 3、從MergedAnnotation獲取與該類型對應的MergedAnnotations
        .get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared())
        // 4、根據MergedAnnotation通過動態代理生成一個註解實例
        .synthesize(MergedAnnotation::isPresent).orElse(null);
}

我們在上文順着 MergedAnnotations.get 一路找到 TypeMappedAnnotations.MergedAnnotationFinderprocess 方法,在這裏我們目睹了一個普通的註解的元註解被解析爲 AnnotationTypeMappingsAnnotationTypeMapping 的過程:

private MergedAnnotation<A> process(
    Object type, int aggregateIndex, @Nullable Object source, Annotation annotation) {

    Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation);
    if (repeatedAnnotations != null) {
        return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations);
    }
    // 將該註解上的元註解解析爲由多個AnnotationTypeMapping聚合的AnnotationTypeMappings
    AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(
        annotation.annotationType(), repeatableContainers, annotationFilter);
    for (int i = 0; i < mappings.size(); i++) {
        // 獲取元註解對應的AnnotationTypeMapping
        AnnotationTypeMapping mapping = mappings.get(i);
        if (isMappingForType(mapping, annotationFilter, this.requiredType)) {
            // 使用AnnotationTypeMapping創建一個合併註解
            MergedAnnotation<A> candidate = TypeMappedAnnotation.createIfPossible(
                mapping, source, annotation, aggregateIndex, IntrospectionFailureLogger.INFO);
            if (candidate != null && (this.predicate == null || this.predicate.test(candidate))) {
                if (this.selector.isBestCandidate(candidate)) {
                    return candidate;
                }
                updateLastResult(candidate);
            }
        }
    }
    return null;
}

其他部分在上文已經詳細的分析了,因此此處我們僅需關注 TypeMappedAnnotation.createIfPossible 這一個方法:

static <A extends Annotation> TypeMappedAnnotation<A> createIfPossible(
    AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation,
    int aggregateIndex, IntrospectionFailureLogger logger) {

    return createIfPossible(mapping, source, annotation,
                            ReflectionUtils::invokeMethod, aggregateIndex, logger);
}

該方法是 AnnotationTypeMapping 轉爲 MergedAnnotation 的關鍵。

1、合併註解的創建

TypeMappedAnnotationMergedAnnotation 一個通用實現,在大部分情況下,我們所說的合併註解其實指的就是這個類。

通過它的構造方法我們得以瞭解其創建過程:

private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader,
                             @Nullable Object source, @Nullable Object rootAttributes, ValueExtractor valueExtractor,
                             int aggregateIndex, @Nullable int[] resolvedRootMirrors) {

    // 當前合併註解對應的AnnotationTypeMapping
    this.mapping = mapping;
    // 類加載器
    this.classLoader = classLoader;
    // 當前註解的數據源
    this.source = source;
    // AnnotationTypeMapping對應的root註解
    this.rootAttributes = rootAttributes;
    // 通過屬性方法對象獲得屬性值的方法,一般是ReflectionUtils::invokeMethod
    this.valueExtractor = valueExtractor;
    // 該註解對應的聚合索引
    this.aggregateIndex = aggregateIndex;
    // 是否使用映射過的屬性值
    this.useMergedValues = true;
    // 屬性過濾器
    this.attributeFilter = null;
    // 通過MirrorSets解析得到的確認屬性
    this.resolvedRootMirrors = (resolvedRootMirrors != null ? resolvedRootMirrors :
                                mapping.getRoot().getMirrorSets().resolve(source, rootAttributes, this.valueExtractor));
    this.resolvedMirrors = (getDistance() == 0 ? this.resolvedRootMirrors :
                            mapping.getMirrorSets().resolve(source, this, this::getValueForMirrorResolution));
}

可以看得出,TypeMappedAnnotation 基本可以認爲是 AnnotationTypeMapping 的包裝類,它以一個 AnnotationTypeMapping 實例作爲數據源,從而提供一些關於映射後的屬性的相關功能。

它本身並沒有很多特殊的邏輯,我們僅需要關注通過它合成註解的代理對象,以及後續調用代理對象時,是如何從映射過的屬性獲取值的。

2、合併註解的合成

回到 AnnotatedElementUtils.findMergedAnnotations,我們可以看到,在通過 MergedAnnotations 獲得了一個 MergedAnnotation 對象——實際上是 TypeMappedAnnotation 對象——之後,又調用了 MergedAnnotation.synthesize 方法,將 MergedAnnotation 轉成了一個調用方指定類型的註解對象。

該方法先調用了 AbstractMergedAnnotationsynthesize 方法:

public Optional<A> synthesize(Predicate<? super MergedAnnotation<A>> condition)
    throws NoSuchElementException {

    return (condition.test(this) ? Optional.of(synthesize()) : Optional.empty());
}

隨後再調用了實現類 TypeMappedAnnotationsynthesize 方法:

public A synthesize() {
    if (!isPresent()) {
        throw new NoSuchElementException("Unable to synthesize missing annotation");
    }
    // 如果已經合成過就直接返回緩存的實例
    A synthesized = this.synthesizedAnnotation;
    if (synthesized == null) {
        // 合成註解
        synthesized = createSynthesized();
        this.synthesizedAnnotation = synthesized;
    }
    return synthesized;
}

繼續點開 createSynthesized

protected A createSynthesized() {
    // 滿足下述條件時,直接返回根註解:
    // 1.若當前註解的屬性與根註解相同
    // 2.且該合成註解還沒有被合成過
    // 3.該註解存在映射後的屬性
    if (getType().isInstance(this.rootAttributes) && !isSynthesizable()) {
        return (A) this.rootAttributes;
    }
    // 使用動態代理創建一個代理註解
    return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getType());
}

private boolean isSynthesizable() {
    // Already synthesized?
    if (this.rootAttributes instanceof SynthesizedAnnotation) {
        return false;
    }
    return this.mapping.isSynthesizable();
}

SynthesizedMergedAnnotationInvocationHandler 是一個用於 JDK 動態代理的 InvocationHandler,我們不需要完全站看,僅需看看它的構造函數與 InvocationHandler.invoke 就能明白它的運作機制了:

final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> implements InvocationHandler {

    // 合併註解實例
    private final MergedAnnotation<?> annotation;

    // 代理註解的類型
    private final Class<A> type;

    // 代理註解的註解方法
    private final AttributeMethods attributes;

    // 註解屬性的值緩存
    private final Map<String, Object> valueCache = new ConcurrentHashMap<>(8);

    // hashcode
    @Nullable
    private volatile Integer hashCode;

    // toString的值
    @Nullable
    private volatile String string;


    private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation<A> annotation, Class<A> type) {
        Assert.notNull(annotation, "MergedAnnotation must not be null");
        Assert.notNull(type, "Type must not be null");
        Assert.isTrue(type.isAnnotation(), "Type must be an annotation");
        this.annotation = annotation;
        this.type = type;
        this.attributes = AttributeMethods.forAnnotationType(type);
    }
}

至此,合併註解的合成機制已經很明確了:

  • 先通過 AnnotationTypeMapping 創建 TypeMappedAnnotation
  • 通過 TypeMappedAnnotation.synthesize 創建一個對應的類型的註解對象;
  • 如果該 TypeMappedAnnotation 無需合成,則直接返回對應的原始註解對象,否則返回基於該合併註解生成的JDK動態代理對象;

二、合併註解的取值

承接上文,當我們使用 MergedAnnotation.synthesize 方法時,我們可能會得到兩種對象:

  • 第一種就是原始的註解對象,這個沒什麼好說的;
  • 第二種就是通過 SynthesizedMergedAnnotationInvocationHandler 生成的註解代理對象;

而通過註解代理對象取值時,這些方法會被代理到 SynthesizedMergedAnnotationInvocationHandler 中存放的 MergedAnnotation 對象上,從而讓這個代理對象通過原始註解的屬性,獲得與原始註解不一樣的屬性值。

1、方法代理

當我們調用代理對象的屬性值時,它會在 SynthesizedMergedAnnotationInvocationHandler 中,通過 invoke 代理到對應的方法上:

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
    // 代理 equals 方法
    if (ReflectionUtils.isEqualsMethod(method)) {
        return annotationEquals(args[0]);
    }
    // 代理 hashCode 方法
    if (ReflectionUtils.isHashCodeMethod(method)) {
        return annotationHashCode();
    }
    // 代理 toString 方法
    if (ReflectionUtils.isToStringMethod(method)) {
        return annotationToString();
    }
    // 代理 isAnnotationTypeMethod 方法
    if (isAnnotationTypeMethod(method)) {
        return this.type;
    }
    // 代理獲取註解屬性的方法
    if (this.attributes.indexOf(method.getName()) != -1) {
        return getAttributeValue(method);
    }
    throw new AnnotationConfigurationException(String.format(
        "Method [%s] is unsupported for synthesized annotation type [%s]", method, this.type));
}

2、註解屬性的獲取

這裏我們代理對象是如何獲取註解屬性值的:

private Object getAttributeValue(Method method) {
    // 緩存屬性值
    Object value = this.valueCache.computeIfAbsent(method.getName(), attributeName -> {
        // 獲取代理方法的返回值類型
        Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
        // 從 MergedAnnotation 獲取對應屬性值
        return this.annotation.getValue(attributeName, type).orElseThrow(
            () -> new NoSuchElementException("No value found for attribute named '" + attributeName +
                                             "' in merged annotation " + this.annotation.getType().getName()));
    });

    // Clone non-empty arrays so that users cannot alter the contents of values in our cache.
    if (value.getClass().isArray() && Array.getLength(value) > 0) {
        value = cloneArray(value);
    }

    return value;
}

這裏的 MergedAnnotation.getValue 最終在經過多次跳轉後,調到 TypeMappedAnnotation.getAttributeValue 上:

protected <T> T getAttributeValue(String attributeName, Class<T> type) {
    // 獲取方法在AttributeMethods中的下標
    int attributeIndex = getAttributeIndex(attributeName, false);
    // 若方法存在,則通過下標調用
    return (attributeIndex != -1 ? getValue(attributeIndex, type) : null);
}

private <T> T getValue(int attributeIndex, Class<T> type) {
    // 獲取屬性對應的方法
    Method attribute = this.mapping.getAttributes().get(attributeIndex);
    // 獲取屬性值,若有必要則進行屬性映射
    Object value = getValue(attributeIndex, true, false);
    if (value == null) {
        // 如果沒有值則嘗試獲得默認值
        value = attribute.getDefaultValue();
    }
    // 進行類型適配
    return adapt(attribute, value, type);
}

而這邊的 getValue 方法就是真正要獲取屬性值的地方。

private Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) {
    AnnotationTypeMapping mapping = this.mapping;
    // 是否要獲取合併後的屬性
    if (this.useMergedValues) {
        // 如果有必要,屬性是否會被來自根註解的屬性覆蓋
        int mappedIndex = this.mapping.getAliasMapping(attributeIndex);
        // 如果不會被來自根註解的屬性覆蓋,並且允許使用子註解屬性覆蓋該屬性
        if (mappedIndex == -1 && useConventionMapping) {
            mappedIndex = this.mapping.getConventionMapping(attributeIndex);
        }
        // 如果上述兩情況只要有任意一點符合,則令attributeIndex變爲根/子註解中覆蓋屬性的下標
        if (mappedIndex != -1) {
            mapping = mapping.getRoot();
            attributeIndex = mappedIndex;
        }
    }
    // 若有必要,且當前屬性下標對應的屬性在註解內還存在別名屬性,則通過MirrorSets獲得唯一確定的屬性的下標
    if (!forMirrorResolution) {
        attributeIndex =
            (mapping.getDistance() != 0 ? this.resolvedMirrors : this.resolvedRootMirrors)[attributeIndex];
    }
    if (attributeIndex == -1) {
        return null;
    }
    // 如果自己就是根節點,則從自己身上獲取
    if (mapping.getDistance() == 0) {
        Method attribute = mapping.getAttributes().get(attributeIndex);
        Object result = this.valueExtractor.extract(attribute, this.rootAttributes);
        return (result != null ? result : attribute.getDefaultValue());
    }
    // 如果自己不是根節點,則從元註解上獲取
    return getValueFromMetaAnnotation(attributeIndex, forMirrorResolution);
}

@Nullable
private Object getValueFromMetaAnnotation(int attributeIndex, boolean forMirrorResolution) {
    Object value = null;
    if (this.useMergedValues || forMirrorResolution) {
        value = this.mapping.getMappedAnnotationValue(attributeIndex, forMirrorResolution);
    }
    if (value == null) {
        Method attribute = this.mapping.getAttributes().get(attributeIndex);
        value = ReflectionUtils.invokeMethod(attribute, this.mapping.getAnnotation());
    }
    return value;
}

Object getMappedAnnotationValue(int attributeIndex, boolean metaAnnotationsOnly) {
    int mappedIndex = this.annotationValueMappings[attributeIndex];
    if (mappedIndex == -1) {
        return null;
    }
    AnnotationTypeMapping source = this.annotationValueSource[attributeIndex];
    if (source == this && metaAnnotationsOnly) {
        return null;
    }
    return ReflectionUtils.invokeMethod(source.attributes.get(mappedIndex), source.annotation);
}

這一步有點複雜,主要是根據不同的情況,通過 AnnotationTypeMapping 中的幾個屬性映射數組,包括 aliasMappingsconventionMappingsannotationValueMappingsannotationValueSource 來確定最終用於取值的 AnnotationTypeMapping 對象與調用的方法在 AttributeMethods 中的下標:

  • 如果要合併屬性值,則:
    1. 若該屬性被 root 中的同名屬性覆蓋,即 aliasMappings 數組對應下標不爲 -1,則記錄該 root 屬性下標;
    2. 若該屬性不被 root 中同名屬性覆蓋,則確定是否被子註解中的同名屬性覆蓋,即 conventionMappings 數組對應下標不爲 -1,則記錄該覆蓋屬性下標;
  • 若允許別名,則在上述基礎上,繼續進行處理:
    1. 若當前註解已經是根註解,則從 resolvedRootMirrors 上,獲得該屬性在同一註解下關聯的別名屬性中,唯一確定的有效屬性的下標;
    2. 若當前註解不是根註解,則從 resolvedRootMirrors 上,獲得該屬性在同一註解下關聯的別名屬性中,唯一確定的有效屬性的下標;
  • 若當前註解是根註解,則使用從根註解對象 rootAttributes 上根據屬性下標獲取對應方法,然後通過反射調用獲取屬性值;
  • 若當前註解不是根註解,則:
    1. 若不支持屬性覆蓋以及別名,則直接和獲取該註解屬性下標對應方法,並通過反射調用獲取屬性值;
    2. 若支持屬性覆蓋以及別名,則通過屬性下標從 annotationValueSource 找到對應的註解對象,再通過 annotationValueMappings 找到要在該註解中調用的屬性下標,然後在通過屬性下標找到對應的屬性方法後,通過反射調用獲取屬性值;

至此,獲取屬性值的方法流程也走完了。

總結

在這一章,我們瞭解了當通過 MergedAnnotations 獲得註解並解析得到 AnnotationTypeMapping 後,AnnotationTypeMapping 是如何再轉爲我們所需的 MergedAnnotation ,以及在此之後,MergedAnnotation 又是如何生成我們最終所需要的代理註解的。

簡而言之,當解析註解的元註解獲得所需的 AnnotationTypeMapping 後,MergedAnnotation 會判斷 AnnotationTypeMapping 是否發生過屬性映射,如果沒有則返回該映射對象對應的原始註解,否則就通過 SynthesizedMergedAnnotationInvocationHandler 生成一個對應類型的 JDK 動態代理對象。

當我們通過代理對象去調用註解的方法,獲取註解的屬性的時候,SynthesizedMergedAnnotationInvocationHandler 會把方法代理到對應的內部方法中,而獲取屬性時,還會通過 MergedAnnotation.getValue ,最終繞到 AnnotationTypeMapping 中獲取被映射後的屬性值。

題外話

至此,關於 Spring 註解機制的三篇文章已經全部寫完了,在瞭解了它是實現原理的同時,筆者也深刻的認識到了其功能的強大,尤其是 MergeAnnotation 真切的彌補了 java 註解不支持繼承所帶來的的一些痛點,這點筆者在自己的項目嘗試了全面擁抱 spring 的增強註解後深有感觸。

出於鞏固知識,也出於爲想要在非 Spring 環境下享受這個功能的同學考慮,作者決定搞一套類似的開源項目,目前也已經有了一些成果:

  • 筆者嘗試爲常用的開源工具類庫 hutool 提了一個 PR ,也有幸被作者大佬採納了,有需要的同學可以在 Hutool-5.8.5 及以上版本使用該功能;
  • 由於考慮有些項目並沒有引入 Hutool,以及跟作者溝通後覺得這個特性並不適合搬到即將發佈的 Hutool-6.x ,因此筆者單獨將作爲一個註解工具類庫開源了,有興趣的同學可以瞭解一下 powerful-annotation
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章