由AnnotatedElementUtils延伸的一些所思所想

這篇博客的兩個主題:java

  • spring的AnnotatedElementUtils
  • 我的源碼閱讀方法論分享

爲何要分享AnnotatedElementUtils這個類呢,這個類看起來就是一個工具類,聽起來很像apache的StringUtils,CollectionUtils。mysql

緣由是,它包含着spring對java註解的另類理解,和運用。程序員

java的是怎樣支撐註解的?

Class<TestAnnotation> clazz = TestAnnotation.class;

// 獲取類註解
MyClassAnnotation myClassAnnotation = clazz.getAnnotation(MyClassAnnotation.class);

// 得到構造方法註解
Constructor<TestAnnotation> cons = clazz.getConstructor(new Class[] {});
MyConstructorAnnotation Constructor = cons.getAnnotation(MyConstructorAnnotation.class);

// 得到方法註解
Method method = clazz.getMethod("setId", new Class[] { String.class });
MyMethodAnnotation myMethodAnnotation = method.getAnnotation(MyMethodAnnotation.class);

// 得到字段註解
Field field = clazz.getDeclaredField("id");
MyFieldAnnotation myFieldAnnotation = field.getAnnotation(MyFieldAnnotation.class);

以及@Inherited,它能夠將父類的註解,帶到繼承體系上的子類中去。正則表達式

這套註解體系有什麼問題?

面嚮對象語言之因此被冠以「面向對象」這樣的名字,是由於它具備多態的能力。有了多態的能力,咱們纔有了面向接口編程的能力,有了這個能力,依賴反轉纔有立足點;全部的設計模式纔有立足點(工廠模式,裝飾器模式,策略模式...)。能夠說多態是java這樣的強類型,面嚮對象語言的靈魂。spring

那麼多態這種能力是怎麼來的?sql

父類與接口。弱類型的語言其實自然就支持多態,但強類型的語言則不是。而java在語言層面支持了"父類與接口",體如今java程序能夠自動的向上轉型,而且能夠安全的向下轉型。向上,向下轉型這兩件事,就實現了所謂的「多態」語義。apache

咱們再向問題的本質進一步,看看java是怎麼實現上下轉型的?編程

當把class文件加載進內存(方法區)時,方法在真正運行以前就有一個肯定的調用版本,且該版本在運行期不可變的一類,將會被解析,符號引用將被替換爲實在的內存地址,成爲該方法的入口地址。靜態方法,私有方法,構造器,父類方法符合這個要求。這類方法也被稱爲非虛方法。設計模式

public class Test {
    public void test() {
        // 實例和方法都是肯定的(Human的靜態方法run)
        Human.run();
    }
}

而虛方法和靜態分派則是:安全

// 實例不肯定,方法也不肯定。
// 此處惟一能肯定的是,方法的重載版本。可見這個方法的版本是無參數的,它肯定了執行器在調用run時,
// 必定不會去調用一個帶任何參數的版本的run方法。這就是靜態分派。
public class Test {
    public void test(Human human) {
        human.run();
    }
}

上面提到了重載,而靜態分派就是用以肯定重載版本的,下面我要說的是覆寫。覆寫會致使不一樣實例的覆寫版本,方法體不同,因此虛擬機只能在運行期經過對象的實際類型來決定調用哪一個版本的覆寫方法。這被稱爲動態分派。

public class Test {
    public void test() {
    	System.out.println("i'm Test");
    }
}

public class SubTest extends Test {
    public void test() {
    	System.out.println("i'm SubTest");
    }
}

public class TestTest {
    public static void main(String[] args) {
    	Test t = new Test();
        t.test();
        
        t = new SubTest();
        t.test();
    }
}

發現沒有,覆寫是多態的原理!動態分派是覆寫的原理!那麼,動態分派也就是多態的原理,進而,動態分派也就是java是面嚮對象語言的根本原理,或者說根本緣由!

而目前,java的註解,並不支持動態分派,就是說它並不支持覆寫!這就是目前java這套註解體系的一個重要的問題,它使得註解不易使用。

舉例:

  • java的註解之間沒有繼承關係。注意@Inherited表達的不是註解間有繼承關係,而是子類能夠得到父類的註解。這致使註解的語義不能傳遞,相似於Man屬於Human這樣的邏輯它沒法表達。
  • Java的註解之間沒有多態關係,你就是你,我就是我。這致使你可能要將某些類似的處理邏輯放到多個不一樣的annotation processor裏。例:@Component和@Service都有註冊bean的能力,則這個能力將在這兩個註解的處理器中分別實現。

在深刻探究Spring解決方案前,還有一個問題有待解決

在闡述AnnotatedElementUtils前,我要引出今天此次分享的第二個主題:源碼閱讀方法論。

我問過好多朋友,也在各社區搜索過,如何閱讀開源代碼這件事情。獲得的答案每每是一些「放之四海而皆準」的指導性建議,始終沒有獲得一個切實可行的方法論,後來我本身總結,摸索了一套。

首先問一個問題:當咱們說「讀源碼」時,咱們到底是要作一件什麼事情?

之前我對這個問題的回答是:讀懂它的邏輯,或叫流程。這個答案背後的含義是,我在意的是代碼中的判斷,分支。可是我常常在讀源碼時有很強的挫敗感,由於我很努力的去讀,卻發現,我讀懂了一個方法,而這個類有好幾十個方法,我對這個類仍是不理解,方法和方法間的關係仍是不明朗,類的抽象仍是很模糊。

也就是在這個階段,我請教過不少朋友,以及論壇,甚至每當遇到新的程序員朋友時,我都會問對方這個問題——怎麼閱讀源碼。

直到後來,我看到《人月神話》中有這樣一句話:讓我看你的流程圖不讓我看錶,我會仍然搞不明白。給我看你的表,通常我就不用看你的流程圖了,表能讓人一目瞭然。

這裏的表指的是數據,以及數據的結構,例如一個類的成員變量就是它的表;咱們寫業務的時候,mysql中的數據就是表。《人月神話》的這句話讓我忽然一驚,難道我一直以來在理解代碼的時候,所關注的點是錯的,我不該該關注邏輯,而應該關注表?

驗證這個道理的最好辦法,就是運用它,實驗它!

在這裏我能夠告訴你們,它是對的!個人方法論就是創建在它之上的。

源碼閱讀方法論——原則

  • 以類爲最小理解單位(指的是聚合類)

    當你要讀源碼時,將一個類看做一個總體去理解,這個類有些什麼方法,其實並非很重要,重要的是,這個類是個什麼東西,或者說抽象是什麼(這裏僅指聚合類,聚合類指的是它的方法和表是爲同一件事情而存在的。舉個例子,apache的StringUtils是一個非聚合類,它的方法之間沒有必然聯繫,是各自爲正的;而spring的AnnotationTypeMapping則是一個聚合類,它的表和方法都圍繞着某個註解而工做,這個類後面會重點介紹)。

  • 以表爲支點

    理解一個類,就是去理解這個類的表(而不是它的業務方法)!理解表有多種途徑:經過註釋,經過表的設值函數,經過表的使用函數,經過其餘文章等等。在理解表的這些途徑中,表的設值函數一般來講是能提供最多信息的地方,因此類的構造函數和設值方法是咱們首先應該關注的東西。

源碼閱讀方法論——技巧

  • 打錨點,協助思惟跳躍

    在讀源碼的時候,常常遇到你要跨越不少次方法調用的狀況,人腦的棧是比較小的,因此我經過打錨點的方式,來協助大腦記憶調用棧。

    打錨點是經過在關鍵代碼上標註 todo 註釋實現的。例以下圖中的「// todo wanxm 1.15.1」。配合這個正則表達式:

    todo wanxm (1.15)\.?\d+.?( |$)

    這裏至關於列出了一條1.15.x的鏈,x能夠是增量的,表示着某種你想要的前後順序(好比方法調用順序,邏輯點順序等)。前綴1.15也是可變的,例如你在圖中看到的這條1.15.x的鏈,實際上是我在讀一條1.x的鏈,讀到1.15這個點上時,我發現它後面有挺多內容,因而我在1.15這個點上使用1.15.0開了一條嵌套鏈。當我使用Idea的Ctrl + Shift + F 搜索時,使用「todo wanxm (1).?\d+.?( |$)」我就能看到那條1.x的鏈,使用「todo wanxm (1.15).?\d+.?( |$)」時,就能看到那條1.15.x的鏈。

    在這裏插入圖片描述

  • 使用idea的Ctrl + Alt + H,來跟蹤類的初始化鏈。

  • 使用「設」,來簡化描述語言。例如後面的文章我將會展現的一段「設」:

    /**
    * 設AnnotationTypeMapping的某個實例爲M,M所映射的註解爲A。
    * A中有5個屬性(方法):H0,H1,H2,H3,H4。(H後面的數字表示方法的行文索引)
    */

源碼閱讀方法論——步驟

  • 開始以前先定目的

    目的,在咱們讀源碼的過程當中,是很是重要的,其一:若是沒有一個清晰明確的目的,你極可能被程序中紛繁的細節所包圍,抓不住重點,搞不清楚本身要幹什麼,有了明確的目的,可讓你在深陷細節泥潭時跳脫出來,從新尋找支點。其二:要肯定目的則你必需對你所要閱讀的代碼有必定的瞭解,這能促使你在閱讀前,先去作必定的準備工做,從側面先對代碼有一個概念性的,籠統的認識。

  • 構建依賴圖

    依賴圖的構建方法有不少,你能夠是從其餘文章中看來的,也能夠是本身找一個切入點,速讀代碼構建依賴圖。

  • 根據依賴圖,自底向上,逐個理解類以及接口

    理解類的時候,先以類的構造函數(或設值函數)爲主,功能方法爲輔理解類的表;後以表爲支點,理解類。

    有一些類,其表很簡單甚至沒有,而其功能方法決定了類的能力,這種類就以功能方法爲支點來理解,一般這種類,讀懂了功能方法,也就明瞭了

    有時,會由於咱們不理解構造函數的參數的用意而致使咱們沒法有效的經過構造函數去理解表,此時從依賴圖中找到最底層,最接近該類的一處實例化過程去閱讀,以搞清楚構造函數中參數的語義。

帶着方法論,探索Spring

到這裏,今天的主角AnnotatedElementUtils就要登場了,它雖然名字叫作utils,但它可不是一個工具類那麼簡單,它蘊含着spring對註解這種語法的思考。

我是經過閱讀《Spring Boot 編程思想》這本書瞭解到AnnotatedElementUtils的,書中沒有詳細展開介紹它,可是經過書中的描述,我知道了spring對註解的處理,是不一樣於java反射的語義的。咱們就來讀一讀,看看有什麼奧祕。個人閱讀目的就是:spring怎樣讓註解實現屬性覆寫?

先展現一下AnnotatedElementUtils的做用

@TestA
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestRoot {
}

@TestB
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestA {
    @AliasFor(value = "c1", annotation = TestC.class)
    String bb() default "testA";
}

@TestC
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestB {
    @AliasFor(value = "c2", annotation = TestC.class)
    String cc() default "testB";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestC {
    @AliasFor(value = "c2")
    String c1() default "testC";

    @AliasFor(value = "c1")
    String c2() default "testC";
}

在這裏插入圖片描述

從AnnotatedElementUtils開始構建依賴圖

AnnotatedElementUtils這個類沒有表,顯然它只是某些其餘類的代理,既然沒有表,按照咱們的方法論,它就沒有什麼太多可理解的了,咱們讀一讀它的註釋:

AnnotatedElementUtils(類)

  • 查找註釋通用方法。就是從AnnotatedElement上獲取註釋信息。
  • 它和jdk提供的原生檢討不同。
  • 它提供了兩類查找,一類是get語義的查找,一類是find語義的查找
  • get語義的查找可查找到直接定義在AnnotatedElement上的,或繼承來的註解。
  • find則比get要廣
  • get和find都支持@Inherited
  • 在組合註解中的元註解的屬性複寫功能被如下方法(及其重載方法)支持
    • getMergedAnnotationAttributes
    • getMergedAnnotation
    • getAllMergedAnnotations
    • getMergedRepeatableAnnotations
    • findMergedAnnotationAttributes
    • findMergedAnnotation
    • findAllMergedAnnotations
    • findMergedRepeatableAnnotations

從註釋裏咱們知道,它有一堆get和find方法,find方法的語義看起來更接近我所提出的問題,因此我選擇了findMergedAnnotationAttributes來做爲切入點。迅速的閱讀一下這個方方法,找出它當中依賴了些什麼。

(下面給出依賴圖)

在這裏插入圖片描述

從底層開始閱讀

在這個依賴圖中,最底層的類是:AttributeMethods,RepeatableContainers的實現類,AnnotationsScanner的實現類,AnnotationFilter,這幾個底層類是比較簡單的,因此今天我不講他們,我講AnnotationTypeMapping,按照正常的順序,你應該是先去讀它們的。

  • 閱讀註釋

    如今讓咱們聚焦到AnnotationTypeMapping這個類上,它的註釋是這樣寫的:

    Provides mapping information for a single annotation (or meta-annotation) in the context of a root annotation type.

    以根註解爲上下文,提供單個註解的映射信息。第一次讀這句話我以爲沒有幾個中國人能理解,好在註釋只是理解的手段之一。

  • 閱讀構造函數

    • source, root,distance

      在這裏插入圖片描述

      構造函數第一句就遇到麻煩了,只看前三行,你們能看出來什麼邏輯嗎?

      不理解不要緊,根據方法論,咱們應該從依賴圖中找到最底層,最接近該類的一處實例化過程去閱讀,若是你嚴格畫出了依賴圖,而且從依賴圖的底部着手去閱讀,那麼當你去尋找最近的實例化過程時,你會發現,它就在隔壁。

      在這裏插入圖片描述

      進入實例化點,利用Idea的功能, 獲得調用層級

      在這裏插入圖片描述

      好了,如今你已經擁有一條實例化鏈了,注意scope選this class,實例化鏈的閱讀要當心,不要被太多細節干擾,咱們的目標僅僅是搞清楚構造函數參數的含義,一旦你在閱讀的過程當中理解了,就能夠馬上停下,回到最初的那個類了,不要在此有多餘的停留。

      經過實例化鏈的閱讀,咱們明白了,AnnotationTypeMapping的實例是由AnnotationTypeMappings建立的,建立的過程是根據註解的註釋體系從下往上進行的,能夠參考「展現AnnotatedElementUtils的做用」那一節的示例(TestRoot -> TestA -> TestB -> TestC,它們將會串起來,後者的source指向前者,而全部的root都指向TestRoot)。這樣前三個成員變量的含義是否是就清晰了。

      在這裏插入圖片描述

    • metaTypes,annotationType,annotation,attributeMethods,aliasMappings,aliasedBy

      在這裏我就不贅述全部成員變量(也就是這個類的表)的閱讀過程了,總之它們都是經過讀AnnotationTypeMapping的構造函數而理解的,下面我直接貼圖,展現了我如何對已經搞清楚的成員變量進行註釋的。

      在這裏插入圖片描述

    • mirrorSets,conventionMappings,annotationValueMappings,annotationValueSource

      這幾個則是依賴MirrorSet這個類的邏輯。MirrorSet是一個內部類,內部類和普通類的一個重要區別就是,當它被實例化的時候,所使用的數據不全來自構造函數的參數,還會來自其外部類的表,因此在閱讀內部類的構造函數時,要先將它所使用的外部類的表理解了。

      我面對內部類的策略是,儘可能推遲閱讀內部類的時間,也就是說,若是不是它阻礙了流程,那麼就先將其擱置。(你掌握越多的外部類信息,則理解內部類時就越少會遇到卡殼的狀況,避免在兩個類之間反覆切換的狀況發生)

      在不理會MirrorSets及其相關邏輯的狀況下,咱們已經疏通了上面那部分數據表的邏輯,根據那些信息,我能夠比較容易的得出MirrorSets以及MirrorSet的表。

      在這裏插入圖片描述

      在這裏插入圖片描述

      這兩內部類就屬於,表很簡單,其功能更多由功能方法決定的類。咱們去讀一讀它的各個功能方法(若是功能方法的參數你沒法理解,有兩種策略,1:使用相似閱讀構造函數的方法;2:先不理他,等之後閱讀其餘代碼時,發現調用到了這個功能方法,那時你帶着相關參數的含義再來讀這個功能方法)(我在這使用了第二種方法)。

      這裏貼出展示這兩個內部類核心能力的代碼註釋:

      MirrorSets的:

      在這裏插入圖片描述

      MirrorSet的:

      在這裏插入圖片描述

    • 讀完MirrorSets的相關邏輯後,整個AnnotationTypeMapping的表的信息就有了。這裏貼出它的所有表信息。

      在這裏插入圖片描述

  • 到這裏,AnnotationTypeMapping這個類其實已經讀的差很少了,總結一下,spring引入了以下層級屬性概念:

    • 別名屬性

      直接經過@AliaseFor關聯起來的屬性

    • 鏡像屬性

      因爲直接或間接的@AliaseFor關係,使得某些屬性實際上必定擁有相同的值,這些屬性被稱爲鏡像屬性。

    • 慣例屬性

      位於註解A中的和Root中同名的屬性,被稱爲慣例屬性,而且,同一註解中的慣例屬性的鏡像屬性也是慣例屬性。如,A中的H0和H1互爲鏡像屬性,Root中的某個方法Hr和A中的H0名字相同,則Hr是Ho的慣例屬性,Hr也是H1的慣例屬性。

    • 最低階屬性

      A的最接近Root的有效屬性。至關於,對A的某個屬性來講,當低階上存在它的鏡像時,就取低階的值,不然取它本身的值。因爲低階具備高優先級,因此我將它稱做「最低階屬性」。

看了AnnotationTypeMapping的表,你的腦殼裏是否已經有了它的概念了呢?

AnnotationTypeMapping提供了三個關鍵功能方法,分別是

  • getAliasMapping

    用以獲取root中的別名屬性行文索引

  • getConventionMapping

    用以獲取root中的慣例屬性的行文索引

  • getMappedAnnotationValue

    獲取最低階屬性的值

這三個方法就造成了spring獲取註解屬性的基礎能力。

回到開篇——spring是如何賦予註解覆寫能力的?

在spring中,註解之間具備多種關係,而且存在層級概念。使用者輸入「別名關係」,spring則將這種關係深化,最終落到「慣例關係」與「最低階關係」上,從而賦予低階註解屬性影響高階註解屬性的能力,實現低階對高階的覆寫,就像子類對父類的覆寫同樣。而且,值得注意的是,spring並無真的去修改高階註解的屬性值,而是經過相似指針的方式,將獲取高階註解屬性值的操做指向它的低階鏡像,從而在外部看來,像是高階屬性被低階屬性覆寫。

這種能力能夠爲咱們帶來什麼優點?

在這裏插入圖片描述

以spring的@Service註解爲例,它被@Component註解元標註,而且其value屬性被標識了是@Component的value屬性的別名。spring在爲咱們提供@Service註解的時候,並不須要專門去寫一個註解處理器來將被@Service標註的類註冊成Bean,spring只須要一個@Component的註解處理器就能夠,由於它能夠從任何被@Service標註的類上獲取到@Component,而且獲取到被覆寫的value值。這是否是很像向上轉型,很像多態?

對於廣大的互聯網開發人員來講,咱們的基礎工做棧之一就是spring,當咱們在spring應用中開發時,何不使用spring已經搭建好的腳手架呢,當咱們須要開發一些註解處理器的時候,徹底可使用spring封裝好的AnnotatedElementUtils。

題外話

你們有沒有注意到MirrorSet的resolve方法有問題?

問題出在:「若是全部屬性都是默認值,則result = -1」(參看前文對MirrorSet的resolve方法的註釋截圖)。

-1表示的是它在某組鏡像屬性中沒有找到有效屬性,若是沒有找到有效屬性,那麼某個高層註解的「最低階屬性」就不可能定位到這組鏡像上來。

舉個例子說明它會致使的問題:

@TestB
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestA {    
    @AliasFor(value = "b1", annotation = TestB.class)    
    String a1() default "testA";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestB {    
    @AliasFor(value = "b2")    
    String b1() default "testB";    
    
    @AliasFor(value = "b1")    
    String b2() default "testB";
}

@TestA
public class Test {    
    public static void main(String[] args) {
        // 這裏你獲得的實例b有兩個key,b1和b2,值都是"testA"
        AnnotationAttributes b = AnnotatedElementUtils.findMergedAnnotationAttributes(Test.class, TestB.class, false, true);    
    }
}

可是當你將TestA修改爲這樣,使得a1和a2成爲鏡像屬性時,獲得的結果就比較奇怪了

@TestB
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestA {    
    @AliasFor(value = "b1", annotation = TestB.class)    
    String a1() default "testA";    
    
    @AliasFor(value = "b2", annotation = TestB.class)    
    String a2() default "testA";
}

@TestA
public class Test {    
    public static void main(String[] args) {
        // 這裏你獲得的實例b有兩個key,b1和b2,值都是"testB"    
        AnnotationAttributes b = AnnotatedElementUtils.findMergedAnnotationAttributes(Test.class, TestB.class, false, true);    
    }
}

註解TestB中的屬性並無被TestA中的屬性覆蓋,但TestA確實是TestB的低層級屬性,它理應具備覆寫上層屬性的能力,當TestA中的屬性沒有造成鏡像時,它確實表現出了這種能力,但當TestA中的屬性造成鏡像時,這種能力消失了(這個bug在spring-framework5.2.x版本下存在,將可能於5.2.3版本修復)。

比較幸運,咱們發現了一個spring的bug。也從側面證實了,咱們的源碼閱讀方法論是有效的。給spring提一個PR,咱們就能收到幾個感謝。

結語

AnnotatedElementUtils的能力其實並非一個AnnotationTypeMapping能夠歸納的,還有其餘一些類在整個邏輯中發揮重要做用,我會繼續更新博客,慢慢將完整的AnnotatedElementUtils展示出來,而面對今天的AnnotationTypeMapping,你在看了表的註釋後,有一個歸納性的認識就能夠了。

但願個人方法能對你們有所幫助,也指望你們和我分享大家的方法,讓咱們取長補短,最後能得出一套高效的方法論。

相關文章
相關標籤/搜索