Android 註解系列之APT工具(三)

該文章中涉及的代碼,我已經提交到GitHub上了,你們按需下載---->源碼html

前言

在上篇文章Android 註解系列之Annotation(二)中,簡要的介紹了註解的基本使用與定義。同時也提出瞭如下幾個問題,當咱們聲明瞭一個註解後,是否是須要手動找到全部的Class對象或Field、Method?怎麼經過註解生成新的類的定義呢?當面對這些問題的時候,我相信你們的第一反應確定會想,"有不有相應的三方庫呢?Java是否提供了相應庫或者方法來解決呢?",固然Java確定給咱們提供了啦,就是咱們既陌生又熟悉的APT工具啦。java

爲何這裏我會說既陌生又熟悉呢?我相信對於大多數安卓程序,咱們都或多或少使用了一些主流庫,如Dagger二、ButterKnife、EventBus等,這些庫都使用了APT技術。既然大佬們都在使用,那咱們怎麼不去了解呢?好了,書歸正傳,下面咱們就來看看怎麼經過APT來處理以前咱們提到的問題。android

APT技術簡介

在具體瞭解APT技術以前,先簡單的對其進行介紹。APT(Annotation Processing Tool)是javac中提供的一種編譯時掃描和處理註解的工具,它會對源代碼文件進行檢查,並找出其中的註解,而後根據用戶自定義的註解處理方法進行額外的處理。APT工具不只能解析註解,還能根據註解生成其餘的源文件,最終將生成的新的源文件與原來的源文件共同編譯(注意:APT並不能對源文件進行修改操做,只能生成新的文件,例如在已有的類中添加方法)。具體流程圖以下圖所示:git

apt使用流程圖.png

APT技術使用規則

APT技術的使用,須要咱們遵照必定的規則。你們先看一下整個APT項目項目構建的一個規則圖,具體以下所示: github

apt_rule.png

APT使用依賴

從圖中咱們能夠整個APT項目的構建須要三個部分:數據庫

  • 註解處理器庫(包含咱們的註解處理器)
  • 註解聲明庫(用於存儲聲明的註解)
  • 實際使用APT的Android/Java項目

且三個部分的依賴關係爲註解處理工具依賴註解聲明庫Android/Java項目同時依賴註解處理工具庫與註解聲明庫api

爲何把註解處理器獨立抽成一個庫呢?

對於Android項目默認是不包含 APT相關類的。因此要使用APT技術,那麼就必須建立一個Java Library。對於Java項目,獨立抽成一個庫,更容易維護與擴展。數組

爲何把註解聲明也單獨抽成一個庫,而不放到註解處理工具庫中呢?

舉個例子,若是註解聲明與註解處理器爲同一個庫,若是有開發者但願把咱們的註解處理器用於他的項目中,那麼就必須包含註解聲明與整個註解處理器的代碼,咱們能很是肯定是,他並不但願已經編譯好的項目中包含處理器相關的代碼。他僅僅但願使用咱們的註解。因此將註解處理器與註解分開單獨抽成一個庫時很是有意義的。接下來的文章中會具體會描述有哪些方法能夠將咱們的註解處理器不打包在咱們的實際項目中。bash

註解處理器的聲明

在瞭解了ATP的使用規則後,如今咱們再來看看怎麼聲明一個註解處理器,每個註解處理器都須要承AbstractProcessor類,具體代碼以下所示:oracle

class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {}
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }
}
複製代碼
  • init(ProcessingEnvironment processingEnv):每一個註解處理器被初始化的時候都會被調用,該方法會被傳入ProcessingEnvironment 參數。ProcessingEnvironment 能提供不少有用的工具類,Elements、Types和Filer。後面咱們將會看到詳細的內容。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):註解處理器實際處理方法,通常要求子類實現該抽象方法,你能夠在在這裏寫你的掃描與處理註解的代碼,以及生成Java文件。其中參數RoundEnvironment ,可讓你查詢出包含特定註解的被註解元素,後面咱們會看到詳細的內容。
  • getSupportedAnnotationTypes(): 返回當前註解處理器處理註解的類型,返回值爲一個字符串的集合。其中字符串爲處理器須要處理的註解的合法全稱
  • getSupportedSourceVersion():用來指定你使用的Java版本,一般這裏返回SourceVersion.latestSupported()。若是你有足夠的理由指定某個Java版本的話,你能夠返回SourceVersion.RELAEASE_XX。可是仍是推薦使用前者。

在Java1.6版本中提供了SupportedAnnotationTypesSupportedSourceVersion兩個註解來替代getSupportedSourceVersiongetSupportedAnnotationTypes兩個方法,也就是這樣:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"合法註解的名稱"})
class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
    
}

複製代碼

這裏須要注意的是以上提到的兩個註解是JAVA 1.6新增的,因此出於兼容性的考慮,建議仍是直接重寫getSupportedSourceVersion()getSupportedAnnotationTypes()方法。

註冊註解處理器

到了如今咱們基本瞭解了處理器聲明,如今咱們可能會有個疑問,怎麼樣將註解處理器註冊到Java編譯器中去呢?你必須提供一個.jar文件,就像其餘.jar文件同樣,你須要打包你的註解處理器到此文件中,而且在你的jar中,你須要打包一個特定的文件javax.annotation.processing.ProcessorMETA-INF/services路徑下。就像下面這樣:

META-INF/services 至關於一個信息包,目錄中的文件和目錄得到Java平臺的承認與解釋用來配置應用程序、擴展程序、類加載器和服務文件,在jar打包時自動生成

放入特定文件夾.png

其中javax.annotation.processing.Processor文件中的內容爲每一個註解處理器的合法的全名列表,每個元素換行分割,也就是相似下面這樣:

com.jennifer.andy.processor.MineProcessor1
com.jennifer.andy.processor.MineProcessor2
com.jennifer.andy.processor.MineProcessor3
複製代碼

最後咱們只要將你生成的.jar放到你的buildPath中,那麼Java編譯器會自動的檢查和讀取javax.annotation.processing.Processor中的內容,並註冊該註解處理器。

固然對於如今咱們的編譯器,如IDEA、AndroidStudio等中,咱們只建立相應文件與文件夾就好了,並不一樣用放在buildPath中去。固然緣由是這些編譯器都幫咱們處理了啦。若是你仍是嫌麻煩,那咱們可使用Google爲咱們提供的AutoService 註解處理器,用於生成META-INF/services/javax.annotation.processing.Processor文件的。也就是咱們能夠像下面這樣使用:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"合法註解的名稱"})
@AutoService(Processor.class)
class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}
複製代碼

咱們只須要在類上聲明@AutoService(Processor.class),那麼就不用考慮其餘的東西啦。是否是很方便呢?(固然使用AutoService在Gralde中你須要添加依賴compile 'com.google.auto.service:auto-service:1.0-rc2')。

註解處理器的掃描

在註解處理過程當中,咱們須要掃描全部的Java源文件,源代碼的每個部分都是一個特定類型的Element,也就是說Element表明源文件中的元素,例如包、類、字段、方法等。總體的關係以下圖所示:

element繼承關係.png

  • Parameterizable:表示混合類型的元素(不只只有一種類型的Element)
  • TypeParameterElement:帶有泛型參數的類、接口、方法或者構造器。
  • VariableElement:表示字段、常量、方法或構造函數。參數、局部變量、資源變量或異常參數。
  • QualifiedNameable:具備限定名稱的元素
  • ExecutableElement:表示類或接口的方法、構造函數或初始化器(靜態或實例),包括註釋類型元素。
  • TypeElement :表示類和接口
  • PackageElement:表示包

那接下來咱們經過下面的例子來具體的分析:

package com.jennifer.andy.aptdemo.domain;//PackageElement
class Person {//TypeElement 
    private String where;//VariableElement
    
    public void doSomething() { }//ExecutableElement
    
    public void run() {//ExecutableElement
        int runTime;//VariableElement
    }
}
複製代碼

經過上述例子咱們能夠看出,APT對整個源文件的掃描。有點相似於咱們解析XML文件(這種結構化文本同樣)。

既然在掃描的時候,源文件是一種結構化的數據,那麼咱們能不能獲取一個元素的父元素和子元素呢?。固然是能夠的啦,舉例來講,假如咱們有個public class Person的TypeElement元素,那麼咱們能夠遍歷它的全部的孩子元素。

TypeElement person= ... ;  
for (Element e : person.getEnclosedElements()){ // 遍歷它的孩子 
    Element parent = e.getEnclosingElement();  // 拿到孩子元素的最近的父元素
}
複製代碼

其中getEnclosedElements()getEnclosingElement()Element中接口的聲明,想了解更多的內容,你們能夠查看一下源碼。

元素種類判斷

如今咱們已經瞭解了Element元素的分類,可是咱們發現Element有時會表明多種元素。例如TypeElement表明類或接口,那有什麼方法具體區別呢?咱們繼續看下面的例子:

public class SpiltElementProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
	    //這裏經過獲取全部包含Who註解的元素set集合
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {//若是元素是類

            } else if (element.getKind() == ElementKind.INTERFACE) {//若是當前元素是接口

            }
        }
        return false;
    }
	...省略部分代碼
}
複製代碼

在上述例子中,咱們經過roundEnvironment.getElementsAnnotatedWith(Who.class)獲取源文件中全部包含@Who註解的元素,經過調用element.getKind()具體判斷當前元素種類,其中具體元素類型爲ElementKind枚舉類型ElementKind枚舉聲明以下表所示:

枚舉類型 種類
PACKAGE
ENUM 枚舉
CLASS
ANNOTATION_TYPE 註解
INTERFACE 接口
ENUM_CONSTANT 枚舉常量
FIELD 字段
PARAMETER 參數
LOCAL_VARIABLE 本地變量
EXCEPTION_PARAMETER 異常參數
METHOD 方法
CONSTRUCTOR 構造函數
OTHER 其餘
省略... 省略...

元素類型判斷

那接下來你們又會有一個問題了,既然咱們在掃描的是獲取的元素且這些元素表明着源文件中的結構化數據。那麼假如咱們想得到元素更多的信息怎麼辦呢?例如對於某個類,如今咱們已經知道了其爲ElementKind.CLASS種類,可是我想獲取其父類的信息,須要經過什麼方式呢?對於某個方法,咱們也一樣知道了其爲ElementKind.METHOD種類,那麼我想獲取該方法的返回值類型、參數類型、參數名稱,須要經過什麼方式呢?

固然Java已經爲咱們提供了相應的方法啦。使用mirror API就能解決這些問題啦,它能使咱們在未經編譯的源代碼中查看方法、域以及類型信息。在實際使用中經過TypeMirror來獲取元素類型。看下面的例子:

public class TypeKindSpiltProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.METHOD) {//若是當前元素是接口
                ExecutableElement methodElement = (ExecutableElement) element;
                TypeMirror returnType = methodElement.getReturnType();//獲取TypeMirror
                TypeKind kind = returnType.getKind();//獲取元素類型
                System.out.println("print return type----->" + kind.toString());
            }
        }
        return false;
    }

}
複製代碼

觀察上述代碼咱們能夠發現,當咱們使用註解處理器時,咱們會先找到相應的Element,若是你想得到該Element的更多的信息,那麼能夠配合TypeMirror使用TypeKind來判斷當前元素的類型。固然對於不一樣種類的Element,其獲取的TypeMirror方法可能會不一樣。TypeKind枚舉聲明以下表所示:

枚舉類型 類型
BOOLEAN boolean 類型
BYTE byte 類型
SHORT short 類型
INT int 類型
LONG long 類型
CHAR char 類型
FLOAT float 類型
DOUBLE double 類型
VOID void類型,主要用於方法的返回值
NONE 無類型
NULL 空類型
ARRAY 數組類型
省略... 省略...

元素可見性修飾符

在註解處理器中,咱們不只能得到元素的種類和信息,咱們還能獲取該元素的可見性修飾符(例如public、private等)。咱們能夠直接調用Element.getModifiers(),具體代碼以下所示:

public class GetModifiersProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {//若是元素是類
                Set<Modifier> modifiers = element.getModifiers();//獲取可見性修飾符
                if (!modifiers.contains(Modifier.PUBLIC)) {//若是當前類不是public
                    throw new ProcessingException(classElement, "The class %s is not public.",
                            classElement.getQualifiedName().toString());
                }
            }
        return false;
    }
}
複製代碼

在上述代碼中Modifer爲枚舉類型,具體枚舉以下所示:

public enum Modifier {

    /** The modifier {@code public} */          PUBLIC,
    /** The modifier {@code protected} */       PROTECTED,
    /** The modifier {@code private} */         PRIVATE,
    /** The modifier {@code abstract} */        ABSTRACT,
    /**
     * The modifier {@code default}
     * @since 1.8
     */
     DEFAULT,
    /** The modifier {@code static} */          STATIC,
    /** The modifier {@code final} */           FINAL,
    /** The modifier {@code transient} */       TRANSIENT,
    /** The modifier {@code volatile} */        VOLATILE,
    /** The modifier {@code synchronized} */    SYNCHRONIZED,
    /** The modifier {@code native} */          NATIVE,
    /** The modifier {@code strictfp} */        STRICTFP;
}

複製代碼

錯誤處理

在註解處理器的自定義中,咱們不只能調用相關方法獲取源文件中的元素信息,還能經過處理器提供的Messager來報告錯誤、警告以及提示信息。能夠直接使用processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);須要注意的是它並非處理器開發中的日誌工具,而是用來寫一些信息給使用此註解庫的第三方開發者的。也就是說若是咱們像傳統的Java應用程序拋出一個異常的話,那麼運行註解處理器的JVM就會崩潰,而且關於JVM中的錯誤信息對於第三方開發者並非很友好,因此推薦而且強烈建議使用Messager。就像下面這樣,當咱們判斷某個類不是public修飾的時候,咱們經過Messager來報告錯誤。

註解處理器是運行它本身的虛擬機JVM中。是的,你沒有看錯,javac啓動一個完整Java虛擬機來運行註解處理器。

public class GetModifiersProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {//若是元素是類
                Set<Modifier> modifiers = element.getModifiers();//獲取可見性修飾符
                if (!modifiers.contains(Modifier.PUBLIC)) {//若是當前類不是public
	                roundEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "the class is not public");
                }
            }
        return false;
    }
}
複製代碼

同時,在官方文檔中,描述了消息的不一樣級別,關於更多的消息級別,你們能夠經過從Diagnostic.Kind枚舉中查看。

錯誤信息顯示界面

若是你須要使用處理器提供的 Messager 來打印日誌,那麼你須要在以下界面中查看輸出的信息:

roundEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "the class is not public");
複製代碼

使用如上代碼,查看日誌界面以下所示:

messager錯誤展現界面.png

在每次編譯代碼的時候,若是你使用了 Messager 來打印日誌,那麼就會顯示。

文件生成

到了如今咱們已經基本瞭解整個APT的基礎知識。如今來說講APT技術如何生成新的類的定義(也就是建立新的源文件)。對於建立新的文件,咱們並不用像基本文件操做同樣,經過調用IO流來進行讀寫操做。而是經過JavaPoet來構造源文件。(固然當你使用JavaPoet時,在gradle中你須要添加依賴compile 'com.google.auto.service:auto-service:1.0-rc2'),JavaPoet的使用也很是簡單,就像下面這樣:

當進行註釋處理或與元數據文件(例如,數據庫模式、協議格式)交互時,JavaPoet對於源文件的生成可能很是有用。經過生成代碼,消除了編寫樣板的必要性,同時也保持了元數據的單一來源。

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.jennifer.andy.apt.annotation.Who")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CreateFileByJavaPoetProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        createFileByJavaPoet(set, roundEnvironment);
        return false;
    }
    
    /**
     * 經過JavaPoet生成新的源文件
     */
    private void createFileByJavaPoet(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //建立main方法
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//設置可見性修飾符public static
                .returns(void.class)//設置返回值爲void
                .addParameter(String[].class, "args")//添加參數類型爲String數組,且參數名稱爲args
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加語句
                .build();
        //建立類
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)//將main方法添加到HelloWord類中
                .build();

        //建立文件,第一個參數是包名,第二個參數是相關類
        JavaFile javaFile = JavaFile.builder("com.jennifer.andy.aptdemo.domain", helloWorld)
                .build();

        try {
            //建立文件
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            log(e.getMessage());
        }

    }

    /**
     * 調用打印語句而已
     */
    private void log(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
    }

}
複製代碼

當咱們build上述代碼後,咱們能夠在咱們的build目錄下獲得下列文件:

生成文件結果.png

關於JavaPoet的更多的詳細使用,你們能夠參考官方文檔-------->JavaPoet

分離處理器和項目

在上文中描述的APT使用規則中,咱們是將註解聲明庫註解處理器庫分紅了兩個庫,具體緣由我也作了詳細的解釋,如今咱們來思考以下問題。就算咱們把兩個庫都抽成了兩個獨立的庫,可是若是有開發者想把咱們自定義的註解處理器用於他的項目中,那麼他整個項目的編譯就必須也要把註解處理器與註解聲明庫包括進來。對於開發者來講,他們並不但願已經編譯好的項目中有包含註解處理器的相關代碼。因此將註解聲明庫與註解處理器庫不打包進入項目是很是有必要的!!換句話說,註解處理器只在編譯處理期間須要用到,編譯處理完後就沒有實際做用了,而主項目添加了這個庫會引入不少沒必要要的文件。

由於做者我自己是Android開發人員,因此如下都是針對Android項目展開討論。

使用android-apt

anroid-apt是Hugo Visser開發的一個Gradle插件,該插件的主要做用有以下兩點:

  • 容許只將編譯時註釋處理器配置爲依賴項,而不在最終APK或庫中包括工件
  • 設置源路徑,以便Android Studio能正確地找到註釋處理器生成的代碼

可是 Google爸爸看到別人這個功能功能不錯,因此爲本身的Android Gradle 插件也添加了名爲annotationProcessor 的功能來徹底代替 android-apt,既然官方支持了。那咱們就去看看annotationProcessor的使用吧。

annotationProcessor使用

其實annotationProcessor的使用也很是簡單,分爲兩種類型,具體使用以下代碼所示:

annotationProcessor project(':apt_compiler')//若是是本地庫
 annotationProcessor 'com.jakewharton:butterknife-compiler:9.0.0-rc1'//若是是遠程庫
複製代碼

總結

整個APT的流程下來,本身也查閱了很是多的資料,也解決了許多問題。雖然寫博客也花了很是多的時間。可是本身也發現了不少有趣的問題。我發現查閱的相關資料都會有一個通病。也就是沒有真正搞懂android apt與annotationProcessor的具體做用。因此這裏這裏也要告誡你們,對於網上的資料,本身必定要帶着懷疑與疑問的態度去瀏覽

同時我的以爲Gradle這一塊的知識點也很是重要。由於關於怎麼不把庫打包到實際項目中也是構建工具的特性與功能。但願你們有時間,必定要學習下相關Gradle知識。做者最近也在學習呢。和我一塊兒加油吧~

該文章中涉及的代碼,我已經提交到GitHub上了,你們按需下載---->源碼

最後

該文章參考如下博客與圖書,站在巨人的肩膀上。能夠看得更遠。

ANNOTATION PROCESSING 101

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

你必須知道的APT、annotationProcessor、android-apt、Provided、自定義註解

相關文章
相關標籤/搜索