註解
這種語法本身很有意思,當前很多流行庫如Dagger
、ButterKnife
等都是基於註解這種語法。
熟練使用註解
,既能讓你的代碼變得簡潔易讀,動態運行時執行你想要的操作,還能幫你生成代碼,省去重複代碼寫作。
本文涉及知識點:註解的生命週期,代碼編輯時註解,編譯時註解代碼生成,運行時註解動態反射。
註解的生命週期與修飾對象
對於 Java 代碼從編寫到運行有三個時期:代碼編輯;編譯成 .class
文件;讀取到 JVM 運行。針對這三個時期有三種 Annotation
對應:
RetentionPolicy.SOURCE // 只在代碼編輯期生效
RetentionPolicy.CLASS // 在編譯期生效,默認值
RetentionPolicy.RUNTIME // 在代碼運行時生效
除了生命週期,我們還可以指定 Annotation
用來指定的對象,比如修飾方法、類、變量、參數等,例如:
@Annotation
public void getName() {}
@Annotation
String name;
public void setName(@Annotation String name) {}
Java 提供了 @Target
這個元註解
來指定某個 Annotation
修飾的目標對象。
例如 @Override
是用來修飾方法的。
@Target(ElementType.METHOD)
public @interface Override {
}
而 @SuppressWarnings
可以用來修飾很多,包括類、方法、變量等等。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
public @interface SuppressWarnings {
String[] value();
}
下面我們依次來看看不同生命週期的三類 Annotation。
1. 代碼編輯時註解
這種 Annotation 只存在於代碼編輯階段(RetentionPolicy.SOURCE),主要功能是讓 IDE 來爲開發者提供 warning 檢查。這一類註解只會在編輯代碼時生效,當編譯器把 .java 文件編譯成 .class 文件時會自動丟棄。
比較常用的有 SuppressWarnings
,Override
。
SuppressWarnings
是抑制編譯器生成警告信息,比如我們調用了某個被標記爲 Deprecated
的方法,這時編譯器會發出警告,而我們又不得不使用這個方法時,就可以用@SuppressWarnings
來抑制這個警告。
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
其作用如下圖,@SuppressWarnings("deprecation")
可以把 deprecation
相關的警告給抑制掉。
Override
用來標記重寫父類某個方法,萬一不小心寫錯方法名或者父類該方法發生改動,IDE 就會發出警告。
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
當然,對於開發者而言,這一類的 Annotation 我們很少自定義,更重要的是學會使用。其實 Java 和 Android 裏提供了非常多有用的靜態檢查的 Annotation,有助於提高代碼的正確率,省去人工的代碼檢查,方便代碼給他人使用。
之後的文章我會具體介紹 Android 內部[support-annotations
]很多有趣有用的 Annotation。
2. 運行時註解
這一類註解是開發者廣泛使用的。基本原理是利用反射機制
在代碼運行過程中動態地執行一些操作。關於 反射機制
我已經在之前的文章Java 技術之反射中細緻闡述過了,不熟悉的讀者可以去閱讀。
下面我們以兩個例子來進行講解。還是利用我們常用的 UserBean
對象爲目標,對它內部的一些 Annotation
進行運行時處理。
public class UserBean {
@Alias("user_name")
public String userName;
@Alias("user_id")
private long userId;
public UserBean(String userName, long userId) {
this.userName = userName;
this.userId = userId;
}
public String getName() {
return userName;
}
public long getId() {
return userId;
}
@Test(value = "static_method", id = 1)
public static void staticMethod() {
System.out.printf("I'm a static method\n");
}
@Test(value = "public_method", id = 2)
public void publicMethod() {
System.out.println("I'm a public method\n");
}
@Test(value = "private_method", id = 3)
private void privateMethod() {
System.out.println("I'm a private method\n");
}
@Test(id = 4)
public void testFailure() {
throw new RuntimeException("Test failure");
}
}
這次我在 UserBean
裏面創建了兩個自定義的 Annotation
: Alias
和 Test
,前者是用來設置變量的別名並在運行時打印,後者是調用所有被Test
標記的方法,得出測試通過率。當然,這兩個功能目前完全沒有實現,只是標記了一下而已。下面我們依次來實現這兩個Annotation的功能。
Alias 功能實現:設置變量別名並在運行時打印別名
首先,我們要創建 Alias
這個註解。那麼要明確三個方面:
- 生命週期是什麼?
- 針對的目標是什麼類型?
- 內部是否有參數?
針對 Alias
的功能,我們可以作如下回答:
- 生命週期是
運行時
,因爲要動態打印出變量的別名 -> @Retention(RetentionPolicy.RUNTIME) - 針對的目標是
變量
-> @Target(ElementType.FIELD) - 內部需要維護一個 String 型的變量來保存別名 -> String value();
因此可以得到
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Alias {
String value();
}
Alias
定義得到了,接下來我們要實現它的功能了,即在運行時取出變量的別名並打印。
代碼如下:
/**
* print alias during runtime
*/
private static void printAlias(Object userBeanObject) {
for (Field field : userBeanObject.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Alias.class)) {
Alias alias = field.getAnnotation(Alias.class);
System.out.println(alias.value());
}
}
}
過程很簡單,利用反射機制,把 userBeanObject
對應 Class 裏所有的成員變量都找到,找出其中被 Alias
修飾的成員變量,然後把真實註解 Alias
對象取出來,把內部的 value
打印出來即可。
Test
功能實現:調用所有被Test
標記的方法,得出測試通過率
同樣我們要回答上面三個問題:生命週期,針對對象類型和內部參數,回答是:
- 生命週期:由於是
動態運行時
去遍歷這些Test
的存在,因此是RUNTIME
; - 針對對象類型:因爲是修飾
方法
的,因此是@Target(ElementType.METHOD)
; - 內部參數:由於需要記錄方法的
名稱
和對應的id
,因此需要String value();
int id;
得到如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
String value() default ""; // 如果沒有設置,那麼直接取函數方法名
int id();
}
接下來我們需要找到所有被 @Test
修飾的方法,並逐一調用。注意兩點:
- 就算是
private
和static
修飾的方法也需要調用; - 在執行每個方法前後都要打印相關log,表示開始測試該方法。打印的內容要含有
value
和id
, 如果@Test
裏的value
沒有設置值,那麼就取函數名爲值。
可以看出,在 UserBean
裏已經定義好了三個被 @Test
修飾的方法了。
@Test(value = "static_method", id = 1)
public static void staticMethod() {
System.out.printf("I'm a static method\n");
}
@Test(value = "public_method", id = 2)
public void publicMethod() {
System.out.println("I'm a public method\n");
}
@Test(value = "private_method", id = 3)
private void privateMethod() {
System.out.println("I'm a private method\n");
}
@Test(id = 4)
public void testFailure() {
throw new RuntimeException("Test failure");
}
接下來我們實現 @Test
的具體功能:
/**
* Test methods which are be annotated with @Test
*/
private static void doTest(Object object) {
Method[] methods = object.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Test.class)) {
Test test = method.getAnnotation(Test.class);
try {
String methodName = test.value().length() == 0 ? method.getName() : test.value(); // if test.value() is empty, use `method.getName()`
System.out.printf("Testing. methodName: %s, id: %s\n", methodName, test.id());
if (Modifier.isStatic(method.getModifiers())) {
method.invoke(null); // static method
} else if (Modifier.isPrivate(method.getModifiers())) {
method.setAccessible(true); // private method
method.invoke(object);
} else {
method.invoke(object); // public method
}
System.out.printf("PASS: Method id: %s\n", test.id());
} catch (Exception e) {
System.out.printf("FAIL: Method id: %s\n", test.id());
e.printStackTrace();
}
}
}
}
打印結果如下:
Testing. methodName: static_method, id: 1
I'm a static method
PASS: Method id: 1
Testing. methodName: public_method, id: 2
I'm a public method
PASS: Method id: 2
Testing. methodName: private_method, id: 3
I'm a private method
PASS: Method id: 3
Testing. methodName: testFailure, id: 4
FAIL: Method id: 4
全部正常打印。其中,由於 testFailure()
的 @Test
裏未設置 value()
,因此直接打印了它的函數名;針對static
方法,直接調用method.invoke(null)
;針對private
,利用method.setAccessible(true);
獲取了權限。
當然這裏有一點要注意,我在 invoke method
時,直接使用 method.invoke(object);
,沒有傳任何參數。這是由於我在 UserBean
裏寫的幾個方法都不用傳參數。如果需要傳參數的話,那就還需要再單獨判斷是哪個函數,並傳遞對應的參數進去。
3. 編譯時註解
當 .java 文件寫好了準備進行編譯時,我們有另一種 Annotation 可以在這時發揮效果。
我們知道,Java 源代碼編譯的過程會對所有的文件進行掃描,而編譯時 Annotation
的作用就是在編譯過程中生成代碼
。在 Java 裏提供了 apt 工具來處理註解,同時有一套 Mirror API 來描述編譯時的程序語義結構,它可以在編譯時獲取到被註解 Java 元素的信息以方便我們處理。該處理過程的核心是編寫註解處理器 AnnotationProcessor
接口。
大概說明下整體的過程。
假設我們希望用某個 Annotation
:@Inject
來生成一些代碼,那麼我們要做下面幾步:
定義 @Inject
由於它是編譯時註解,因此是 RetentionPolicy.CLASS
,對象的話就是變量和構造函數,因此得到:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD})
public @interface Inject {
}
定義 Processor 類
// Helper to define the Processor
@AutoService(Processor.class)
// Define the supported Java source code version
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// Define which annotation you want to process
@SupportedAnnotationTypes("com.wingjay.annotation.Inject")
public class MyProcessor extends AbstractProcessor { ... }
重寫 Processor
內部的 process
方法
這個 process
方法會在編譯時被執行到,我們可以在這個方法裏進行代碼生成的工作。
關於具體的代碼生成部分,可以利用 Square
提供的 JavaPoet
工具:https://github.com/square/javapoet
下面有一些代碼片段供參考:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//掃描所有 Inject 標記過的元素
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Inject.class);
Set<? extends TypeElement> typeElements = ElementFilter.typesIn(elements);
for (TypeElement element : typeElements) {
// 拼接待生成代碼
ClassName currentType = ClassName.get(element);
MethodSpec.Builder builder = MethodSpec.methodBuilder("fromCursor")
.returns(currentType)
.addModifiers(Modifier.STATIC)
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get("android.database", "Cursor"), "cursor");
// 將這些拼接代碼寫入文件
String className = ... // 設置你要生成的代碼class名字
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(className, element);
Writer writer = sourceFile.openWriter();
javaFile.writeTo(writer);
writer.close();
}
return false;
}
小結
本文在前文Java 技術之反射的基礎上,對 Java 的註解 Annotation
做了一定的介紹。
熟練使用 Annotation
能在很多時候幫助代碼變得更簡潔,也能幫我們生成很多代碼,免除重複寫作相同代碼,是一種很高效的編程方式。
下一篇我會爲 Android 的小夥伴介紹不少你可能不太知道但非常好用的 Android Annotation
。
謝謝。
wingjay