品Spring:能工巧匠們對注解的“加持”


問題的描述與方案的提出


在Spring從XML轉向注解時,為了自身的開發方便,對注解含義進行了擴充( 具體參考本號上一篇文章 )。

這個擴充直接導致了一個問題,就是需要從注解往元注解以及元元注解(即沿着 從下向上 的方向)里傳遞數據。

為了更好的描述這個問題,請再看個示例:

@interface A {

    String a() default "";
}

@A
@interface B {

    String a() default "";

    String b() default "";
}

@B
@interface C {

    String a() default "";

    String b() default "";

    String c() default "";
}

@C(a = "a", b = "b", c = "c")
class D {

}

這里共有三個注解@A、@B、@C。@A的級別最高,@C的級別最低,數據傳遞是從@C向@A,所以是從下向上。

溫馨提示 ,請務必明白注解級別的高低,什么是高級別注解,什么是低級別注解,本文會一直這樣稱呼。

我們最終設置的是注解@C的a、b、c三個屬性。同時急切期望的是注解@C能把a、b屬性傳遞給注解@B,注解@B能把a屬性傳遞給注解@A。

這個期望對Spring來說非常重要,可惜了,Java注解在這方面是空白的。不過Spring早已司空見慣,不必大驚小怪,沒有路就自己開一條。

首先要解決的就是一個映射的問題,總得知道哪個注解的哪個屬性映射到哪個注解的哪個屬性吧,其實就是需要再額外附加上一個映射信息。

Spring給出的方案很簡單,就是用一個注解 @AliasFor ,把它標注在需要傳遞數據的屬性上,並指明傳遞給誰(哪個注解的哪個屬性)。

再次呼應一下上一篇文章的主題,要添加的映射信息是額外附加信息,這正是注解的功能所在呀,所以現在就是用注解去解決注解的問題。哈哈。

這樣一來的話,上面的示例就變為:

@interface A {

    String a() default "";
}

@A
@interface B {

    @AliasFor(annotation = A.class, attribute = "a")
    String a() default "";

    String b() default "";
}

@B
@interface C {

    @AliasFor(annotation = B.class, attribute = "a")
    String a() default "";

    @AliasFor(annotation = B.class, attribute = "b")
    String b() default "";

    String c() default "";
}

通過簡單的標注,就已經建立起了映射關系,可以說已經對上號了,至於代碼如何實現則另當別論啦。

@AliasFor也有一些簡潔用法:

1) 如果映射的屬性名稱一樣,則可以不指定屬性名,即attribute不用設置。

2) 如果映射的是同一個注解里的屬性名,則可以不指定注解,即annotation不用設置。

3) 這個映射是可以跳級的,可以從注解跳過元注解直接映射到元元注解。

下面請再看下@AliasFor的源碼:

public @interface AliasFor {

    @AliasFor("attribute")
    String value() default "";

    @AliasFor("value")
    String attribute() default "";

    Class<? extends Annotation> annotation() default Annotation.class;
}



@AliasFor注解的三種用法


一) 注解內部的顯式別名

看下面這個注解:

@interface A {

    @AliasFor("y")
    String x() default "";

    @AliasFor("x")
    String y() default "";
}

在一個注解里面, 把一對屬性相互指定為對方的別名

注意,只能是一對,即兩個,不能是三個及以上。

至於為什么會有這種用法,我給出一個猜測吧。

Java注解規定:

當只需設置注解的一個屬性時,且屬性名稱是“value”,設置時可以省略屬性名稱。

看個示例:

@interface E {

    String value(default "";
}

在使用這個注解時有兩種寫法,@E(value = "abc") 和 @E("abc"),它倆是一回事,但后者要簡潔很多。

這里有個問題,就是屬性名必須是“value”才行,但value這個名字很中性,不能很好的表達意圖。

所以很多時候,都會定義更有意義的屬性名,比如用“name”表示名稱要比value好得多,如下:

@interface E {

    String value() default "";

    String name() default "";
}

但是又想使用剛才那種簡潔的用法,咋辦呢?讓它倆互為別名即可,如下:

@interface E {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

現在value和name已經互為對方的別名,所以@E("abc")、@E(name = "abc")都是一樣的,顯然前者更好看。

不知你是否注意到@AliasFor注解的源碼本身就已經使用了這種方法。

二) 元注解中屬性顯式別名

看個示例:

@E
@interface F {

    @AliasFor(annotation = E.class, attribute = "name")
    String id() default "";
}

這樣就把注解@F的id屬性映射為元注解@E的name屬性了。

三) 注解內部的隱式別名

看個示例:

@F
@interface G {

    @AliasFor(annotation = F.class, attribute = "id")
    String a() default "";

    @AliasFor(annotation = F.class, attribute = "id")
    String b() default "";

    @AliasFor(annotation = E.class, attribute = "name")
    String c() default "";
}

屬性a和b都映射為注解@F的id屬性,所以它倆指向一處,因此互為別名。

注解@F的id屬性又映射為注解@E的name屬性,這恰好又和屬性c的映射一樣。

因此屬性a、b和c三者互相互為別名。因為它們雖然殊途但是同歸。

由此也可以看出,映射是具有傳遞性的。X -> Y -> Z,可以推導出X -> Z。

這個映射的建立是有前提條件的:

1)屬性的返回類型必須是同一個

2)屬性必須要有默認值

3)屬性的默認值必須是同一個

在使用時也是有限制的:

1)互為別名的屬性只需設置其中任何一個即可

2)如果設置了其中多個,設置的值必須一樣。

這些其實很好理解,就像不管是大名、小名、筆名或曾用名,最后指向的必須是同一個人才行。

這就是Spring給出的方案,既簡單又直白,所見即所得啊,個人覺得這是上等的方案,也是鄙人的追求。

終於還是要走到該思索如何實現這一步,就像不管什么樣的媳婦兒最終都要見公婆。那就勇敢面對吧。

不過可以預見的是,實現起來應該比較麻煩。因為不可能既有魔鬼身材又有天使面孔。

好事不會都讓一個人占完,上帝對誰都是公平的。

長得帥的學習差,學習好的長的丑。哈哈哈哈。


從簡單處入手,先撿軟柿子捏


從Java語言規范中得知,注解就是一個接口(多了個@而已),注解的屬性就是接口方法,而且方法必須是無參的,返回值必須不能為void。

這些方法就相當於“getter”方法,啥意思呢?只讀的唄。明白了吧,就是注解的屬性值在運行時是無法修改的。

因此,我們在向上傳遞屬性值的時候,是不能像普通Java bean那樣,去設置屬性值的。所以只能想別的辦法。

先從最簡單的情況入手,看個示例:

@interface H {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

@H("編程新說")
class K {

}

來讀取下注解的屬性看看,如下:

H h = K.class.getAnnotation(H.class);

h.value(); //"編程新說"

h.name(); //""

value屬性的值就是“編程新說”,因為我們設置的就是它,這毋庸置疑。

name屬性的值是空字符串,因為我們沒有顯式設置它,所以是它的默認值。

這就是Java注解的表現行為,是正常合理的。

Spring想做的大家都知道了,就是標上@AliasFor注解后,name屬性也能返回“編程新說”,即使站在Java注解的角度來看,name屬性並沒有被設置。

這叫什么呢?“異想天開、痴人說夢”?No、No、No,都不是,這叫運行時改變代碼的行為。那這又是什么意思呢?

如果你現在突然想到了,說明還是很聰明的。運行時改變代碼行為,這可是Spring的看家本領呀。

想想聲明式事務管理,不就是在普通方法上加個注解,然后運行時就有了事務的開啟和提交。這是堂堂正正的運行時改變代碼行為。

背后原理再熟悉不過了,就是基於AOP(面向切面編程)實現的,在運行時攔截住它,然后加入特定行為。

這讓我想到了王首富的名言,來個“截胡”。引申一下,還挺符合場景的。

AOP是采用代理實現的,代理的生成有兩種方式,很熟悉的字眼吧,沒錯,就是JDK動態代理和CGLIB動態生成子類。

上面剛剛說過,注解其實是一個接口,正好采用JDK動態代理,在代理類內部,攔截住正在調用的方法,插入處理別名的邏輯即可。

這樣當再調用h.name()時,就也會返回“編程新說”了。

看完之后是不是直拍大腿,哎呀,這么簡單,我怎么就沒想起來呢。哈哈。

不過沒關系,機會有的是,比如這個:

如何找出在同一個注解內部的某個屬性的所有別名?


穩扎穩打之后,便要節節攀升


上面通過代理的方式解決了注解內部別名的問題,可以認為是屬性數據在同一個注解內部的流動。

剩下的就是要考慮,屬性數據沖出注解流向別處了。不過不是隨便流的,是只能單向流動,且只能從下往上,不能回頭。

來一個稍微專業一點的說法,可能是這樣的:

a)一個注解的屬性可以去重寫比它級別高的任何注解的屬性。

b)一個注解的屬性可以被比它級別低的任何注解的屬性重寫。

很容易理解吧,那就再容易一點,看個比喻吧。

假如一群超人在天上飛,每人一把槍,規定只能向上射擊,且想打誰就打誰。

最終就是這樣的:

1)一個超人可以打到比他高的任何一個人。

表示一個注解的屬性數據可以流入比它級別高的任何一個注解里。

2)一個超人可以被比他低的任何一個人打到。

表示一個注解可以被比它級別低的任何一個注解的屬性數據流入。

現在已經從宏觀上清楚了屬性數據的流動規則了,相當於中學物理老師常說的“定性分析”。

每個物理學的好的人都清楚,定性分析完之后,就該“定量計算”了。那就來吧,come on。

一旦涉及到細節,就必須改用精確、穩定、可靠的模型了。超人亂飛這種肯定是不行的,嘻嘻。

在城市里最穩定的東西就是高樓大廈了,就以樓房的不同樓層為模型,因為這和注解的情況如出一轍。

從最上層的注解到最底層的注解,之間可以有很多級,而且在查找一個注解的時候,只能從最底層開始逐級往上,且不能跳級。

樓房也有很多層,(正常情況下)我們也只能從第一層開始,然后逐層往上,無論電梯還是步梯,也都是不會跳過某層的。

下面就開始把它倆結合起來,假如注解共有10級,樓房也至少有10層。想象一下,就把所有級別的注解逐個逐個放到對應的樓層上。

假如我現在想要獲取8樓的那個注解的所有屬性,那我得先找到它才行啊,於是只能從1樓開始沿着步梯吭哧吭哧爬到8樓。

注解就在那里等着我,它不離不棄,我且找且珍惜,哈哈。好了,現在我已經找到它了,萬里長征總算走完了第一步。

此時我就把注解的所有屬性值都讀了出來,但是,這是不准確的,因為1到7樓的注解都可以向它提供屬性值,去“覆蓋”原有的。

因為屬性值“覆蓋”的方向只能從下往上進行,所以越靠下的優先級越高,也就是離8樓越遠的越是在最后勝出。

這就好比,長江后浪推前浪,后浪更比前浪浪,最浪的那一浪,就是最后一浪。后來者居上嘛。好理解吧。

這是一個多批次、多優先級按一定的順序依次覆蓋的過程。 生活中到處是這種場景,而且非常自然。

如小弟先上場,大哥再登台,最后大BOSS閃亮登場。再如頒獎,先頒三等獎,再二等,再一等嘛。這大概就是所謂的三六九等吧。

想到了這一層,那解決方法自然就浮現出來了。我此刻在8樓,已經讀完了注解的屬性值,那就下到7樓吧。

找出7樓注解的屬性對8樓注解的屬性的覆蓋關系,然后用7樓的覆蓋8樓的。完事后再下到6樓。

重復同樣的動作,即用6樓的覆蓋8樓的。然后依次下到5樓,4樓,3樓,2樓,1樓。

最后用1樓的覆蓋完之后,就全部結束了。此時的屬性值就是准確無誤的了。

整體來看就是,先依次進入,再原路返回,在返回時的每個節點上,執行覆蓋動作。當最后退出時,處理完畢。

擦,這不就是個遞歸嘛,沒錯,就是個遞歸。

這里也有一個問題,可以先思考下:

在跨越不同級別注解時,如何找出一個注解有沒有重寫指定的注解,如果有的話如何找出屬性名的映射關系?


硬核,直擊映射關系的實現


先說下“傳遞性”,傳遞性非常強大,網上曾說過,一個人經過約六、七個人的傳遞后就能見到任何地球人。

強大的東西一般都不容易搞定,@AliasFor注解建立的映射關系就具有傳遞性,來看看Spring是如何實現它的。

映射關系就類似於小學生做的連線題,用一條線把相關的兩個東西連起來,因此映射關系的模型抽象是非常簡單的。

只需把雙方都列出來,再定義一個類,把它們封裝到一起即可, Spring也是這么做的:

class AliasDescriptor {

    private Class<? extends Annotation> sourceAnnotationType;

    private String sourceAttributeName;

    private Method sourceAttribute;


    private Class<? extends Annotation> aliasedAnnotationType;

    private String aliasedAttributeName;

    private Method aliasedAttribute;
}

一共六個字段,可以三個三個分成兩組,前面三個是屬性數據輸出方(提供方),后面三個是屬性數據輸入方(接收方)。

每組里的三個屬性分別是: 注解類型 (Class<?>), 屬性名稱 (String), 屬性方法 (Method)。

注解類型和屬性名稱是建立映射必不可少的元素,那屬性方法又是干啥用的?

前文已經說過,注解的屬性就是一個方法(Method),把它也加進來有三方面作用:

1)校驗,參與映射的屬性返回類型和默認值必須一樣,通過方法可以獲得返回類型和默認值

method.getReturnType();

method.getDefaultValue();

2)比較,比較兩個方法相等和比較兩個注解類型和屬性名稱分別同時相等是一樣的,但前者更簡潔

method1.equals(method2);

annotation1.equals(annotation2) && name1.equals(name2);

3)構建,因為模型類就是基於方法來生成實例的,方法名稱就是屬性名稱,方法所在的類就是注解類型

method.getName();

method.getDeclaringClass();

先看個注解內部的示例:

@interface M {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

這其實是注解內部的兩組映射關系:

一個是從屬性value到屬性name的映射

一個是從屬性name到屬性value的映射

就以value到name的映射,看下映射模型里字段的數據值,如下:

Class<? extends Annotation> sourceAnnotationType = M.class;

String sourceAttributeName = "value";

Method sourceAttribute = M.class.getDeclaredMethod("value");


Class<? extends Annotation> aliasedAnnotationType = M.class;

String aliasedAttributeName = "name";

Method aliasedAttribute = M.class.getDeclaredMethod("name");

由於是注解內部的,所以注解類型都是M.class,源屬性就是value,別名屬性就是name。

再看個注解間的示例:

@M
@interface N {

    @AliasFor(annotation = M.class, attribute = "name")
    String id() default "";
}

這只有一組映射關系:

從注解@N的id屬性到注解@M的name屬性

看下映射模型里字段的數據值,如下:

Class<? extends Annotation> sourceAnnotationType = N.class;

String sourceAttributeName = "id";

Method sourceAttribute = M.class.getDeclaredMethod("id");


Class<? extends Annotation> aliasedAnnotationType = M.class;

String aliasedAttributeName = "name";

Method aliasedAttribute = M.class.getDeclaredMethod("name");

源注解是@N,源屬性是id,別名注解是@M,別名屬性是name。

這就是Spring抽象出來的映射關系的模型,同樣非常簡單直白。

下面就基於這個模型,把文章中預留的問題分析一下,看如何解決。

一)注解內部的顯式別名

這種其實相當於 自己到自己的映射 ,即X -> X。所以模型中兩組數據值中的注解類型必然是相同的。

因此,只需做如下判斷即可:

sourceAnnotationType == aliasedAnnotationType

如果這個返回true,就說明該屬性是映射到了內部的另一個屬性了。

所以sourceAttributeName就是屬性名,aliasedAttributeName就是映射到的別名。

二)注解內部的隱式別名

隱式別名是由跨注解映射造成的 ,由於注解的級別可以是好幾級,所以跨注解映射可以出現類似的級聯映射。

先看幾組“稍微復雜”點的映射示例吧:

X -> A -> B -> C -> D

Y -> E -> F -> G -> C

Z -> H -> I -> J -> G

這種映射是一級接着一級的,看起來有點像是級聯。

能看出來X、Y、Z是分別互為別名嗎?看不出來的話,我們來把這個映射關系補充完善一下。

X -> A -> B -> C -> D

Y -> E -> F -> G -> C -> D

Z -> H -> I -> J -> G -> C -> D

可以看到X、Y、Z通過若干次映射后都可以到達D,所以它們互為別名。

由於這種情況是隱含的,或者說是推導出來的,所以稱為隱式別名。

如果此時把屬性X的值設為“李大胖”,通過生成代理后,讀取屬性Y和Z的值也都是“李大胖”。

那么通過代碼應該如何實現呢?其實上面已經把數據結構展示出來了。

每一組級聯的映射,其實就是一個單向鏈表,從頭節點開始一直逐級映射直到尾節點結束。

三組映射關系,就是三個單向鏈表了。

兩個屬性成為隱式別名的前提是它們最終能夠指向同一個注解的同一個屬性。

站在數據結構的角度看,就是兩條單向鏈表在某一點進行了匯合。

就相當於兩條河分別往前流着流着,在某一點交匯在了一起,自此成為一條河了。

就像一首詩描述的那樣,“百川東到海,何時復西歸”,怕是永無西歸之時,因為是單向流動的。

它的下一句大家也都比較熟悉了,“少壯不努力,老大徒傷悲”。各位碼農趁着年輕趕緊努力吧。

所以最終就演變為求兩條單向鏈表有沒有交點的問題了。這其實就是Spring采用的實現方案。

具體代碼實現就很簡單了,兩層(for/while)循環就搞定了。

三)判斷注解間是否存在重寫

還以上一小節示例來說,我從8樓來到6樓,我必須要能判斷出6樓的注解屬性到底有沒有重寫8樓的。

現在這個問題就非常好解決了,只要檢測下8樓的注解是否出現在以6樓注解屬性為頭節點的單向鏈表中即可。

簡直so easy了,如果在的話,根據映射模型自然可以獲取到被重寫的屬性名。

其實Spring的源碼還是寫的比較復雜的,而且真的不是隨便看幾眼就能看懂的,不看個幾天根本搞不定。

我盡最大的努力,以最簡單而又直擊要害的方式講述出來,這正是作者及本號的追求,即“深入淺出”。

通過本文還應該明白,無論是做模型設計還是代碼實現,如果能在生活中找到映射或類比,將會變得比較容易。

最后,祝看到本文的人都有所收獲。


(END)


品Spring系列文章列表:

品Spring:帝國的基石

品Spring:bean定義上梁山

品Spring:實現bean定義時采用的“先進生產力”

品Spring:注解終於“成功上位”



作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!

 

       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM