引子
最近我寫了一篇關於元件化的開源框架原始碼分析的文章(傳送門在下面兒)。那麼現在元件化小有名氣的JIMU框架,也是我下一個要給大家分享的原始碼分析文章。但因為其中涉及到了很多Java Annotation
相關的知識。所以不得不在這裡,先安利一下本篇,這也是本篇的由來。
優秀框架原始碼分析系列(一)讓解耦更輕鬆!多程式元件化框架-ModularizationArchitecture
“註解”,在Java世界裡隨處可見,但通常情況下,多數人對其是視而不見的。但當我們設計SDK,設計基礎庫的時候,運用註解,可以起到簡化配置的作用。熟悉ButterKnife
的朋友都知道,它就是通過註解來在編譯期間,增加Java程式碼來實現的。如果你還不知道它是如何實現的,那麼相信你食用完本篇和下一篇以後,就會明白這一切了。
食用路線
學習新知識的時候,要掌握正確的進食方法,腦子裡必須先對知識結構有預期,學習完之後再回顧結構,根據結構記住知識。本篇將按照導圖的結構,來進行講解。
紮實打基礎
基礎知識-四大元註解
元註解,就是用來修飾註解的註解。
@Target(value=ElementType)
@Target
被用來指明此Annotation所修飾的物件範圍(即:被描述的註解可以用在什麼地方)。Java中,註解(Annotation
)可被用於以下位置 :
- package、types(類、介面、列舉、Annotation型別)
- 型別成員(方法、構造方法、成員變數、列舉值)
- 方法引數和本地變數(如迴圈變數、catch引數)
註解的使用範圍,通過Target
的取值來指定。指定好以後,@Target
修飾的元素一定是與其取的value
相匹配的,否則編譯會報錯。
value
取值(ElementType
)常見的有:
ElementType | 含義 |
---|---|
CONSTRUCTOR | 用於描述構造器 |
FIELD | 用於描述域(包括enum常量) |
LOCAL_VARIABLE | 用於描述區域性變數 |
METHOD | 用於描述方法 |
PACKAGE | 用於描述包 |
PARAMETER | 用於描述引數 |
TYPE | 用於描述類、介面(包括註解型別) 或enum宣告 |
注:PACKAGE,它並不是使用在一般的類中,而是用在固定的檔案package-info.java中。這裡需要強調命名一定是“package-info”
這裡需要特殊說明的是,在以前的Java版本中,開發者只能將註解(Annotation)寫在宣告中。但從Java 8開始,註解可以寫在使用型別的任何地方,例如宣告、泛型和強制型別轉換等語句:
@Encrypted String str;
List<@NonNull String> strs;
test = (@Immutable Test) tmpTest;
複製程式碼
針對這個擴充,JAVA8對原有的@Target的取值做了擴充,引入了新的型別(TYPE)
註解,即ElmentType
增加了:
ElementType | 含義 |
---|---|
TYPE_PARAMETER | 表示註解可以用在型別變數的宣告語句中(如 class Test {...}) |
TYPE_USE | 表示註解可以用在使用型別的任何語句中(如宣告語句、泛型和強轉) |
關於型別的解釋參考上文。
@Retention(value=RetentionPolicy)
@Retention
,翻譯為保留,指示了一個註解被保留的時間期限,一個被其修飾的註解會被保留到其value
指定三個階段的其中之一,如果註解型別宣告中不存在Retention
註解,則保留策略預設為CLASS
:
RetentionPolicy | 含義 |
---|---|
RetentionPolicy.SOURCE | 只在原始碼級別保留,編譯時就會被忽略 |
RetentionPolicy.CLASS | 在編譯時被保留,在class檔案中存在,但JVM將會忽略 |
RetentionPolicy.RUNTIME | 被JVM保留,所以他能在執行時被JVM或其他使用反射機制的程式碼所讀取和使用 |
@Documented
被@Documented修飾的註解,用來表示這個被修飾註解應該被 javadoc工具記錄。預設情況下,javadoc是不包括註解的。但如果宣告註解時指定了@Documented,則它會被javadoc之類的工具處理,所以註解型別資訊也會被包括在生成的文件中。
@Inherited
如果一個用來修飾class的註解,希望被這個class的sub-class繼承,則可以對這個註解使用@Inherited修飾。 上面這句話強調了以下兩點:
- 註解的可繼承性。當自定義的註解希望被繼承時,就要使用@Inherited修飾
- @Inherited只在修飾class時有效,修飾其他型別時無效
自定義註解如何寫-Java Annotation的語法
自定義註解,通過@符號和interface關鍵字來定義。類似於class的寫法,例如:
package com.xm.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno{
String name();
String website() default "example.com";
int version() default 1;
}
複製程式碼
當註解需要引數的時候,我們需要先定義註解的方法,方法名即引數名。這裡有點類似於介面的定義,如果引數需要預設值,則在方法後加default + 預設值
的方式來實現。
例如本例中,我們指定了三個引數,name、website、version
。分別定義了三個方法,name
沒有指定預設值,所以它預設為null,website
指定了預設值為example.com
,而version
則指定了1為其預設值。
這裡有三點規則強調一下:
- 註解方法不帶引數,比如
name()
,website()
; - 註解方法返回值型別:基本型別、String、Enums、Annotation以及前面這些型別的陣列型別
- 註解方法可有預設值,比如
default "example.com"
,預設website=”example.com”
當我們在使用時,需要給引數傳遞值,很簡單:
@TestAnno(name="xiaoming", website="example.com", version=1)
複製程式碼
後文對這個例子中的其他部分,還會有詳細的解釋。
高效學精髓
為了方便學習,我們拿最常見的Java內建的註解@Override
來食用。把握一下自定義註解實現時的幾個步驟。
1. 編寫定義註解的Java檔案
要自定義註解,首先要建立一個以註解名字命名的Java檔案,並使用@interface關鍵字來定義註解
Override.java
package java.lang
public @interface Override {
}
複製程式碼
2. 確定自定義註解的作用範圍
這個也很容易理解,Override註解用來修飾方法,所以作用範圍就指定為METHOD。
3. 確定自定義註解的作用時限
SOURCE、CLASS、RUNTIME的保留時限依次增加。而對於Override來說,我們日常編寫程式碼時,通過這個註解可以知道哪些方法是被重寫的。這個提示,也僅僅停留在了原始碼層面,所以這裡使用SOURCE。
4. 確定自定義註解的引數、方法
Override並沒有使用任何的引數和方法,這裡也忽略了,後面我們實戰例子裡會重點介紹。
基於上述分析,我們寫出瞭如下的定義原始碼:
package java.lang;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
複製程式碼
通過以上的三個步驟,我們就定義好了一個註解。當我們使用時,直接通過@符號加註解名稱即可。那麼,加了註解的元素,有什麼用呢?
其一,我們可以通過編譯期處理,為註解所在的元素自動生成其他程式碼。像ButterKnife
等均是如此,它幫助我們生成了View與元件的繫結,實現了點選監聽器的繫結等等。保留到此時限的註解,我們也稱其為編譯期註解。
其二,我們在執行時,可以通過反射,獲取到註解資訊,這些註解資訊,往往是程式編寫者希望傳遞給執行時使用的一些資訊或配置,可以起到簡化配置的作用。像Spring2.5
以後,運用了大量的執行時註解,它在實現AOP方面,有著廣泛的應用。需要強調的是,這裡的註解,都是RUNTIME
規則的,只有這樣才能保留到執行時。所以我們稱其為執行時註解。
本篇我們重點介紹程式碼編寫階段的提示性註解和執行時註解。下面我們再通過兩個實際例子來理解一下。
大膽練實戰(一)
提示性註解示例
我們在java中,除了Override
,還會經常見到一些其他的內建註解。例如SuppressWarnings
(壓制警告),他用於告知編譯器忽略特定的警告資訊,如在泛型中使用原始資料型別。
package java.lang;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target( { ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,
ElementType.PARAMETER, ElementType.CONSTRUCTOR,
ElementType.LOCAL_VARIABLE })
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* The list of warnings a compiler should not issue.
*/
public String[] value();
}
複製程式碼
按我們上節所需編寫自定義註解的幾個步驟,來分別分析一下:
註解的作用範圍
SuppressWarnings
可用於除註解型別宣告和包名之外的所有元素。所以這裡的Target
做了相應的指定。
註解的作用時限
這裡的警告,都是靜態語法檢查型別的警告資訊,所以這個註解也只需要保留在原始碼層面。即SOURCE
。
註解的引數方法
SuppressWarning
指定了一個String
型別的陣列。它支援了多個字串引數。其可取值為需要壓制的警告型別,見下表:
引數 | 含義 |
---|---|
deprecation | 使用了過時的類或方法時的警告 |
unchecked | 執行了未檢查的轉換時的警告 |
fallthrough | 當Switch程式塊進入進入下一個case而沒有Break時的警告 |
path | 在類路徑、原始檔路徑等有不存在路徑時的警告 |
serial | 當可序列化的類缺少serialVersionUID定義時的警告 |
finally | 任意finally子句不能正常完成時的警告 |
all | 以上所有情況的警告 |
使用時,可按如下的方法賦值:
@SupressWarning(value={"uncheck","deprecation"})
複製程式碼
執行時註解示例
針對我們上一節中自定義的註解TestAnno,我們來實踐一下執行時註解。
準備工作
package com.xm.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno{
String name();
String website() default "example.com";
int version() default 1;
}
複製程式碼
假設我們在如下的程式碼塊中應用了此註解:
package com.xm.annotation;
public class AnnotationDemo {
@AuthorAnno(name="xiaoming", website="example.com", version=1)
public static void main(String[] args) {
System.out.println("I am main method");
}
@SuppressWarnings({ "unchecked", "deprecation" })
@AuthorAnno(name="suby", website="example2.com", version=2)
public void demo(){
System.out.println("I am demo method");
}
}
複製程式碼
針對註解解析
現在,我們在執行時,通過反射來解析自定義的註解@TestAnno,關於反射類位於包java.lang.reflect,其中有一個介面AnnotatedElement,該介面定義了註釋相關的幾個核心方法,如下:
返回值 | 方法 | 含義 |
---|---|---|
T | getAnnotation(Class annotationClass) | 當存在該元素的指定型別註解,則返回相應註釋,否則返回null |
Annotation[] | getAnnotations() | 返回此元素上存在的所有註解 |
Annotation[] | getDeclaredAnnotations() | 返回直接存在於此元素上的所有註解 |
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 當存在該元素的指定型別註解,則返回true,否則返回false |
前面我們自定義的註解,適用物件為Method。類Method繼承類AccessibleObject,而類AccessibleObject實現了AnnotatedElement介面,那麼可以利用上面的反射方法,來實現解析@TestAnno的功能(AnnotationParser.java),內容如下:
package com.xm.annotation;
import java.lang.reflect.Method;
public class AnnotationParser {
public static void main(String[] args) throws SecurityException, ClassNotFoundException {
String clazz = "com.xm.annotation.AnnotationDemo";
Method[] demoMethod = AnnotationParser.class
.getClassLoader().loadClass(clazz).getMethods();
for (Method method : demoMethod) {
if (method.isAnnotationPresent(TestAnno.class)) {
AuthorAnno authorInfo = method.getAnnotation(TestAnno.class);
System.out.println("method: "+ method);
System.out.println("name= "+ authorInfo.name() +
" , website= "+ authorInfo.website()
+ " , version= "+authorInfo.version());
}
}
}
}
複製程式碼
程式的輸出結果:
method: public void com.xm.annotation.AnnotationDemo.demo()
name= suby , website= example2.com , version= 2
method: public static void com.xm.annotation.AnnotationDemo.main(java.lang.String[])
name= xiaoming , website= example.com , version= 1
複製程式碼
由此可見,我們可以通過在編寫程式碼時,將一些執行時需要的資訊,通過註解的方式傳遞給執行時的程式碼,以達到資訊傳遞和配置的目的。在Spring中,也大量採用了執行時註解,為程式的配置,以及程式開發,特別是AOP程式設計,都提供了極大的便利。
下一篇重點介紹編譯期註解,在眾多的知名框架或工具型SDK中,都能看見它的身影,它可以以一種極度優雅簡潔的方式,為我們提供開發上的便捷。
小銘出品,必屬精品
歡迎關注xNPE技術論壇,更多原創乾貨每日推送。