版權聲明:本文可以自由轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
?
作者:cleverpig(作者的Blog: http://blog.matrix.org.cn/page/cleverpig )
原文: http://www.matrix.org.cn/resource/article/44/44055_Java+Annotation+Reflect.html
關鍵字:java,annotation,reflect
前言:
在上篇文章 《Java Annotation入門》 中概要性的介紹了Annotation的定義、使用,范圍涵蓋較廣,但是深度不夠。所以作者在《Java Annotation入門》后,繼續整理了Annotation的概念和知識點,與喜歡research的朋友們共享。
閱讀提示:文中提到的程序成員或者程序元素是一個概念,指組成程序代碼的單元:如類、方法、成員變量。
一、Annotation究竟是什么?
Annotation提供了一條與程序元素關聯任何信息或者任何元數據(metadata)的途徑。從某些方面看,annotation就像修飾符一樣被使用,并應用于包、類型、構造方法、方法、成員變量、參數、本地變量的聲明中。這些信息被存儲在annotation的“name=value”結構對中。annotation類型是一種接口,能夠通過java反射API的方式提供對其信息的訪問。
annotation能被用來為某個程序元素(類、方法、成員變量等)關聯任何的信息。需要注意的是,這里存在著一個基本的潛規則:annotaion不能影響程序代碼的執行,無論增加、刪除annotation,代碼都始終如一的執行。另外,盡管一些annotation通過java的反射api方法在運行時被訪問,而java語言解釋器在工作時忽略了這些annotation。正是由于java虛擬機忽略了annotation,導致了annotation類型在代碼中是“不起作用”的;只有通過某種配套的工具才會對annotation類型中的信息進行訪問和處理。本文中將涵蓋標準的annotation和meta-annotation類型,陪伴這些annotation類型的工具是java編譯器(當然要以某種特殊的方式處理它們)。
由于上述原因,annotation在使用時十分簡便。一個本地變量可以被一個以NonNull命名的annotation類型所標注,來作為對這個本地變量不能被賦予null值的斷言。而我們可以編寫與之配套的一個annotation代碼分析工具,使用它來對具有前面變量的代碼進行解析,并且嘗試驗證這個斷言。當然這些代碼并不必自己編寫。在JDK安裝后,在JDK/bin目錄中可以找到名為“apt”的工具,它提供了處理annotation的框架:它啟動后掃描源代碼中的annotation,并調用我們定義好的annotation處理器完成我們所要完成的工作(比如驗證前面例子中的斷言)。說到這里,annotation的強大功能似乎可以替代XDoclet這類的工具了,隨著我們的深入,大家會更加堅信這一點。
注:詳細描述請參看jsr250規范:
http://www.jcp.org/aboutJava/communityprocess/pfd/jsr250/
二、Annotation的定義:
這段文字開始介紹annotation相關技術。在此大家將看到java5.0的標準annotation類型,這種標準類型就是前文中所說的“內建”類型,它們可以直接被javac支持。可喜的是,在java6.0beta版中的javac已經加入了對自定義annotation的支持。
1。Annotation的概念和語法:
首先,關鍵的概念是理解annotation是與一個程序元素相關聯信息或者元數據的標注。它從不影響java程序的執行,但是對例如編譯器警告或者像文檔生成器等輔助工具產生影響。
下面是常用的annotation列表,我們應該注意在annotation和annotation類型之間的不同:
A.annotation:
annotation使用了在java5.0所帶來的新語法,它的行為十分類似public、final這樣的修飾符。每個annotation具有一個名字和成員個數>=0。每個annotation的成員具有被稱為name=value對的名字和值(就像javabean一樣),name=value裝載了annotation的信息。
B.annotation類型:
annotation類型定義了annotation的名字、類型、成員默認值。一個annotation類型可以說是一個特殊的java接口,它的成員變量是受限制的,而聲明annotation類型時需要使用新語法。當我們通過java反射api訪問annotation時,返回值將是一個實現了該annotation類型接口的對象,通過訪問這個對象我們能方便的訪問到其annotation成員。后面的章節將提到在java5.0的java.lang包里包含的3個標準annotation類型。
C.annotation成員:
annotation的成員在annotation類型中以無參數的方法的形式被聲明。其方法名和返回值定義了該成員的名字和類型。在此有一個特定的默認語法:允許聲明任何annotation成員的默認值:一個annotation可以將name=value對作為沒有定義默認值的annotation成員的值,當然也可以使用name=value對來覆蓋其它成員默認值。這一點有些近似類的繼承特性,父類的構造函數可以作為子類的默認構造函數,但是也可以被子類覆蓋。
D.marker annotation類型:
一個沒有成員定義的annotation類型被稱為marker annotation。這種annotation類型僅使用自身的存在與否來為我們提供信息。如后面要說的Override。
E.meta-annotation:
meta-annotation也稱為元annotation,它是被用來聲明annotation類型的annotation。Java5.0提供了一些標準的元-annotation類型。下面介紹的target、retention就是meta-annotation。
F.target:
annotation的target是一個被標注的程序元素。target說明了annotation所修飾的對象范圍:annotation可被用于packages、types(類、接口、枚舉、annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。在annotation類型的聲明中使用了target可更加明晰其修飾的目標。
G.retention:
annotation的retention定義了該annotation被保留的時間長短:某些annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀取(請注意并不影響class的執行,因為annotation與class在使用上是被分離的)。使用這個meta-annotation可以對annotation的“生命周期”限制。
H.metadata:
由于metadata被廣泛使用于各種計算機開發過程中,所以當我們在這里談論的metadata即元數據通常指被annotation裝載的信息或者annotation本身。
2。使用標準Annotation:
java5.0在java.lang包中定義了3種標準的annotation類型:
A.Override:
java.lang.Override是一個marker annotation類型,它被用作標注方法。它說明了被標注的方法重載了父類的方法,起到了斷言的作用。如果我們使用了這種annotation在一個沒有覆蓋父類方法的方法時,java編譯器將以一個編譯錯誤來警示。
這個annotaton常常在我們試圖覆蓋父類方法而確又寫錯了方法名時發揮威力。
使用方法極其簡單:在使用此annotation時只要在被修飾的方法前面加上@Override。
下面的代碼是一個使用@Override修飾一個企圖重載父類的toString方法,而又存在拼寫錯誤的sample:
清單1:
B.Deprecated:
同樣Deprecated也是一個marker annotation。當一個類型或者類型成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標注的程序元素。而且這種修飾具有一定的“延續性”:如果我們在代碼中通過繼承或者覆蓋的方式使用了這個過時的類型或者成員,雖然繼承或者覆蓋后的類型或者成員并不是被聲明為@Deprecated,但編譯器仍然要報警。
值得注意,@Deprecated這個annotation類型和javadoc中的@deprecated這個tag是有區別的:前者是java編譯器識別的,而后者是被javadoc工具所識別用來生成文檔(包含程序成員為什么已經過時、它應當如何被禁止或者替代的描述)。
在java5.0,java編譯器仍然象其從前版本那樣尋找@deprecated這個javadoc tag,并使用它們產生警告信息。但是這種狀況將在后續版本中改變,我們應在現在就開始使用@Deprecated來修飾過時的方法而不是@deprecated javadoc tag。
清單2:
C.SuppressWarnings:
@SuppressWarnings被用于有選擇的關閉編譯器對類、方法、成員變量、變量初始化的警告。在java5.0,sun提供的javac編譯器為我們提供了-Xlint選項來使編譯器對合法的程序代碼提出警告,此種警告從某種程度上代表了程序錯誤。例如當我們使用一個generic collection類而又沒有提供它的類型時,編譯器將提示出"unchecked warning"的警告。
通常當這種情況發生時,我們就需要查找引起警告的代碼。如果它真的表示錯誤,我們就需要糾正它。例如如果警告信息表明我們代碼中的switch語句沒有覆蓋所有可能的case,那么我們就應增加一個默認的case來避免這種警告。
相仿,有時我們無法避免這種警告,例如,我們使用必須和非generic的舊代碼交互的generic collection類時,我們不能避免這個unchecked warning。此時@SuppressWarning就要派上用場了,在調用的方法前增加@SuppressWarnings修飾,告訴編譯器停止對此方法的警告。
SuppressWarning不是一個marker annotation。它有一個類型為String[]的成員,這個成員的值為被禁止的警告名。對于javac編譯器來講,被-Xlint選項有效的警告名也同樣對@SuppressWarings有效,同時編譯器忽略掉無法識別的警告名。
annotation語法允許在annotation名后跟括號,括號中是使用逗號分割的name=value對用于為annotation的成員賦值:
清單3:
在這個例子中SuppressWarnings annotation類型只定義了一個單一的成員,所以只有一個簡單的value={...}作為name=value對。又由于成員值是一個數組,故使用大括號來聲明數組值。
注意:我們可以在下面的情況中縮寫annotation:當annotation只有單一成員,并成員命名為"value="。這時可以省去"value="。比如將上面的SuppressWarnings annotation進行縮寫:
清單4:
如果SuppressWarnings所聲明的被禁止警告個數為一個時,可以省去大括號:
3。Annotation語法:
在上一個章節中,我們看到書寫marker annotation和單一成員annotation的語法。下面本人來介紹一下完整的語法:
annotation由“@+annotation類型名稱+(..逗號分割的name-value對...)”組成。其中成員可以按照任何的順序。如果annotation類型定義了某個成員的默認值,則這個成員可以被省略。成員值必須為編譯時常量、內嵌的annotation或者數組。
下面我們將定義一個annotation類型名為Reviews,它有一個由@Review annotation數組構成的成員。這個@Review annotation類型有三個成員:"reviewer"是一個字符串,"comment" 是一個具有默認值的可選的字符串,"grade"是一個Review.Grade枚舉類型值。
清單5:
annotation語法的另一個重要規則是沒有程序成員可以有多于一個的同一annotation實例。例如在一個類中簡單的放置多個@Review annotation。這也是在上面代碼中定義@Reviews annotation類型數組的原因。
4。Annotation成員類型和值:
annotation成員必須是非空的編譯時常量表達式。可用的成員類型為:primitive類型、, String, Class, enumerated類型, annotation類型, 和前面類型的數組。
下面我們定義了一個名為UncheckedExceptions 的annotation類型,它的成員是一個擴展了RuntimeException類的類數組。
清單6:
5。Annotation的目標:
annotation通常被放在類型定義和成員定義的前面。然而它也出現在package、方法參數、本地變量的前面。下面,我們來討論一下這些不大常用的寫法:
package annotation出現在package聲明的前面。
下面的例子package-info.java中不包含任何的公共類型定義,卻包含一個可選的javadoc注釋。
清單7:
當package-info.java文件被編譯時,它將產生名為包含annotation(特殊的接口)聲明的package-info.class的類。這個接口沒有成員,它的名字package-info不是一個合法的java標識,所以它不能用在java源代碼中。這個接口的存在只是簡單的被看作一個為package annotation準備的占位符。
用于修飾方法參數、catch參數、本地變量的annotation只是簡單的出現在這些程序成員的修飾符位置。java類文件格式沒有為本地變量或者catch參數存儲annotation作準備,所以這些annotation總是保留在源代碼級別(source retention);方法參數annotation能夠保存在類文件中,也可以在保留到運行時。
最后,請注意,枚舉類型定義中不允許任何的修飾符修飾其枚舉值。
6。Annotation和默認值:
在Annotation中,沒有默認值的成員必須有一個成員值。而如何理解默認值是如何被處理就是一個很重要的細節:annotation類型所定義的成員默認值被存儲在class文件中,不被編譯到annotation里面。如果我們修改一個annotation類型使其成員的默認值發生了改變,這個改變對于所有此類型的annotation中沒有明確提供成員值的成員產生影響(即修改了該成員的成員值)。即使在annotation類型使其成員的默認值被改變后annotation從沒被重新編譯過,該類型的annotation(改變前已經被編譯的)也受到影響。
三、Annotation工作原理:
Annotation與反射
在java5.0中Java.lang.reflect提供的反射API被擴充了讀取運行時annotation的能力。讓我們回顧一下前面所講的:一個annotation類型被定義為runtime retention后,它才是在運行時可見,當class文件被裝載時被保存在class文件中的annotation才會被虛擬機讀取。那么reflect是如何幫助我們訪問class中的annotation呢?
下文將在java.lang.reflect用于annotation的新特性,其中java.lang.reflect.AnnotatedElement是重要的接口,它代表了提供查詢annotation能力的程序成員。這個接口被java.lang.Package、java.lang.Class實現,并間接地被Method類、Constructor類、java.lang.reflect的Field類實現。而annotation中的方法參數可以通過Method類、Constructor類的getParameterAnnotations()方法獲得。
下面的代碼使用了AnnotatedElement類的isAnnotationPresent()方法判斷某個方法是否具有@Unstable annotation,從而斷言此方法是否穩定:
清單8:
isAnnotationPresent()方法對于檢查marker annotation是十分有用的,因為marker annotation沒有成員變量,所以我們只要知道class的方法是否使用了annotation修飾就可以了。而當處理具有成員的annotation時,我們通過使用getAnnotation()方法來獲得annotation的成員信息(成員名稱、成員值)。這里我們看到了一套優美的java annotation系統:如果annotation存在,那么實現了相應的annotation類型接口的對象將被getAnnotation()方法返回,接著調用定義在annotation類型中的成員方法可以方便地獲得任何成員值。
回想一下,前面介紹的@Reviews annotation,如果這個annotation類型被聲明為runtime retention的話,我們通過下面的代碼來訪問@Reviews annotation的成員值:
清單9:
四、如何自定義Annotation?
1.詳解annotation與接口的異同:
因為annotation類型是一個非凡的接口,所以兩者之間存在著某些差異:
A.Annotation類型使用關鍵字@interface而不是interface。
這個關鍵字聲明隱含了一個信息:它是繼承了java.lang.annotation.Annotation接口,并非聲明了一個interface。
B.Annotation類型、方法定義是獨特的、受限制的。
Annotation類型的方法必須聲明為無參數、無異常拋出的。這些方法定義了annotation的成員:方法名成為了成員名,而方法返回值成為了成員的類型。而方法返回值類型必須為primitive類型、Class類型、枚舉類型、annotation類型或者由前面類型之一作為元素的一維數組。方法的后面可以使用default和一個默認數值來聲明成員的默認值,null不能作為成員默認值,這與我們在非annotation類型中定義方法有很大不同。
Annotation類型和它的方法不能使用annotation類型的參數、成員不能是generic。只有返回值類型是Class的方法可以在annotation類型中使用generic,因為此方法能夠用類轉換將各種類型轉換為Class。
C.Annotation類型又與接口有著近似之處。
它們可以定義常量、靜態成員類型(比如枚舉類型定義)。Annotation類型也可以如接口一般被實現或者繼承。
2.實例:
下面,我們將看到如何定義annotation類型的example。它展示了annotation類型聲明以及@interface與interface之間的不同:
清單10:
下面的另一個example只定義了一個成員。并通過將這個成員命名為value,使我們可以方便的使用這種annotation的快捷聲明方式:
清單11:
以下的example更加復雜。Reviews annotation類型只有一個成員,但是這個成員的類型是復雜的:由Review annotation組成的數組。Review annotation類型有3個成員:枚舉類型成員grade、表示Review名稱的字符串類型成員Reviewer、具有默認值的字符串類型成員Comment。
清單12:
最后,我們來定義一個annotation方法用于羅列出類運行中所有的unchecked異常(上文已經提到這種情況不一定是錯誤)。這個annotation類型將一個數組作為了唯一的成員。數組中的每個元素都是異常類。為了加強對未檢查的異常(此類異常都是在運行時拋出)進行報告,我們可以在代碼中對異常的類型進行限制:
清單13:
五、Meta-Annotation
Annotation類型可以被它們自己所標注。Java5.0定義了4個標準的meta-annotation類型,它們被用來提供對其它annotation類型作說明。這些類型和它們所支持的類在java.lang.annotation包中可以找到。如果需要更詳細的信息可以參考jdk5.0手冊。
1.再談Target
作為meta-annotation類型的Target,它描述了annotation所修飾的程序成員的類型。當一個annotation類型沒有Target時,它將被作為普通的annotation看待。當將它修飾一個特定的程序成員時,它將發揮其應用的作用,例如:Override用于修飾方法時,增加了@Target這個meta-annotation就使編譯器對annotation作檢查,從而去掉修飾錯誤類型的Override。
Target meta-annotation類型有唯一的value作為成員。這個成員的類型是java.lang.annotation.ElementType[]類型的,ElementType類型是可以被標注的程序成員的枚舉類型。
2.Retention的用法
我們在文章的開頭曾經提到過Retention,但是沒有詳細講解。Retention描述了annotation是否被編譯器丟棄或者保留在class文件;如果保留在class文件中,是否在class文件被裝載時被虛擬機讀取。默認情況下,annotation被保存在class文件中,但在運行時并不能被反射訪問。Retention具有三個取值:source、class、runtime,這些取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。
Retention meta-annotation類型有唯一的value作為成員,它的取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。
3.Documented
Documented是一個meta-annotation類型,用于描述其它類型的annotation應該被作為被標注的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化。
Documented是一個marker annotation,沒有成員。
4.Inherited
@Inherited meta-annotation也是一個marker annotation,它闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation將被用于該class的子類。
注意:@Inherited annotation類型是被標注過的class的子類所繼承。類并不從它所實現的接口繼承annotation,方法并不從它所重載的方法繼承annotation。
值得思考的是,當@Inherited annotation類型標注的annotation的Retention是RetentionPolicy.RUNTIME,則反射API增強了這種繼承性。如果我們使用java.lang.reflect去查詢一個@Inherited annotation類型的annotation時,反射代碼檢查將展開工作:檢查class和其父類,直到發現指定的annotation類型被發現,或者到達類繼承結構的頂層。
六、總結:
本文幾乎覆蓋了所有的Annotation的概念和知識點,從annotation的定義、語法到工作原理、如何自定義annotation,直至meta-annotation。其中也具有一些配套的代碼片斷可參考,雖然不是很多,但是可謂言簡意賅、著其重點,本人認為用好annotation的關鍵還在于使用。希望本手冊能夠幫助大家用好annotation,這也是本人的最大快樂。
作者:cleverpig(作者的Blog: http://blog.matrix.org.cn/page/cleverpig )
原文: http://www.matrix.org.cn/resource/article/44/44055_Java+Annotation+Reflect.html
關鍵字:java,annotation,reflect
前言:
在上篇文章 《Java Annotation入門》 中概要性的介紹了Annotation的定義、使用,范圍涵蓋較廣,但是深度不夠。所以作者在《Java Annotation入門》后,繼續整理了Annotation的概念和知識點,與喜歡research的朋友們共享。
閱讀提示:文中提到的程序成員或者程序元素是一個概念,指組成程序代碼的單元:如類、方法、成員變量。
一、Annotation究竟是什么?
Annotation提供了一條與程序元素關聯任何信息或者任何元數據(metadata)的途徑。從某些方面看,annotation就像修飾符一樣被使用,并應用于包、類型、構造方法、方法、成員變量、參數、本地變量的聲明中。這些信息被存儲在annotation的“name=value”結構對中。annotation類型是一種接口,能夠通過java反射API的方式提供對其信息的訪問。
annotation能被用來為某個程序元素(類、方法、成員變量等)關聯任何的信息。需要注意的是,這里存在著一個基本的潛規則:annotaion不能影響程序代碼的執行,無論增加、刪除annotation,代碼都始終如一的執行。另外,盡管一些annotation通過java的反射api方法在運行時被訪問,而java語言解釋器在工作時忽略了這些annotation。正是由于java虛擬機忽略了annotation,導致了annotation類型在代碼中是“不起作用”的;只有通過某種配套的工具才會對annotation類型中的信息進行訪問和處理。本文中將涵蓋標準的annotation和meta-annotation類型,陪伴這些annotation類型的工具是java編譯器(當然要以某種特殊的方式處理它們)。
由于上述原因,annotation在使用時十分簡便。一個本地變量可以被一個以NonNull命名的annotation類型所標注,來作為對這個本地變量不能被賦予null值的斷言。而我們可以編寫與之配套的一個annotation代碼分析工具,使用它來對具有前面變量的代碼進行解析,并且嘗試驗證這個斷言。當然這些代碼并不必自己編寫。在JDK安裝后,在JDK/bin目錄中可以找到名為“apt”的工具,它提供了處理annotation的框架:它啟動后掃描源代碼中的annotation,并調用我們定義好的annotation處理器完成我們所要完成的工作(比如驗證前面例子中的斷言)。說到這里,annotation的強大功能似乎可以替代XDoclet這類的工具了,隨著我們的深入,大家會更加堅信這一點。
注:詳細描述請參看jsr250規范:
http://www.jcp.org/aboutJava/communityprocess/pfd/jsr250/
二、Annotation的定義:
這段文字開始介紹annotation相關技術。在此大家將看到java5.0的標準annotation類型,這種標準類型就是前文中所說的“內建”類型,它們可以直接被javac支持。可喜的是,在java6.0beta版中的javac已經加入了對自定義annotation的支持。
1。Annotation的概念和語法:
首先,關鍵的概念是理解annotation是與一個程序元素相關聯信息或者元數據的標注。它從不影響java程序的執行,但是對例如編譯器警告或者像文檔生成器等輔助工具產生影響。
下面是常用的annotation列表,我們應該注意在annotation和annotation類型之間的不同:
A.annotation:
annotation使用了在java5.0所帶來的新語法,它的行為十分類似public、final這樣的修飾符。每個annotation具有一個名字和成員個數>=0。每個annotation的成員具有被稱為name=value對的名字和值(就像javabean一樣),name=value裝載了annotation的信息。
B.annotation類型:
annotation類型定義了annotation的名字、類型、成員默認值。一個annotation類型可以說是一個特殊的java接口,它的成員變量是受限制的,而聲明annotation類型時需要使用新語法。當我們通過java反射api訪問annotation時,返回值將是一個實現了該annotation類型接口的對象,通過訪問這個對象我們能方便的訪問到其annotation成員。后面的章節將提到在java5.0的java.lang包里包含的3個標準annotation類型。
C.annotation成員:
annotation的成員在annotation類型中以無參數的方法的形式被聲明。其方法名和返回值定義了該成員的名字和類型。在此有一個特定的默認語法:允許聲明任何annotation成員的默認值:一個annotation可以將name=value對作為沒有定義默認值的annotation成員的值,當然也可以使用name=value對來覆蓋其它成員默認值。這一點有些近似類的繼承特性,父類的構造函數可以作為子類的默認構造函數,但是也可以被子類覆蓋。
D.marker annotation類型:
一個沒有成員定義的annotation類型被稱為marker annotation。這種annotation類型僅使用自身的存在與否來為我們提供信息。如后面要說的Override。
E.meta-annotation:
meta-annotation也稱為元annotation,它是被用來聲明annotation類型的annotation。Java5.0提供了一些標準的元-annotation類型。下面介紹的target、retention就是meta-annotation。
F.target:
annotation的target是一個被標注的程序元素。target說明了annotation所修飾的對象范圍:annotation可被用于packages、types(類、接口、枚舉、annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。在annotation類型的聲明中使用了target可更加明晰其修飾的目標。
G.retention:
annotation的retention定義了該annotation被保留的時間長短:某些annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀取(請注意并不影響class的執行,因為annotation與class在使用上是被分離的)。使用這個meta-annotation可以對annotation的“生命周期”限制。
H.metadata:
由于metadata被廣泛使用于各種計算機開發過程中,所以當我們在這里談論的metadata即元數據通常指被annotation裝載的信息或者annotation本身。
2。使用標準Annotation:
java5.0在java.lang包中定義了3種標準的annotation類型:
A.Override:
java.lang.Override是一個marker annotation類型,它被用作標注方法。它說明了被標注的方法重載了父類的方法,起到了斷言的作用。如果我們使用了這種annotation在一個沒有覆蓋父類方法的方法時,java編譯器將以一個編譯錯誤來警示。
這個annotaton常常在我們試圖覆蓋父類方法而確又寫錯了方法名時發揮威力。
使用方法極其簡單:在使用此annotation時只要在被修飾的方法前面加上@Override。
下面的代碼是一個使用@Override修飾一個企圖重載父類的toString方法,而又存在拼寫錯誤的sample:
清單1:
@Override public String toSting() {?? // 注意方法名拼寫錯了 ????return "[" + super.toString() + "]"; }
B.Deprecated:
同樣Deprecated也是一個marker annotation。當一個類型或者類型成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標注的程序元素。而且這種修飾具有一定的“延續性”:如果我們在代碼中通過繼承或者覆蓋的方式使用了這個過時的類型或者成員,雖然繼承或者覆蓋后的類型或者成員并不是被聲明為@Deprecated,但編譯器仍然要報警。
值得注意,@Deprecated這個annotation類型和javadoc中的@deprecated這個tag是有區別的:前者是java編譯器識別的,而后者是被javadoc工具所識別用來生成文檔(包含程序成員為什么已經過時、它應當如何被禁止或者替代的描述)。
在java5.0,java編譯器仍然象其從前版本那樣尋找@deprecated這個javadoc tag,并使用它們產生警告信息。但是這種狀況將在后續版本中改變,我們應在現在就開始使用@Deprecated來修飾過時的方法而不是@deprecated javadoc tag。
清單2:
下面是一段使用@Deprecated的代碼: /** * 這里是javadoc的@deprecated聲明. * @deprecated No one has players for this format any more.??Use VHS instead. */ @Deprecated public class Betamax { ... }
C.SuppressWarnings:
@SuppressWarnings被用于有選擇的關閉編譯器對類、方法、成員變量、變量初始化的警告。在java5.0,sun提供的javac編譯器為我們提供了-Xlint選項來使編譯器對合法的程序代碼提出警告,此種警告從某種程度上代表了程序錯誤。例如當我們使用一個generic collection類而又沒有提供它的類型時,編譯器將提示出"unchecked warning"的警告。
通常當這種情況發生時,我們就需要查找引起警告的代碼。如果它真的表示錯誤,我們就需要糾正它。例如如果警告信息表明我們代碼中的switch語句沒有覆蓋所有可能的case,那么我們就應增加一個默認的case來避免這種警告。
相仿,有時我們無法避免這種警告,例如,我們使用必須和非generic的舊代碼交互的generic collection類時,我們不能避免這個unchecked warning。此時@SuppressWarning就要派上用場了,在調用的方法前增加@SuppressWarnings修飾,告訴編譯器停止對此方法的警告。
SuppressWarning不是一個marker annotation。它有一個類型為String[]的成員,這個成員的值為被禁止的警告名。對于javac編譯器來講,被-Xlint選項有效的警告名也同樣對@SuppressWarings有效,同時編譯器忽略掉無法識別的警告名。
annotation語法允許在annotation名后跟括號,括號中是使用逗號分割的name=value對用于為annotation的成員賦值:
清單3:
@SuppressWarnings(value={"unchecked","fallthrough"}) public void lintTrap() { /* sloppy method body omitted */ }
在這個例子中SuppressWarnings annotation類型只定義了一個單一的成員,所以只有一個簡單的value={...}作為name=value對。又由于成員值是一個數組,故使用大括號來聲明數組值。
注意:我們可以在下面的情況中縮寫annotation:當annotation只有單一成員,并成員命名為"value="。這時可以省去"value="。比如將上面的SuppressWarnings annotation進行縮寫:
清單4:
@SuppressWarnings({"unchecked","fallthrough"})
如果SuppressWarnings所聲明的被禁止警告個數為一個時,可以省去大括號:
@SuppressWarnings("unchecked")
3。Annotation語法:
在上一個章節中,我們看到書寫marker annotation和單一成員annotation的語法。下面本人來介紹一下完整的語法:
annotation由“@+annotation類型名稱+(..逗號分割的name-value對...)”組成。其中成員可以按照任何的順序。如果annotation類型定義了某個成員的默認值,則這個成員可以被省略。成員值必須為編譯時常量、內嵌的annotation或者數組。
下面我們將定義一個annotation類型名為Reviews,它有一個由@Review annotation數組構成的成員。這個@Review annotation類型有三個成員:"reviewer"是一個字符串,"comment" 是一個具有默認值的可選的字符串,"grade"是一個Review.Grade枚舉類型值。
清單5:
@Reviews({??// Single-value annotation, so "value=" is omitted here ????@Review(grade=Review.Grade.EXCELLENT, ????????????reviewer="df"), ????@Review(grade=Review.Grade.UNSATISFACTORY, ????????????reviewer="eg", ????????????comment="This method needs an @Override annotation") })
annotation語法的另一個重要規則是沒有程序成員可以有多于一個的同一annotation實例。例如在一個類中簡單的放置多個@Review annotation。這也是在上面代碼中定義@Reviews annotation類型數組的原因。
4。Annotation成員類型和值:
annotation成員必須是非空的編譯時常量表達式。可用的成員類型為:primitive類型、, String, Class, enumerated類型, annotation類型, 和前面類型的數組。
下面我們定義了一個名為UncheckedExceptions 的annotation類型,它的成員是一個擴展了RuntimeException類的類數組。
清單6:
@UncheckedExceptions({ ????IllegalArgumentException.class, StringIndexOutOfBoundsException.class })
5。Annotation的目標:
annotation通常被放在類型定義和成員定義的前面。然而它也出現在package、方法參數、本地變量的前面。下面,我們來討論一下這些不大常用的寫法:
package annotation出現在package聲明的前面。
下面的例子package-info.java中不包含任何的公共類型定義,卻包含一個可選的javadoc注釋。
清單7:
/** * This package holds my custom annotation types. */ @com.davidflanagan.annotations.Author("David Flanagan") package com.davidflanagan.annotations;
當package-info.java文件被編譯時,它將產生名為包含annotation(特殊的接口)聲明的package-info.class的類。這個接口沒有成員,它的名字package-info不是一個合法的java標識,所以它不能用在java源代碼中。這個接口的存在只是簡單的被看作一個為package annotation準備的占位符。
用于修飾方法參數、catch參數、本地變量的annotation只是簡單的出現在這些程序成員的修飾符位置。java類文件格式沒有為本地變量或者catch參數存儲annotation作準備,所以這些annotation總是保留在源代碼級別(source retention);方法參數annotation能夠保存在類文件中,也可以在保留到運行時。
最后,請注意,枚舉類型定義中不允許任何的修飾符修飾其枚舉值。
6。Annotation和默認值:
在Annotation中,沒有默認值的成員必須有一個成員值。而如何理解默認值是如何被處理就是一個很重要的細節:annotation類型所定義的成員默認值被存儲在class文件中,不被編譯到annotation里面。如果我們修改一個annotation類型使其成員的默認值發生了改變,這個改變對于所有此類型的annotation中沒有明確提供成員值的成員產生影響(即修改了該成員的成員值)。即使在annotation類型使其成員的默認值被改變后annotation從沒被重新編譯過,該類型的annotation(改變前已經被編譯的)也受到影響。
三、Annotation工作原理:
Annotation與反射
在java5.0中Java.lang.reflect提供的反射API被擴充了讀取運行時annotation的能力。讓我們回顧一下前面所講的:一個annotation類型被定義為runtime retention后,它才是在運行時可見,當class文件被裝載時被保存在class文件中的annotation才會被虛擬機讀取。那么reflect是如何幫助我們訪問class中的annotation呢?
下文將在java.lang.reflect用于annotation的新特性,其中java.lang.reflect.AnnotatedElement是重要的接口,它代表了提供查詢annotation能力的程序成員。這個接口被java.lang.Package、java.lang.Class實現,并間接地被Method類、Constructor類、java.lang.reflect的Field類實現。而annotation中的方法參數可以通過Method類、Constructor類的getParameterAnnotations()方法獲得。
下面的代碼使用了AnnotatedElement類的isAnnotationPresent()方法判斷某個方法是否具有@Unstable annotation,從而斷言此方法是否穩定:
清單8:
import java.lang.reflect.*; Class c = WhizzBangClass.class;?????????????????????????? Method m = c.getMethod("whizzy", int.class, int.class);?? boolean unstable = m.isAnnotationPresent(Unstable.class);
isAnnotationPresent()方法對于檢查marker annotation是十分有用的,因為marker annotation沒有成員變量,所以我們只要知道class的方法是否使用了annotation修飾就可以了。而當處理具有成員的annotation時,我們通過使用getAnnotation()方法來獲得annotation的成員信息(成員名稱、成員值)。這里我們看到了一套優美的java annotation系統:如果annotation存在,那么實現了相應的annotation類型接口的對象將被getAnnotation()方法返回,接著調用定義在annotation類型中的成員方法可以方便地獲得任何成員值。
回想一下,前面介紹的@Reviews annotation,如果這個annotation類型被聲明為runtime retention的話,我們通過下面的代碼來訪問@Reviews annotation的成員值:
清單9:
AnnotatedElement target = WhizzBangClass.class; //獲得被查詢的AnnotatedElement // 查詢AnnotatedElement的@Reviews annotation信息 Reviews annotation = target.getAnnotation(Reviews.class); // 因為@Reviews annotation類型的成員為@Review annotation類型的數組, // 所以下面聲明了Review[] reviews保存@Reviews annotation類型的value成員值。 Review[] reviews = annotation.value(); // 查詢每個@Review annotation的成員信息 for(Review r : reviews) { ????Review.Grade grade = r.grade(); ????String reviewer = r.reviewer(); ????String comment = r.comment(); ????System.out.printf("%s assigned a grade of %s and comment '%s'%n", ??????????????????????reviewer, grade, comment); }
四、如何自定義Annotation?
1.詳解annotation與接口的異同:
因為annotation類型是一個非凡的接口,所以兩者之間存在著某些差異:
A.Annotation類型使用關鍵字@interface而不是interface。
這個關鍵字聲明隱含了一個信息:它是繼承了java.lang.annotation.Annotation接口,并非聲明了一個interface。
B.Annotation類型、方法定義是獨特的、受限制的。
Annotation類型的方法必須聲明為無參數、無異常拋出的。這些方法定義了annotation的成員:方法名成為了成員名,而方法返回值成為了成員的類型。而方法返回值類型必須為primitive類型、Class類型、枚舉類型、annotation類型或者由前面類型之一作為元素的一維數組。方法的后面可以使用default和一個默認數值來聲明成員的默認值,null不能作為成員默認值,這與我們在非annotation類型中定義方法有很大不同。
Annotation類型和它的方法不能使用annotation類型的參數、成員不能是generic。只有返回值類型是Class的方法可以在annotation類型中使用generic,因為此方法能夠用類轉換將各種類型轉換為Class。
C.Annotation類型又與接口有著近似之處。
它們可以定義常量、靜態成員類型(比如枚舉類型定義)。Annotation類型也可以如接口一般被實現或者繼承。
2.實例:
下面,我們將看到如何定義annotation類型的example。它展示了annotation類型聲明以及@interface與interface之間的不同:
清單10:
package com.davidflanagan.annotations; import java.lang.annotation.*; /** * 使用annotation來描述那些被標注的成員是不穩定的,需要更改 */ @Retention(RetentionPolicy.RUNTIME) public @interface Unstable {}
下面的另一個example只定義了一個成員。并通過將這個成員命名為value,使我們可以方便的使用這種annotation的快捷聲明方式:
清單11:
/** * 使用Author這個annotation定義在程序中指出代碼的作者 */ public @interface Author { ????/** 返回作者名 */ ????String value(); }
以下的example更加復雜。Reviews annotation類型只有一個成員,但是這個成員的類型是復雜的:由Review annotation組成的數組。Review annotation類型有3個成員:枚舉類型成員grade、表示Review名稱的字符串類型成員Reviewer、具有默認值的字符串類型成員Comment。
清單12:
import java.lang.annotation.*; ???????? /** * Reviews annotation類型只有一個成員, * 但是這個成員的類型是復雜的:由Review annotation組成的數組 */ @Retention(RetentionPolicy.RUNTIME) public @interface Reviews { ????Review[] value(); } /** * Review annotation類型有3個成員: * 枚舉類型成員grade、 ??* 表示Review名稱的字符串類型成員Reviewer、 ??* 具有默認值的字符串類型成員Comment。 */ public @interface Review { ????// 內嵌的枚舉類型 ????public static enum Grade { EXCELLENT, SATISFACTORY, UNSATISFACTORY }; ????// 下面的方法定義了annotation的成員 ????Grade grade();???????????????? ????String reviewer();?????????? ????String comment() default "";?? }
最后,我們來定義一個annotation方法用于羅列出類運行中所有的unchecked異常(上文已經提到這種情況不一定是錯誤)。這個annotation類型將一個數組作為了唯一的成員。數組中的每個元素都是異常類。為了加強對未檢查的異常(此類異常都是在運行時拋出)進行報告,我們可以在代碼中對異常的類型進行限制:
清單13:
public @interface UncheckedExceptions { ????Class<? extends RuntimeException>[] value(); }
五、Meta-Annotation
Annotation類型可以被它們自己所標注。Java5.0定義了4個標準的meta-annotation類型,它們被用來提供對其它annotation類型作說明。這些類型和它們所支持的類在java.lang.annotation包中可以找到。如果需要更詳細的信息可以參考jdk5.0手冊。
1.再談Target
作為meta-annotation類型的Target,它描述了annotation所修飾的程序成員的類型。當一個annotation類型沒有Target時,它將被作為普通的annotation看待。當將它修飾一個特定的程序成員時,它將發揮其應用的作用,例如:Override用于修飾方法時,增加了@Target這個meta-annotation就使編譯器對annotation作檢查,從而去掉修飾錯誤類型的Override。
Target meta-annotation類型有唯一的value作為成員。這個成員的類型是java.lang.annotation.ElementType[]類型的,ElementType類型是可以被標注的程序成員的枚舉類型。
2.Retention的用法
我們在文章的開頭曾經提到過Retention,但是沒有詳細講解。Retention描述了annotation是否被編譯器丟棄或者保留在class文件;如果保留在class文件中,是否在class文件被裝載時被虛擬機讀取。默認情況下,annotation被保存在class文件中,但在運行時并不能被反射訪問。Retention具有三個取值:source、class、runtime,這些取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。
Retention meta-annotation類型有唯一的value作為成員,它的取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。
3.Documented
Documented是一個meta-annotation類型,用于描述其它類型的annotation應該被作為被標注的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化。
Documented是一個marker annotation,沒有成員。
4.Inherited
@Inherited meta-annotation也是一個marker annotation,它闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation將被用于該class的子類。
注意:@Inherited annotation類型是被標注過的class的子類所繼承。類并不從它所實現的接口繼承annotation,方法并不從它所重載的方法繼承annotation。
值得思考的是,當@Inherited annotation類型標注的annotation的Retention是RetentionPolicy.RUNTIME,則反射API增強了這種繼承性。如果我們使用java.lang.reflect去查詢一個@Inherited annotation類型的annotation時,反射代碼檢查將展開工作:檢查class和其父類,直到發現指定的annotation類型被發現,或者到達類繼承結構的頂層。
六、總結:
本文幾乎覆蓋了所有的Annotation的概念和知識點,從annotation的定義、語法到工作原理、如何自定義annotation,直至meta-annotation。其中也具有一些配套的代碼片斷可參考,雖然不是很多,但是可謂言簡意賅、著其重點,本人認為用好annotation的關鍵還在于使用。希望本手冊能夠幫助大家用好annotation,這也是本人的最大快樂。
發表于 @?
2006年04月11日 13:45:00
?|?
評論 (0)
版權聲明:本文可以自由轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
?
作者:cleverpig(作者的Blog: http://blog.matrix.org.cn/page/cleverpig )
原文: http://www.matrix.org.cn/resource/article/44/44062_Java+Annotation+Apt.html
關鍵字:java,annotation,apt
前言:
前不久在matrix上先后發表了 《java annotation 入門》 、 《java annotation 手冊》 兩篇文章,比較全面的對java annotation的語法、原理、使用三方面進行了闡述。由于《入門》中的簡單例程雖然簡單明了的說明了annotation用法,但給大家的感覺可能是意猶未見,所以在此行文《java annotation高級應用》,具體實例化解釋annotation和annotation processing tool(APT)的使用。望能對各位的有所幫助。
一、摘要:
《java annotation高級應用》具體實例化解釋annotation和annotation processing tool(APT)的使用。望能對各位的有所幫助。本文列舉了用于演示annotation的BRFW演示框架、演示APT的apt代碼實例,并對其進行較為深度的分析,希望大家多多提意見。
二、annotation實例分析
1.BRFW(Beaninfo Runtime FrameWork)定義:
本人編寫的一個annotation功能演示框架。顧名思義,BRFW就是在運行時取得bean信息的框架。
2.BRFW的功能:
A.源代碼級annotation :在bean的源代碼中使用annotation定義bean的信息;
B.運行時獲取bean數據 :在運行時分析bean class中的annotation,并將當前bean class中field信息取出,功能類似xdoclet;
C.運行時bean數據的xml綁定 :將獲得的bean數據構造為xml文件格式展現。熟悉j2ee的朋友知道,這個功能類似jaxb。
3.BRFW框架:
BRFW主要包含以下幾個類:
A.Persistent類 :定義了用于修飾類的固有類型成員變量的annotation。
B.Exportable類 :定義了用于修飾Class的類型的annotation。
C.ExportToXml類 :核心類,用于完成BRFW的主要功能:將具有Exportable Annotation的bean對象轉換為xml格式文本。
D.AddressForTest類 :被A和B修飾過的用于測試目的的地址bean類。其中包含了地址定義所必需的信息:國家、省級、城市、街道、門牌等。
E.AddressListForTest類 :被A和B修飾過的友人通訊錄bean類。其中包含了通訊錄所必備的信息:友人姓名、年齡、電話、住址(成員為AddressForTest類型的ArrayList)、備注。需要說明的是電話這個bean成員變量是由字符串類型組成的ArrayList類型。由于朋友的住址可能不唯一,故這里的住址為由AddressForTest類型組成的ArrayList。
從上面的列表中,可以發現A、B用于修飾bean類和其類成員;C主要用于取出bean類的數據并將其作xml綁定,代碼中使用了E作為測試類;E中可能包含著多個D。
在了解了這個簡單框架后,我們來看一下BRFW的代碼吧!
4.BRFW源代碼分析:
A.Persistent類:
清單1:
B.Exportable類:
清單2:
C.AddressForTest類:
清單3:
D.AddressListForTest類:
清單4:
E.ExportToXml類:
清單5:
在ExportToXml類之前的類比較簡單,這里必須說明一下ExportToXml類:此類的核心函數是exportObject和exportFields方法,前者輸出對象的xml信息,后者輸出對象成員變量的信息。由于對象類型和成員類型的多樣性,所以采取了以下的邏輯:
在exportObject方法中,當對象類型為Collection和Map類型時,則需要遞歸調用exportObject進行處理;
而如果對象類型不是Collection和Map類型的話,將判斷對象類是否被Exportable annotation修飾過:
如果沒有被修飾,則直接輸出<對象類名>對象.toString()</對象類名>作為xml綁定結果的一部分;
如果被修飾過,則需要調用exportFields方法對對象的成員變量進行xml綁定。
在exportFields方法中,首先取出對象的所有成員,然后獲得被Persisitent annotation修飾的成員。在其后的一句:field.setAccessible(true)是很重要的,因為bean類定義中的成員訪問修飾都是private,所以為了避免java虛擬機檢查對私有成員的訪問權限,加上這一句是必需的。接著后面的語句便是輸出<成員名>成員值</成員名>這樣的xml結構。像在exportObject方法中一般,仍然需要判斷成員類型是否為Collection和Map類型,如果為上述兩種類型之一,則要在exportFields中再次調用exportObject來處理這個成員。
在main方法中,本人編寫了一段演示代碼:建立了一個由單個友人地址類(AddressForTest)組成的ArrayList作為通訊錄類(AddressForTest)的成員的通訊錄對象,并且輸出這個對象的xml綁定,運行結果如下:
清單6:
三、APT實例分析:
1.何謂APT?
根據sun官方的解釋,APT(annotation processing tool)是一個命令行工具,它對源代碼文件進行檢測找出其中的annotation后,使用annotation processors來處理annotation。而annotation processors使用了一套反射API并具備對JSR175規范的支持。
annotation processors處理annotation的基本過程如下:首先,APT運行annotation processors根據提供的源文件中的annotation生成源代碼文件和其它的文件(文件具體內容由annotation processors的編寫者決定),接著APT將生成的源代碼文件和提供的源文件進行編譯生成類文件。
簡單的和前面所講的annotation實例BRFW相比,APT就像一個在編譯時處理annotation的javac。而且從sun開發者的blog中看到,java1.6 beta版中已將APT的功能寫入到了javac中,這樣只要執行帶有特定參數的javac就能達到APT的功能。
2.為何使用APT?
使用APT主要目的是簡化開發者的工作量,因為APT可以在編譯程序源代碼的同時,生成一些附屬文件(比如源文件、類文件、程序發布描述文字等),這些附屬文件的內容也都是與源代碼相關的。換句話說,使用APT就是代替了傳統的對代碼信息和附屬文件的維護工作。使用過hibernate或者beehive等軟件的朋友可能深有體會。APT可以在編譯生成代碼類的同時將相關的文件寫好,比如在使用beehive時,在代碼中使用annotation聲明了許多struct要用到的配置信息,而在編譯后,這些信息會被APT以struct配置文件的方式存放。
3.如何定義processor?
A.APT工作過程:
從整個過程來講,首先APT檢測在源代碼文件中哪些annotation存在。然后APT將查找我們編寫的annotation processor factories類,并且要求factories類提供處理源文件中所涉及的annotation的annotation processor。接下來,一個合適的annotation processors將被執行,如果在processors生成源代碼文件時,該文件中含有annotation,則APT將重復上面的過程直到沒有新文件生成。
B.編寫annotation processors:
編寫一個annotation processors需要使用java1.5 lib目錄中的tools.jar提供的以下4個包:
com.sun.mirror.apt: 和APT交互的接口;
com.sun.mirror.declaration: 用于模式化類成員、類方法、類聲明的接口;
com.sun.mirror.type: 用于模式化源代碼中類型的接口;?
com.sun.mirror.util: 提供了用于處理類型和聲明的一些工具。?
每個processor實現了在com.sun.mirror.apt包中的AnnotationProcessor接口,這個接口有一個名為“process”的方法,該方法是在APT調用processor時將被用到的。一個processor可以處理一種或者多種annotation類型。
一個processor實例被其相應的工廠返回,此工廠為AnnotationProcessorFactory接口的實現。APT將調用工廠類的getProcessorFor方法來獲得processor。在調用過程中,APT將提供給工廠類一個AnnotationProcessorEnvironment 類型的processor環境類對象,在這個環境對象中,processor將找到其執行所需要的每件東西,包括對所操作的程序結構的參考,與APT通訊并合作一同完成新文件的建立和警告/錯誤信息的傳輸。
提供工廠類有兩個方式:通過APT的“-factory”命令行參數提供,或者讓工廠類在APT的發現過程中被自動定位(關于發現過程詳細介紹請看 http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html )。前者對于一個已知的factory來講是一種主動而又簡單的方式;而后者則是需要在jar文件的META-INF/services目錄中提供一個特定的發現路徑:
在包含factory類的jar文件中作以下的操作:在META-INF/services目錄中建立一個名為com.sun.mirror.apt.AnnotationProcessorFactory 的UTF-8編碼文件,在文件中寫入所有要使用到的factory類全名,每個類為一個單獨行。
4.一個簡單的APT實例分析:
A.實例構成:
Review類:定義Review Annotation;
ReviewProcessorFactory類:生成ReviewProcessor的工廠類;
ReviewProcessor類:定義處理Review annotation的Processor;
ReviewDeclarationVisitor類:定義Review annotation聲明訪問者,ReviewProcessor將要使用之對Class進行訪問。
runapt.bat:定義了使用自定義的ReviewProcessor對Review類源代碼文件進行處理的APT命令行。
B.Review類:
清單7:
C.ReviewProcessorFactory類:
清單8:
D.ReviewProcessor類:
清單9:
E.ReviewDeclarationVisitor類:
清單10:
作者:cleverpig(作者的Blog: http://blog.matrix.org.cn/page/cleverpig )
原文: http://www.matrix.org.cn/resource/article/44/44062_Java+Annotation+Apt.html
關鍵字:java,annotation,apt
前言:
前不久在matrix上先后發表了 《java annotation 入門》 、 《java annotation 手冊》 兩篇文章,比較全面的對java annotation的語法、原理、使用三方面進行了闡述。由于《入門》中的簡單例程雖然簡單明了的說明了annotation用法,但給大家的感覺可能是意猶未見,所以在此行文《java annotation高級應用》,具體實例化解釋annotation和annotation processing tool(APT)的使用。望能對各位的有所幫助。
一、摘要:
《java annotation高級應用》具體實例化解釋annotation和annotation processing tool(APT)的使用。望能對各位的有所幫助。本文列舉了用于演示annotation的BRFW演示框架、演示APT的apt代碼實例,并對其進行較為深度的分析,希望大家多多提意見。
二、annotation實例分析
1.BRFW(Beaninfo Runtime FrameWork)定義:
本人編寫的一個annotation功能演示框架。顧名思義,BRFW就是在運行時取得bean信息的框架。
2.BRFW的功能:
A.源代碼級annotation :在bean的源代碼中使用annotation定義bean的信息;
B.運行時獲取bean數據 :在運行時分析bean class中的annotation,并將當前bean class中field信息取出,功能類似xdoclet;
C.運行時bean數據的xml綁定 :將獲得的bean數據構造為xml文件格式展現。熟悉j2ee的朋友知道,這個功能類似jaxb。
3.BRFW框架:
BRFW主要包含以下幾個類:
A.Persistent類 :定義了用于修飾類的固有類型成員變量的annotation。
B.Exportable類 :定義了用于修飾Class的類型的annotation。
C.ExportToXml類 :核心類,用于完成BRFW的主要功能:將具有Exportable Annotation的bean對象轉換為xml格式文本。
D.AddressForTest類 :被A和B修飾過的用于測試目的的地址bean類。其中包含了地址定義所必需的信息:國家、省級、城市、街道、門牌等。
E.AddressListForTest類 :被A和B修飾過的友人通訊錄bean類。其中包含了通訊錄所必備的信息:友人姓名、年齡、電話、住址(成員為AddressForTest類型的ArrayList)、備注。需要說明的是電話這個bean成員變量是由字符串類型組成的ArrayList類型。由于朋友的住址可能不唯一,故這里的住址為由AddressForTest類型組成的ArrayList。
從上面的列表中,可以發現A、B用于修飾bean類和其類成員;C主要用于取出bean類的數據并將其作xml綁定,代碼中使用了E作為測試類;E中可能包含著多個D。
在了解了這個簡單框架后,我們來看一下BRFW的代碼吧!
4.BRFW源代碼分析:
A.Persistent類:
清單1:
package com.bjinfotech.practice.annotation.runtimeframework; import java.lang.annotation.*; /** * 用于修飾類的固有類型成員變量的annotation * @author cleverpig * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Persistent { ????????String value() default ""; }
B.Exportable類:
清單2:
package com.bjinfotech.practice.annotation.runtimeframework; import java.lang.annotation.*; /** * 用于修飾類的類型的annotation * @author cleverpig * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Exportable { ????????//名稱 ????????String name() default ""; ????????//描述 ????????String description() default ""; ????????//省略name和description后,用來保存name值 ????????String value() default ""; ???????? }
C.AddressForTest類:
清單3:
package com.bjinfotech.practice.annotation.runtimeframework; /** * 用于測試的地址類 * @author cleverpig * */ @Exportable("address") public class AddressForTest { ????????//國家 ????????@Persistent ????????private String country=null; ???????? ????????//省級 ????????@Persistent ????????private String province=null; ???????? ????????//城市 ????????@Persistent ????????private String city=null; ???????? ????????//街道 ????????@Persistent ????????private String street=null; ????????//門牌 ????????@Persistent ????????private String doorplate=null; ???????? ????????public AddressForTest(String country,String province, ????????????????????????String city,String street,String doorplate){ ????????????????this.country=country; ????????????????this.province=province; ????????????????this.city=city; ????????????????this.street=street; ????????????????this.doorplate=doorplate; ????????} ???????? }
D.AddressListForTest類:
清單4:
package com.bjinfotech.practice.annotation.runtimeframework; import java.util.*; /** * 友人通訊錄 * 包含:姓名、年齡、電話、住址(多個)、備注 * @author cleverpig * */ @Exportable(name="addresslist",description="address list") public class AddressListForTest { ????????//友人姓名 ????????@Persistent ????????private String friendName=null; ???????? ????????//友人年齡 ????????@Persistent ????????private int age=0; ???????? ????????//友人電話 ????????@Persistent ????????private ArrayList<String> telephone=null; ???????? ????????//友人住址:家庭、單位 ????????@Persistent ????????private ArrayList<AddressForTest> AddressForText=null; ???????? ????????//備注 ????????@Persistent ????????private String note=null; ???????? ????????public AddressListForTest(String name,int age, ????????????????????????ArrayList<String> telephoneList, ????????????????????????ArrayList<AddressForTest> addressList, ????????????????????????String note){ ????????????????this.friendName=name; ????????????????this.age=age; ????????????????this.telephone=new ArrayList<String>(telephoneList); ????????????????this.AddressForText=new ArrayList<AddressForTest>(addressList); ????????????????this.note=note; ???????????????? ????????} }
E.ExportToXml類:
清單5:
package com.bjinfotech.practice.annotation.runtimeframework; import java.lang.reflect.Field; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.ArrayList; /** * 將具有Exportable Annotation的對象轉換為xml格式文本 * @author cleverpig * */ public class ExportToXml { ????????/** ???????? * 返回對象的成員變量的值(字符串類型) ???????? * @param field 對象的成員變量 ???????? * @param fieldTypeClass 對象的類型 ???????? * @param obj 對象 ???????? * @return 對象的成員變量的值(字符串類型) ???????? */ ????????private String getFieldValue(Field field,Class fieldTypeClass,Object obj){ ????????????????String value=null; ???????????????? ????????????????try{ ????????????????????????if (fieldTypeClass==String.class){ ????????????????????????????????value=(String)field.get(obj); ????????????????????????} ????????????????????????else if (fieldTypeClass==int.class){ ????????????????????????????????value=Integer.toString(field.getInt(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==long.class){ ????????????????????????????????value=Long.toString(field.getLong(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==short.class){ ????????????????????????????????value=Short.toString(field.getShort(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==float.class){ ????????????????????????????????value=Float.toString(field.getFloat(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==double.class){ ????????????????????????????????value=Double.toString(field.getDouble(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==byte.class){ ????????????????????????????????value=Byte.toString(field.getByte(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==char.class){ ????????????????????????????????value=Character.toString(field.getChar(obj)); ????????????????????????} ????????????????????????else if (fieldTypeClass==boolean.class){ ????????????????????????????????value=Boolean.toString(field.getBoolean(obj)); ????????????????????????} ????????????????} ????????????????catch(Exception ex){ ????????????????????????ex.printStackTrace(); ????????????????????????value=null; ????????????????} ????????????????return value; ????????} ???????? ????????/** ???????? * 輸出對象的字段,當對象的字段為Collection或者Map類型時,要調用exportObject方法繼續處理 ???????? * @param obj 被處理的對象 ???????? * @throws Exception ???????? */ ????????public void exportFields(Object obj) throws Exception{ ????????????????Exportable exportable=obj.getClass().getAnnotation(Exportable.class);???????? ????????????????if (exportable!=null){ ????????????????????????if (exportable.value().length()>0){ //????????????????????????????????System.out.println("Class annotation Name:"+exportable.value()); ????????????????????????} ????????????????????????else{ //????????????????????????????????System.out.println("Class annotation Name:"+exportable.name()); ????????????????????????} ????????????????} ????????????????else{ //????????????????????????System.out.println(obj.getClass()+"類不是使用Exportable標注過的"); ????????????????} ???????????????? ????????????????//取出對象的成員變量 ????????????????Field[] fields=obj.getClass().getDeclaredFields(); ???????????????? ????????????????for(Field field:fields){ ????????????????????????//獲得成員變量的標注 ????????????????????????Persistent fieldAnnotation=field.getAnnotation(Persistent.class); ????????????????????????if (fieldAnnotation==null){ ????????????????????????????????continue; ????????????????????????} ????????????????????????//重要:避免java虛擬機檢查對私有成員的訪問權限 ????????????????????????field.setAccessible(true); ????????????????????????Class typeClass=field.getType(); ????????????????????????String name=field.getName(); ????????????????????????String value=getFieldValue(field,typeClass,obj); ???????????????????????? ????????????????????????//如果獲得成員變量的值,則輸出 ????????????????????????if (value!=null){ ????????????????????????????????System.out.println(getIndent()+"<"+name+">\n" ????????????????????????????????????????????????+getIndent()+"\t"+value+"\n"+getIndent()+"</"+name+">"); ????????????????????????} ????????????????????????//處理成員變量中類型為Collection或Map ????????????????????????else if ((field.get(obj) instanceof Collection)|| ????????????????????????????????????????(field.get(obj) instanceof Map)){ ????????????????????????????????exportObject(field.get(obj)); ????????????????????????} ????????????????????????else{ ????????????????????????????????exportObject(field.get(obj)); ????????????????????????} ???????????????????????? ????????????????} ????????} ???????? ????????//縮進深度 ????????int levelDepth=0; ????????//防止循環引用的檢查者,循環引用現象如:a包含b,而b又包含a ????????Collection<Object> cyclicChecker=new ArrayList<Object>(); ???????? ????????/** ???????? * 返回縮進字符串 ???????? * @return ???????? */ ????????private String getIndent(){ ????????????????String s=""; ????????????????for(int i=0;i<levelDepth;i++){ ????????????????????????s+="\t"; ????????????????} ????????????????return s; ????????} ????????/** ???????? * 輸出對象,如果對象類型為Collection和Map類型,則需要遞歸調用exportObject進行處理 ???????? * @param obj ???????? * @throws Exception ???????? */ ????????public void exportObject(Object obj) throws Exception{ ????????????????Exportable exportable=null; ????????????????String elementName=null; ???????????????? ????????????????//循環引用現象處理 ????????????????if (cyclicChecker.contains(obj)){ ????????????????????????return; ????????????????} ???????????????? ????????????????cyclicChecker.add(obj); ???????????????? ????????????????//首先處理Collection和Map類型 ????????????????if (obj instanceof Collection){ ????????????????????????for(Iterator i=((Collection)obj).iterator();i.hasNext();){ ????????????????????????????????exportObject(i.next()); ????????????????????????} ????????????????} ????????????????else if (obj instanceof Map){ ????????????????????????for(Iterator i=((Map)obj).keySet().iterator();i.hasNext();){ ????????????????????????????????exportObject(i.next()); ????????????????????????} ????????????????} ????????????????else{ ????????????????????????exportable=obj.getClass().getAnnotation(Exportable.class); ????????????????????????//如果obj已經被Exportable Annotation修飾過了(注意annotation是具有繼承性的), ????????????????????????//則使用其name作為輸出xml的元素name ????????????????????????if (exportable!=null){ ????????????????????????????????if (exportable.value().length()>0){ ????????????????????????????????????????elementName=exportable.value(); ????????????????????????????????} ????????????????????????????????else{ ????????????????????????????????????????elementName=exportable.name(); ????????????????????????????????} ????????????????????????} ????????????????????????//未被修飾或者Exportable Annotation的值為空字符串, ????????????????????????//則使用類名作為輸出xml的元素name ????????????????????????if (exportable==null||elementName.length()==0){ ????????????????????????????????elementName=obj.getClass().getSimpleName(); ????????????????????????} ????????????????????????//輸出xml元素頭 ????????????????????????System.out.println(getIndent()+"<"+elementName+">"); ????????????????????????levelDepth++; ????????????????????????//如果沒有被修飾,則直接輸出其toString()作為元素值 ????????????????????????if (exportable==null){ ????????????????????????????????System.out.println(getIndent()+obj.toString()); ????????????????????????} ????????????????????????//否則將對象的成員變量導出為xml ????????????????????????else{ ????????????????????????????????exportFields(obj); ????????????????????????} ????????????????????????levelDepth--; ????????????????????????//輸出xml元素結尾 ????????????????????????System.out.println(getIndent()+"</"+elementName+">"); ???????????????????????? ????????????????} ????????????????cyclicChecker.remove(obj); ????????} ???????? ????????public static void main(String[] argv){ ????????????????try{ ????????????????????????AddressForTest ad=new AddressForTest("China","Beijing", ????????????????????????????????????????"Beijing","winnerStreet","10"); ???????????????????????? ????????????????????????ExportToXml test=new ExportToXml(); ???????????????????????? ????????????????????????ArrayList<String> telephoneList=new ArrayList<String>(); ????????????????????????telephoneList.add("66608888"); ????????????????????????telephoneList.add("66608889"); ???????????????????????? ????????????????????????ArrayList<AddressForTest> adList=new ArrayList<AddressForTest>(); ????????????????????????adList.add(ad); ???????????????????????? ????????????????????????AddressListForTest adl=new AddressListForTest("coolBoy", ????????????????????????????????????????18,telephoneList,adList,"some words"); ???????????????????????? ????????????????????????test.exportObject(adl); ????????????????} ????????????????catch(Exception ex){ ????????????????????????ex.printStackTrace(); ????????????????} ????????} }
在ExportToXml類之前的類比較簡單,這里必須說明一下ExportToXml類:此類的核心函數是exportObject和exportFields方法,前者輸出對象的xml信息,后者輸出對象成員變量的信息。由于對象類型和成員類型的多樣性,所以采取了以下的邏輯:
在exportObject方法中,當對象類型為Collection和Map類型時,則需要遞歸調用exportObject進行處理;
而如果對象類型不是Collection和Map類型的話,將判斷對象類是否被Exportable annotation修飾過:
如果沒有被修飾,則直接輸出<對象類名>對象.toString()</對象類名>作為xml綁定結果的一部分;
如果被修飾過,則需要調用exportFields方法對對象的成員變量進行xml綁定。
在exportFields方法中,首先取出對象的所有成員,然后獲得被Persisitent annotation修飾的成員。在其后的一句:field.setAccessible(true)是很重要的,因為bean類定義中的成員訪問修飾都是private,所以為了避免java虛擬機檢查對私有成員的訪問權限,加上這一句是必需的。接著后面的語句便是輸出<成員名>成員值</成員名>這樣的xml結構。像在exportObject方法中一般,仍然需要判斷成員類型是否為Collection和Map類型,如果為上述兩種類型之一,則要在exportFields中再次調用exportObject來處理這個成員。
在main方法中,本人編寫了一段演示代碼:建立了一個由單個友人地址類(AddressForTest)組成的ArrayList作為通訊錄類(AddressForTest)的成員的通訊錄對象,并且輸出這個對象的xml綁定,運行結果如下:
清單6:
<addresslist> ????????<friendName> ????????????????coolBoy ????????</friendName> ????????<age> ????????????????18 ????????</age> ????????<String> ????????????????66608888 ????????</String> ????????<String> ????????????????66608889 ????????</String> ????????<address> ????????????????<country> ????????????????????????China ????????????????</country> ????????????????<province> ????????????????????????Beijing ????????????????</province> ????????????????<city> ????????????????????????Beijing ????????????????</city> ????????????????<street> ????????????????????????winnerStreet ????????????????</street> ????????????????<doorplate> ????????????????????????10 ????????????????</doorplate> ????????</address> ????????<note> ????????????????some words ????????</note> </addresslist>
三、APT實例分析:
1.何謂APT?
根據sun官方的解釋,APT(annotation processing tool)是一個命令行工具,它對源代碼文件進行檢測找出其中的annotation后,使用annotation processors來處理annotation。而annotation processors使用了一套反射API并具備對JSR175規范的支持。
annotation processors處理annotation的基本過程如下:首先,APT運行annotation processors根據提供的源文件中的annotation生成源代碼文件和其它的文件(文件具體內容由annotation processors的編寫者決定),接著APT將生成的源代碼文件和提供的源文件進行編譯生成類文件。
簡單的和前面所講的annotation實例BRFW相比,APT就像一個在編譯時處理annotation的javac。而且從sun開發者的blog中看到,java1.6 beta版中已將APT的功能寫入到了javac中,這樣只要執行帶有特定參數的javac就能達到APT的功能。
2.為何使用APT?
使用APT主要目的是簡化開發者的工作量,因為APT可以在編譯程序源代碼的同時,生成一些附屬文件(比如源文件、類文件、程序發布描述文字等),這些附屬文件的內容也都是與源代碼相關的。換句話說,使用APT就是代替了傳統的對代碼信息和附屬文件的維護工作。使用過hibernate或者beehive等軟件的朋友可能深有體會。APT可以在編譯生成代碼類的同時將相關的文件寫好,比如在使用beehive時,在代碼中使用annotation聲明了許多struct要用到的配置信息,而在編譯后,這些信息會被APT以struct配置文件的方式存放。
3.如何定義processor?
A.APT工作過程:
從整個過程來講,首先APT檢測在源代碼文件中哪些annotation存在。然后APT將查找我們編寫的annotation processor factories類,并且要求factories類提供處理源文件中所涉及的annotation的annotation processor。接下來,一個合適的annotation processors將被執行,如果在processors生成源代碼文件時,該文件中含有annotation,則APT將重復上面的過程直到沒有新文件生成。
B.編寫annotation processors:
編寫一個annotation processors需要使用java1.5 lib目錄中的tools.jar提供的以下4個包:
com.sun.mirror.apt: 和APT交互的接口;
com.sun.mirror.declaration: 用于模式化類成員、類方法、類聲明的接口;
com.sun.mirror.type: 用于模式化源代碼中類型的接口;?
com.sun.mirror.util: 提供了用于處理類型和聲明的一些工具。?
每個processor實現了在com.sun.mirror.apt包中的AnnotationProcessor接口,這個接口有一個名為“process”的方法,該方法是在APT調用processor時將被用到的。一個processor可以處理一種或者多種annotation類型。
一個processor實例被其相應的工廠返回,此工廠為AnnotationProcessorFactory接口的實現。APT將調用工廠類的getProcessorFor方法來獲得processor。在調用過程中,APT將提供給工廠類一個AnnotationProcessorEnvironment 類型的processor環境類對象,在這個環境對象中,processor將找到其執行所需要的每件東西,包括對所操作的程序結構的參考,與APT通訊并合作一同完成新文件的建立和警告/錯誤信息的傳輸。
提供工廠類有兩個方式:通過APT的“-factory”命令行參數提供,或者讓工廠類在APT的發現過程中被自動定位(關于發現過程詳細介紹請看 http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html )。前者對于一個已知的factory來講是一種主動而又簡單的方式;而后者則是需要在jar文件的META-INF/services目錄中提供一個特定的發現路徑:
在包含factory類的jar文件中作以下的操作:在META-INF/services目錄中建立一個名為com.sun.mirror.apt.AnnotationProcessorFactory 的UTF-8編碼文件,在文件中寫入所有要使用到的factory類全名,每個類為一個單獨行。
4.一個簡單的APT實例分析:
A.實例構成:
Review類:定義Review Annotation;
ReviewProcessorFactory類:生成ReviewProcessor的工廠類;
ReviewProcessor類:定義處理Review annotation的Processor;
ReviewDeclarationVisitor類:定義Review annotation聲明訪問者,ReviewProcessor將要使用之對Class進行訪問。
runapt.bat:定義了使用自定義的ReviewProcessor對Review類源代碼文件進行處理的APT命令行。
B.Review類:
清單7:
package com.bjinfotech.practice.annotation.apt; /** * 定義Review Annotation * @author cleverpig * */ public @interface Review { ????????public static enum TypeEnum{EXCELLENT,NICE,NORMAL,BAD}; ????????TypeEnum type(); ????????String name() default "Review"; }
C.ReviewProcessorFactory類:
清單8:
package com.bjinfotech.practice.annotation.apt; import java.util.Collection; import java.util.Set; import java.util.Arrays; import com.sun.mirror.apt.*; import com.sun.mirror.declaration.AnnotationTypeDeclaration; import com.sun.mirror.apt.AnnotationProcessorEnvironment; //請注意為了方便,使用了靜態import import static java.util.Collections.unmodifiableCollection; import static java.util.Collections.emptySet; /** * 生成ReviewProcessor的工廠類 * @author cleverpig * */ public class ReviewProcessorFactory implements AnnotationProcessorFactory{ ????????/** ???????? * 獲得針對某個(些)類型聲明定義的Processor ???????? * @param atds 類型聲明集合 ???????? * @param env processor環境 ???????? */ ????????public AnnotationProcessor getProcessorFor( ????????????????????????Set<AnnotationTypeDeclaration> atds, ????????????????????????AnnotationProcessorEnvironment env){ ????????????????return new ReviewProcessor(env); ????????} ????????/** ???????? * 定義processor所支持的annotation類型 ???????? * @return processor所支持的annotation類型的集合 ???????? */ ????????public Collection<String> ????????supportedAnnotationTypes(){ ????????????????//“*”表示支持所有的annotation類型 ????????????????//當然也可以修改為“foo.bar.*”、“foo.bar.Baz”,來對所支持的類型進行修飾 ????????????return unmodifiableCollection(Arrays.asList("*")); ????} ???????? ????????/** ???????? * 定義processor支持的選項 ???????? * @return processor支持選項的集合 ???????? */ ????????public Collection<String> ????????supportedOptions(){ ????????????????//返回空集合 ????????????return emptySet(); ????} ???????? ????????public static void main(String[] argv){ ????????????????System.out.println("ok"); ????????} }
D.ReviewProcessor類:
清單9:
package com.bjinfotech.practice.annotation.apt; import com.sun.mirror.apt.AnnotationProcessor; import com.sun.mirror.apt.AnnotationProcessorEnvironment; import com.sun.mirror.declaration.TypeDeclaration; import com.sun.mirror.util.DeclarationVisitors; import com.sun.mirror.util.DeclarationVisitor; /** * 定義Review annotation的Processor * @author cleverpig * */ public class ReviewProcessor implements AnnotationProcessor{ ????????//Processor所工作的環境 ????????AnnotationProcessorEnvironment env=null; ???????? ????????/** ???????? * 構造方法 ???????? * @param env 傳入processor環境 ???????? */ ????????public ReviewProcessor(AnnotationProcessorEnvironment env){ ????????????????this.env=env; ????????} ???????? ????????/** ???????? * 處理方法:查詢processor環境中的類型聲明, ???????? */ ????????public void process(){ ????????????????//查詢processor環境中的類型聲明 ????????????????for(TypeDeclaration type:env.getSpecifiedTypeDeclarations()){ ????????????????????????//返回對類進行掃描、訪問其聲明時使用的DeclarationVisitor, ????????????????????????//傳入參數:new ReviewDeclarationVisitor(),為掃描開始前進行的對類聲明的處理 ????????????????????????//????????DeclarationVisitors.NO_OP,表示在掃描完成時進行的對類聲明不做任何處理 ????????????????????????DeclarationVisitor visitor=DeclarationVisitors.getDeclarationScanner( ????????????????????????????????????????new ReviewDeclarationVisitor(),DeclarationVisitors.NO_OP); ????????????????????????//應用DeclarationVisitor到類型 ????????????????????????type.accept(visitor); ????????????????} ????????} }
E.ReviewDeclarationVisitor類:
清單10:
package com.bjinfotech.practice.annotation.apt; import com.sun.mirror.util.*; import com.sun.mirror.declaration.*; /** * 定義Review annotation聲明訪問者 * @author cleverpig * */ public class ReviewDeclarationVisitor extends SimpleDeclarationVisitor{ ????????/** ???????? * 定義訪問類聲明的方法:打印類聲明的全名 ???????? * @param cd 類聲明對象 ???????? */ ????????public void visitClassDeclaration(ClassDeclaration cd){ ????????????????System.out.println("獲取Class聲明:"+cd.getQualifiedName()); ????????} ???????? ????????public void visitAnnotationType
發表評論
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

評論