自定義註解之編譯時註解(RetentionPolicy.CLASS)(一)

Java註解基礎概念總結

自定義註解之編譯時註解(RetentionPolicy.CLASS)(一)

自定義註解之編譯時註解(RetentionPolicy.CLASS)(二)——JavaPoet

自定義註解之編譯時註解(RetentionPolicy.CLASS)(三)—— 常用接口介紹

說到編譯時註解(RetentionPolicy.CLASS)都要和註解處理器(Annotation Processor) 扯上關係,因爲這裏是真正體現編譯時註解價值的地方。需要注意的一點是,運行時註解(RetentionPolicy.RUNTIME)和源碼註解(RetentionPolicy.SOURCE)也可以在註解處理器進行處理,不同的註解有各自的生命週期,根據你實際使用來確定。

註解處理器(Annotation Processor)

首先來了解下什麼是註解處理器,註解處理器是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。一個註解的註解處理器,以Java代碼(或者編譯過的字節碼)作爲輸入,生成文件(通常是.java文件)作爲輸出。這些生成的Java代碼是在生成的.java文件中,所以你不能修改已經存在的Java類,例如向已有的類中添加方法。這些生成的Java文件,會同其他普通的手動編寫的Java源代碼一樣被javac編譯。

自定義註解(RetentionPolicy.CLASS)

先來定義要使用的註解,這裏建一個Java庫來專門放註解,庫名爲:annotations,和下面要創建的註解處理器分開,至於爲什麼要分開創建後面再說。註解庫指定JDK版本爲1.7,如何指定往下看。自定義註解如下:

/**
 * 編譯時註解
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String value();
}


定義的是編譯時註解,對象爲類或接口等。

定義註解處理器

下面來定義註解處理器,另外建一個Java庫工程,庫名爲:processors,記得是和存放註解的庫分開的。注意,這裏必須爲Java庫,不然會找不到javax包下的相關資源。來看下現在的目錄結構:

這裏定義一個註解處理器 MyProcessor,每一個處理器都是繼承於AbstractProcessor,並要求必須複寫 process() 方法,通常我們使用會去複寫以下4個方法:

/**
 * 每一個註解處理器類都必須有一個空的構造函數,默認不寫就行;
 */
public class MyProcessor extends AbstractProcessor {
 
    /**
     * init()方法會被註解處理工具調用,並輸入ProcessingEnviroment參數。
     * ProcessingEnviroment提供很多有用的工具類Elements, Types 和 Filer
     * @param processingEnv 提供給 processor 用來訪問工具框架的環境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }
 
    /**
     * 這相當於每個處理器的主函數main(),你在這裏寫你的掃描、評估和處理註解的代碼,以及生成Java文件。
     * 輸入參數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素
     * @param annotations   請求處理的註解類型
     * @param roundEnv  有關當前和以前的信息環境
     * @return  如果返回 true,則這些註解已聲明並且不要求後續 Processor 處理它們;
     *          如果返回 false,則這些註解未聲明並且可能要求後續 Processor 處理它們
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
 
    /**
     * 這裏必須指定,這個註解處理器是註冊給哪個註解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的註解類型的合法全稱
     * @return  註解器所支持的註解類型集合,如果沒有這樣的類型,則返回一個空集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }
 
    /**
     * 指定使用的Java版本,通常這裏返回SourceVersion.latestSupported(),默認返回SourceVersion.RELEASE_6
     * @return  使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}


上面註釋說的挺清楚了,我們需要處理的工作在 process() 方法中進行,等下給出例子。對於 getSupportedAnnotationTypes() 方法標明瞭這個註解處理器要處理哪些註解,返回的是一個Set 值,說明一個註解處理器可以處理多個註解。除了在這個方法中指定要處理的註解外,還可以通過註解的方式來指定(SourceVersion也一樣):

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.annotation.cls.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
    // ...
}


因爲兼容的原因,特別是針對Android平臺,建議使用重載 getSupportedAnnotationTypes() 和 getSupportedSourceVersion()方法代替@SupportedAnnotationTypes 和@SupportedSourceVersion
現在來添加對註解的處理,簡單的輸出一些信息即可,代碼如下:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    // roundEnv.getElementsAnnotatedWith()返回使用給定註解類型的元素
    for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
        System.out.println("------------------------------");
        // 判斷元素的類型爲Class
        if (element.getKind() == ElementKind.CLASS) {
            // 顯示轉換元素類型
            TypeElement typeElement = (TypeElement) element;
            // 輸出元素名稱
            System.out.println(typeElement.getSimpleName());
            // 輸出註解屬性值
            System.out.println(typeElement.getAnnotation(MyAnnotation.class).value());
        }
        System.out.println("------------------------------");
    }
    return false;
}


到這裏註解處理器也寫好了,下面就看怎麼運行它了。

運行註解處理器

在運行前,你需要在主項目工程中引入 annotations 和 processors 這兩個庫(引入 processors 庫不是個好做法,後面介紹更適當的方法)。這時如果你直接編譯或者運行工程的話,是看不到任何輸出信息的,這裏還要做的一步操作是指定註解處理器的所在,需要做如下操作:

1、在 processors 庫的 main 目錄下新建 resources 資源文件夾;

2、在 resources文件夾下建立 META-INF/services 目錄文件夾;

3、在 META-INF/services 目錄文件夾下創建 javax.annotation.processing.Processor 文件;

4、在 javax.annotation.processing.Processor 文件寫入註解處理器的全稱,包括包路徑;

來看下整個目錄結構:

處理完就可以使用了,我們在項目中使用 @MyAnnotation 註解:

 

@MyAnnotation("Hello Annotation")
public class MainActivity extends AppCompatActivity {
    // ...
}

到這裏我們重新編譯下工程就應該有輸出了,如果沒看到輸出則先清理下工程在編譯,如下兩個操作:


輸出信息如下:

現在註解處理器已經可以正常工作了~

當然了,上面還遺留着一個問題,我們的主項目中引用了 processors 庫,但註解處理器只在編譯處理期間需要用到,編譯處理完後就沒有實際作用了,而主項目添加了這個庫會引入很多不必要的文件,爲了處理這個問題我們需要引入個插件android-apt,它能很好地處理這個問題。

在介紹這個插件前,我想先介紹個好用的庫AutoService,這裏有個坑。


AutoService

前面在指定註解處理器的時候你會不會覺得很麻煩?那麼多步驟就爲添加一個註解處理器,不過沒關係,AutoService 可以幫你解決這個問題(和上面的方式選擇一種使用即可)。

AutoService註解處理器是Google開發的,用來生成 META-INF/services/javax.annotation.processing.Processor 文件的,你只需要在你定義的註解處理器上添加 @AutoService(Processor.class) 就可以了,簡直不能再方便了。

先給 processors 庫依賴上 AutoService,你可以直接在 AndroidStudio 工具上搜索添加,如下:

添加好以後就可以直接用了,在我們之前定義的註解處理器上使用:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    // ...
}


一句話完全搞定!這時重新Make下工程也能看到同樣的輸出信息了。但是如果你編譯生成APK時,你會發現出現錯誤了,如下:

發現文件重複了!這裏有個解決辦法是在主項目的 build.gradle 加上這麼一段:

apply plugin: 'com.android.application'
 
android {
    // ...
    packagingOptions {
        exclude 'META-INF/services/javax.annotation.processing.Processor'
    }
}

這樣就不會報錯了,這是其中的一個解決方法,還有個更好的解決方法就是用上上面提到的android-apt了,下面正式登場

android-apt

那麼什麼是android-apt呢?官網有這麼一段描述:

The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:

1、Allow to configure a compile time only annotation processor as a dependency, not including the artifact in the final APK or library
2、Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio

大體來講它有兩個作用:
能在編譯時期去依賴註解處理器並進行工作,但在生成 APK 時不會包含任何遺留的東西
能夠輔助 Android Studio 在項目的對應目錄中存放註解處理器在編譯期間生成的文件
這個就可以很好地解決上面我們遇到的問題了,來看下怎麼用。

首先在整個工程的 build.gradle 中添加如下兩段語句:

 

buildscript {
    repositories {
        jcenter()
        mavenCentral()  // add
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.2'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  // add
    }
}


在主項目(app)的 build.gradle 中也添加兩段語句:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // add
// ...
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile project(':annotations')
//    compile project(':processors')  替換爲下面
    apt project(':processors')
}


這樣就OK了,重新運行可以很好地工作了~
上面提到android-apt的作用有對編譯時期生成的文件處理,關於生成文件的功能就不得不提 JavaPoet 了,關於這方面的內容放在下個文章裏講~

自定義註解之編譯時註解(RetentionPolicy.CLASS)(二)——JavaPoet

 

 

發佈了43 篇原創文章 · 獲贊 25 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章