天天看點

Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)

注解處理器(Annotation Processor)

注解處理器是javac的一個工具,它用來在編譯時掃描和處理注解(Annotation)。你可以自定義注解,并注冊到相應的注解處理器,由注解處理器來處理你的注解。一個注解的注解處理器,以Java代碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。這些生成的Java代碼是在生成的.java檔案中,是以你不能修改已經存在的Java類,例如向已有的類中添加方法。這些生成的Java檔案,會同其他普通的手動編寫的Java源代碼一樣被javac編譯。

RetentionPolicy.CLASS:注解被保留到class檔案,但jvm加載class檔案時候被遺棄,這是預設的生命周期;

1. 建立兩個Module

File->New->New Module...->Java Library,填寫好Library name和Java class name後點選完成。

注意,這裡必須為Java庫,不然會找不到javax包下的相關資源。

Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)

圖1.png

其中annations是存放注解的,processors是存放注解處理器的。

2. 建立編譯時注解

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

3. 定義注解處理器

定義一個注解處理器 CustomProcessor ,每一個處理器都是繼承于AbstractProcessor,并要求必須複寫 process() 方法,通常我們使用複寫以下4個方法:

/**
 * 每一個注解處理器類都必須有一個空的構造函數,預設不寫就行
 */
public class CustomProcessor extends AbstractProcessor{
    /**
     * init()方法會被注解處理器工具調用,并輸入ProcessingEnvironment參數。
     * ProcessingEnvironment 提供很多有用的工具類Elements,Types和Filter
     * @param processingEnvironment 提供給process用來通路工具架構的環境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /**
     * 相當于每個處理器的主函數main(),在這裡寫掃描、評估和處理注解的代碼,以及生成java檔案。
     * 輸入參數RoundEnvironment可以查詢出包含特定注解的被注解元素
     * @param set 請求處理注解類型
     * @param roundEnvironment 有關目前和以前的資訊環境
     * @return 傳回true,則這些注解已聲明并且不要求後續Processor處理他們;
     *          傳回false,則這些注解未聲明并且可能要求後續Processor處理他們;
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    /**
     * 這裡必須指定,這個注解處理器是注冊給那個注解的。
     * 注意:它的傳回值是一個字元串的集合,包含本處理器想要處理注解的注解類型的合法全程。
     * @return 注解器所支援的注解類型集合,如果沒有這樣的類型,則傳回一個空集合。
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(CustomAnnation.class.getCanonicalName());
        return annotations;
    }

    /**
     * 指定Java版本,通常這裡使用SourceVersion.latestSupported(),
     * 預設傳回SourceVersion.RELEASE_6
     * @return 使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
           

也可以使用注解的方式來指定Java版本和注解類型

Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)

圖2.png

4. 添加注解的處理

/**
     * 相當于每個處理器的主函數main(),在這裡寫掃描、評估和處理注解的代碼,以及生成java檔案。
     * 輸入參數RoundEnvironment可以查詢出包含特定注解的被注解元素
     * @param set 請求處理注解類型
     * @param roundEnvironment 有關目前和以前的資訊環境
     * @return 傳回true,則這些注解已聲明并且不要求後續Processor處理他們;
     *          傳回false,則這些注解未聲明并且可能要求後續Processor處理他們;
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)傳回使用給定的注解類型的元素
        for (Element element : roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)) {
            System.out.println("----------------------------------");
            // 判斷元素的類型為Class
            if (element.getKind() == ElementKind.CLASS) {
                // 顯示轉換元素類型
                TypeElement typeElement = (TypeElement) element;
                // 輸出元素名稱
                System.out.println(typeElement.getSimpleName());
                // 輸出注解屬性值
                System.out.println(typeElement.getAnnotation(CustomAnnotation.class).value());
            }
            System.out.println("----------------------------------");
        }
        return false;
    }
           

5. 注解處理器配置

1、在 processors 庫的 main 目錄下建立 resources 資源檔案夾;
2、在 resources檔案夾下建立 META-INF/services 目錄檔案夾;
3、在 META-INF/services 目錄檔案夾下建立 javax.annotation.processing.Processor 檔案;
4、在 javax.annotation.processing.Processor 檔案寫入注解處理器的全稱,包括包路徑;
           

項目結構:

Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)

圖3.png

6. 使用

主Module依賴annotations和processors兩個moudle,然後編譯,如果未列印出結果,則Build->ReBuild或者Build->Clean Project->Make Project.

@CustomAnnotation("Hello World")
public class MainActivity extends AppCompatActivity {

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

7. 列印結果

Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)

圖4.png

8. 存在的問題

我們的主項目中引用了 processors 庫,但注解處理器隻在編譯處理期間需要用到,編譯處理完後就沒有實際作用了,而主項目添加了這個庫會引入很多不必要的檔案,為了處理這個問題我們需要引入個插件

android-apt

,它能很好地處理這個問題。

9. AutoService

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

  • 依賴AutoService庫,在processors的build.gradle檔案中添加一下依賴,并同步工程。
compile 'com.google.auto.service:auto-service:1.0-rc2'
           
  • 使用
    Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)
    圖5.png

10. 問題

這時重新Make下工程也能看到同樣的輸出資訊了。但是如果你編譯生成APK時,可能會出現檔案重複的問題。解決辦法是在主項目的 build.gradle 加上這麼一段:

packagingOptions {  
     exclude 'META-INF/services/javax.annotation.processing.Processor'  
  }  
           

這樣就不會報錯了,這是其中的一個解決方法,還有個更好的解決方法就是用上面提到的

11. 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 在項目的對應目錄中存放注解處理器在編譯期間生成的檔案

12. 使用

// 主工程的build.gradle
buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        // replace with the current version of the Android plugin
        classpath 'com.android.tools.build:gradle:1.3.0'
        // the latest version of the android-apt plugin
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
// 主Module的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'


dependencies {
// ...
    compile project(':annotations')
    apt project(':processors')
//    compile project(':processors')
//    compile project(':processors1')
}
           

13. JavaPoet

在使用編譯時注解時,需要在編譯期間對注解進行處理,在這裡我們沒辦法影響程式的運作邏輯,但我們可以進行一些需處理,比如生成一些功能性代碼來輔助程式的開發,最常見的是生成.java 源檔案,并在程式中可以調用到生成的檔案。這樣我們就可以用注解來幫助我們處理一些固定邏輯的重複性代碼(如

butterknife

),提高開發的效率。

通過注解處理器來生成 .java 源檔案基本上都會使用

javapoet

這個庫,JavaPoet一個是用于産生 .java 源檔案的輔助庫,它可以很友善地幫助我們生成需要的.java 源檔案,下面來看下具體使用方法。

使用如下代碼進行依賴:

compile "com.squareup:javapoet:1.9.0"
           

14. 注解生成器

/**
 * 每一個注解處理器類都必須有一個空的構造函數,預設不寫就行
 */
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.mazaiting.CustomAnnotation")
public class CustomProcessor extends AbstractProcessor{

    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // Filter是個接口,至此吃通過注解處理器建立新檔案
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element element : roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)) {
            // 判斷元素的類型為Class
            if (element.getKind() == ElementKind.CLASS) {
                // 顯示轉換元素類型
                TypeElement typeElement = (TypeElement) element;
                // 輸出元素名稱
//                System.out.println(typeElement.getSimpleName());
//                // 輸出注解屬性值
//                System.out.println(typeElement.getAnnotation(CustomAnnotation.class).value());
                // 建立main方法
                MethodSpec main =
                        MethodSpec.methodBuilder("main")
                                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                                .returns(void.class)
                                .addParameter(String[].class, "args")
                                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                                .build();
                String value = element.getAnnotation(CustomAnnotation.class).value();
                String first = value.substring(0, 1);
                String className = first.toUpperCase() + value.substring(1, value.length());
                TypeSpec valueClass =
                        TypeSpec.classBuilder(className)
                                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                                .addMethod(main)
                                .build();
                // 生成檔案
                try {
                    // 生成com.mazaiting.xxx.java
                    JavaFile javaFile =
                            JavaFile.builder("com.mazaiting.example", valueClass)
                                    .addFileComment("This codes are generated automatically. Do not modify!")
                                    .build();
                    javaFile.writeTo(mFiler);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }
}
           

15. MainActivity使用

@CustomAnnotation("HelloWorld")
public class MainActivity extends AppCompatActivity {

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

           

16. 生成Java類

生成的Java類結構

Android 自定義注解之編譯時注解(RetenttionPolicy.CLASS)

圖6.png

繼續閱讀