Java 註解與註解處理器基礎總結與實操步驟分析

AndroidDev2020發表於2020-12-09

Java 註解與註解處理器

從 JDK 5 開始,Java 增加了註解,註解是程式碼裡面的特殊標記,這些標記可以在編譯、類載入、執行時被讀取,並執行一些相應的處理。通過使用註解,開發人員可以在不改變原有邏輯的情況下,在原始檔中嵌入一些補充的資訊。程式碼分析工具、開發工具和部署工具可以通過這些補充資訊進行驗證、處理或者進行部署。

註解

註解可以分為標準註解和元註解。

標準註解是 JDK 自帶的註解。元註解是用來註解其他註解的註解。

標準註解

標準註解由以下 4 種:

  1. @Override 重寫註解。對覆蓋超類中的方法進行標記,如果被標記的方法沒有實際覆蓋超類中的方法,編譯器會發出警告。
  2. @Deprecated 過時註解。對不鼓勵使用或者已經過時的方法新增註解。當程式設計人員在使用這些方法時,會在編譯時顯示提示資訊。
  3. @SuppressWarnings 取消警告註解。選擇性地取消特定程式碼段中的警告,比如 lint 警告。
  4. @SafeVarargs JDK 7 新增,用來宣告使用了可變長度引數的方法,其在與泛型類一起使用時不會出現型別安全的問題。

元註解

除了標準註解,還有元註解,它用來註解其他註解,從而建立新的註解。元註解有以下幾種:

  • @Target 註解所修飾的物件範圍
  • @Inherited 表示註解可以被繼承
  • @Documented 表示這個註解應該被 JavaDoc 工具記錄
  • @Retention 用來宣告註解的保留策略
  • @Repeatable JDK 8 新增,允許一個註解在同一個宣告型別(類、屬性或者方法)上多次使用

@Target 註解取值是一個 ElementType 型別的陣列,其中有以下幾種取值,對應不同的物件範圍。

  • ElementType.TYPE 修飾類、介面或者列舉型別
  • ElementType.FIELD 修飾成員變數
  • ElementType.METHOD 修飾方法
  • ElementType.PARAMETER 修飾引數
  • ElementType.CONSTRUCTOR 修飾構造方法
  • ElementType.LOCAL_VARIABLE 修飾區域性變數
  • ElementType.ANNOTATION_TYPE 修飾註解
  • ElementType.PACKAGE 修飾包
  • ElementType.TYPE_PARAMETER 型別引數宣告
  • ElementType.TYPE_USE 使用型別

@Retention 註解有 3 種型別,分別表示不同級別的保留策略。

  • RetentionPolicy.SOURCE 原始碼級註解。註解資訊只會保留在 java 原始碼中,原始碼在編譯後註解資訊被丟棄,不會保留在 class 檔案
  • RetentionPolicy.CLASS 編譯時註解。註解資訊會保留在 java 原始碼以及 class 檔案中。當執行 java 程式時,JVM 會丟棄該註解資訊,不會保留在 JVM 中
  • RetentionPolicy.RUNTIME 執行時註解,當執行 java 程式時,JVM 也會保留該註解資訊,可以通過反射獲取該註解資訊。

定義註解

定義註解使用 @interface 關鍵字。

public @interface Swordsman {
    String name() default "zhaomin";

    int age() default 18;
}

定義註解後,可以在程式中使用該註解。

@Swordsman(name = "zhangwuji", age = 20)
public class AnnotationTest {
}

可以看出註解只有成員變數,沒有方法。註解的成員變數在註解定義中以無形參的方法表示。返回值定義了成員變數的型別。

使用註解時,可以給成員變數指定值。也可以在定義註解時使用 default 關鍵字指定預設值。如果有預設值,可以在使用註解時不為成員變數賦值。

定義執行時註解

可以用 @Retention 來設定註解的保留策略。這 3 個策略的生命週期長度為 SOURCE < CLASS < RUNTIME 。生命週期短的能起作用的地方,生命週期長的也一定能起作用。一般如果需要在執行時去動態獲取註解資訊,那麼只能用 RetentionPolicy.RUNTIME;如果要在編譯時進行一些預處理操作,比如生產一些輔助程式碼,就用 RetentionPolicy.CLASS;如果只是做一些檢查的操作,就用 RetentionPolicy.SOURCE。

@Override 註解是原始碼級別註解(RetentionPolicy.SOURCE):

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

Retrofit 的 @GET 註解是執行時註解(Retention(RUNTIME)):

@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
  String value() default "";
}

定義編譯時註解

如果將 @Retention 的保留策略設定為 RetentionPolicy.CLASS,這個註解就是編譯時註解,如下所示:

@Retention(RetentionPolicy.CLASS)
public @interface Swordsman {
    String name() default "zhaomin";

    int age() default 18;
}

註解處理器

如果沒有處理註解的工具,那麼註解也不會有太大的作用。對於不同的註解有不同的註解處理器。雖然註解處理器的編寫千變萬化,但是也有處理標準。

針對執行時註解會採用反射機制處理,針對編譯時註解會採用 AbstractProcessor 來處理。

執行時註解處理器

處理執行時註解需要用到反射機制。定義執行時註解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Get {
    String value() default "";
}

接著使用 Get 註解如下:

public class AnnotationTest {

    @Get(value = "http://ip.taobao.com/59.108.54.37")
    public String getIpMsg() {
        return "";
    }

    @Get(value = "http://ip.taobao.com/")
    public String getIp() {
        return "";
    }
}

接下來寫一個簡單的註解處理器,用來獲取 Get 註解的值。

public class AnnotationProcessor {

    public static void main(String[] args) {
        Method[] methods = AnnotationTest.class.getDeclaredMethods();
        for (Method m : methods) {
            Get get = m.getAnnotation(Get.class);
            System.out.println(get.value());
        }
    }
}

以上程式碼通過 getDeclaredMethods 獲取類的方法,通過 getAnnotation 獲取方法的註解,最後呼叫 Get 的 value 方法返回注接的成員變數的值。

輸出如下:

http://ip.taobao.com/59.108.54.37
http://ip.taobao.com/

編譯時註解處理器

定義註解

首先新建一個叫做 annotations 的 Java Library 存放註解:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value() default 1;
}

編寫註解處理器

然後新建一個叫做 processor 的 Java Library 存放註解處理器:

public class ClassProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Messager messager = processingEnv.getMessager();
        for (Element ele : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            if (ele.getKind() == ElementKind.FIELD) {
                messager.printMessage(Diagnostic.Kind.NOTE, "printMessage:" + ele.toString());
            }
        }
        return true;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
  • init 被註解處理工具呼叫,並輸入 processingEnvironment 引數。processingEnvironment 提供了很多工具類,如 Elements、Types、Filer 和 Messenger 等。
  • process 註解處理的主函式,這裡處理掃描、評估和處理註解的程式碼,以及生產 Java 檔案。
  • getSupportedAnnotationTypes 指明註解處理器是處理哪些註解的。
  • getSupportedSourceVersion 指明使用的 Java 版本,通常返回 SourceVersion.latestSupported。

在 Java 7 以後,也可以用註解的形式代替 getSupportedAnnotationTypes 和 getSupportedSourceVersion。即 @SupportedAnnotationTypes 和 @SupportedSourceVersion 註解。

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.caoshen.annotations.BindView")
public class ClassProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        ...
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }
}

並在 processor 的 build.gradle 配置依賴:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':annotations')
}

註冊註解處理器

在 processor 的 main 目錄下新建 resources 目錄,然後新增一個 META-INF/services 目錄。

在 META-INF/services 目錄下新建一個名叫 javax.annotation.processing.Processor 的檔案。

檔案內容如下:

com.caoshen.processor.ClassProcessor

ClassProcessor 用來表示之前定義的註解處理器。

auto-service

如果覺得上一步的註冊 service 步驟麻煩,可以使用 google 的 auto service 自動註冊。

在 processor 的 build.gradle 配置依賴:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':annotations')
    // auto service
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}

注意這裡的依賴要加上 annotationProcessor 這一行,不然無法生成 META-INF/services 目錄以及裡面的註解處理器。

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

然後在註解處理器類新增 @AutoService 註解如下:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.caoshen.annotations.BindView")
public class ClassProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
       ...
    }
...
}

生成的 javax.annotation.processing.Processor 檔案在以下目錄

processor\build\classes\java\main\META-INF\services

這樣就可以避免手動註冊。

應用註解

在 app 模組的 build.gradle 依賴註解和註解處理器

dependencies {
    ...
    // annotation
    implementation project(':annotations')
    annotationProcessor project(':processor')
}

在 AnnotationActivity 使用註解如下:

public class AnnotationActivity extends Activity {

    @BindView(value = R.id.tv_text)
    TextView textTv;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_annotations);
    }
}

重新 build 專案,在 Run 視窗會列印出 @BindView 註解對應的 TextView 的名稱,即 textTv。

輸出如下:

注: printMessage:textTv

參考

本文轉載自 CSND Java註解與註解處理器

相關文章