Java技術之註解 Annotation

註解這種語法本身很有意思,當前很多流行庫如 DaggerButterKnife等都是基於註解這種語法。
熟練使用註解,既能讓你的代碼變得簡潔易讀,動態運行時執行你想要的操作,還能幫你生成代碼,省去重複代碼寫作。
本文涉及知識點:註解的生命週期,代碼編輯時註解,編譯時註解代碼生成,運行時註解動態反射。

註解的生命週期與修飾對象

對於 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 文件時會自動丟棄。

比較常用的有 SuppressWarningsOverride

SuppressWarnings是抑制編譯器生成警告信息,比如我們調用了某個被標記爲 Deprecated 的方法,這時編譯器會發出警告,而我們又不得不使用這個方法時,就可以用@SuppressWarnings來抑制這個警告。

@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

其作用如下圖,@SuppressWarnings("deprecation") 可以把 deprecation 相關的警告給抑制掉。

281665-8bfaf4da70cf61e0.png
warning
281665-af7a66d2f0d98d4b.png
no-warning

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: AliasTest,前者是用來設置變量的別名並在運行時打印,後者是調用所有被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 修飾的方法,並逐一調用。注意兩點:

  1. 就算是 privatestatic 修飾的方法也需要調用;
  2. 在執行每個方法前後都要打印相關log,表示開始測試該方法。打印的內容要含有 valueid, 如果 @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

281665-9ffa921d5b9d214a.jpg
Android技術·面試技巧·職業感悟
發佈了45 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章