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

該文章中涉及的代碼,我已經(jīng)提交到GitHub上了,大家按需下載---->源碼

騎車車.jpg

前言

在上篇文章Android 注解系列之Annotation(二)中,簡要的介紹了注解的基本使用與定義。同時也提出了以下幾個問題,當我們聲明了一個注解后,是不是需要手動找到所有的Class對象或Field、Method?,怎么通過注解生成新的類的定義呢?當面對這些問題的時候,我相信大家的第一反應(yīng)肯定會想,"有不有相應(yīng)的三方庫呢?Java是否提供了相應(yīng)庫或者方法來解決呢?",當然Java肯定給我們提供了啦,就是我們既陌生又熟悉的APT工具啦。

為什么這里我會說既陌生又熟悉呢?我相信對于大多數(shù)安卓程序,我們都或多或少使用了一些主流庫,如Dagger2、ButterKnife、EventBus等,這些庫都使用了APT技術(shù)。既然大佬們都在使用,那我們怎么不去了解呢?好了,書歸正傳,下面我們就來看看怎么通過APT來處理之前我們提到的問題。

APT技術(shù)簡介

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

apt使用流程圖.png

APT技術(shù)使用規(guī)則

APT技術(shù)的使用,需要我們遵守一定的規(guī)則。大家先看一下整個APT項目項目構(gòu)建的一個規(guī)則圖,具體如下所示:


apt_rule.png

APT使用依賴

從圖中我們可以整個APT項目的構(gòu)建需要三個部分:

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

且三個部分的依賴關(guān)系為注解處理工具依賴注解聲明庫Android/Java項目同時依賴注解處理工具庫與注解聲明庫。

為什么把注解處理器獨立抽成一個庫呢?

對于Android項目默認是不包含 APT相關(guān)類的。所以要使用APT技術(shù),那么就必須創(chuàng)建一個Java Library。對于Java項目,獨立抽成一個庫,更容易維護與擴展。

為什么把注解聲明也單獨抽成一個庫,而不放到注解處理工具庫中呢?

舉個例子,如果注解聲明與注解處理器為同一個庫,如果有開發(fā)者希望把我們的注解處理器用于他的項目中,那么就必須包含注解聲明與整個注解處理器的代碼,我們能非常確定是,他并不希望已經(jīng)編譯好的項目中包含處理器相關(guān)的代碼。他僅僅希望使用我們的注解。所以將注解處理器與注解分開單獨抽成一個庫時非常有意義的。接下來的文章中會具體會描述有哪些方法可以將我們的注解處理器不打包在我們的實際項目中。

注解處理器的聲明

在了解了ATP的使用規(guī)則后,現(xiàn)在我們再來看看怎么聲明一個注解處理器,每一個注解處理器都需要承AbstractProcessor類,具體代碼如下所示:

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):每個注解處理器被初始化的時候都會被調(diào)用,該方法會被傳入ProcessingEnvironment 參數(shù)。ProcessingEnvironment 能提供很多有用的工具類,Elements、Types和Filer。后面我們將會看到詳細的內(nèi)容。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):注解處理器實際處理方法,一般要求子類實現(xiàn)該抽象方法,你可以在在這里寫你的掃描與處理注解的代碼,以及生成Java文件。其中參數(shù)RoundEnvironment ,可以讓你查詢出包含特定注解的被注解元素,后面我們會看到詳細的內(nèi)容。
  • 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()方法。

注冊注解處理器

到了現(xiàn)在我們基本了解了處理器聲明,現(xiàn)在我們可能會有個疑問,怎么樣將注解處理器注冊到Java編譯器中去呢?你必須提供一個.jar文件,就像其他.jar文件一樣,你需要打包你的注解處理器到此文件中,并且在你的jar中,你需要打包一個特定的文件javax.annotation.processing.ProcessorMETA-INF/services路徑下。就像下面這樣:

META-INF/services 相當于一個信息包,目錄中的文件和目錄獲得Java平臺的認可與解釋用來配置應(yīng)用程序、擴展程序、類加載器和服務(wù)文件,在jar打包時自動生成

放入特定文件夾.png

其中javax.annotation.processing.Processor文件中的內(nèi)容為每個注解處理器的合法的全名列表,每一個元素換行分割,也就是類似下面這樣:

com.jennifer.andy.processor.MineProcessor1
com.jennifer.andy.processor.MineProcessor2
com.jennifer.andy.processor.MineProcessor3

最后我們只要將你生成的.jar放到你的buildPath中,那么Java編譯器會自動的檢查和讀取javax.annotation.processing.Processor中的內(nèi)容,并注冊該注解處理器。

當然對于現(xiàn)在我們的編譯器,如IDEA、AndroidStudio等中,我們只創(chuàng)建相應(yīng)文件與文件夾就行了,并不同用放在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代表源文件中的元素,例如包、類、字段、方法等。整體的關(guān)系如下圖所示:

element繼承關(guān)系.png
  • Parameterizable:表示混合類型的元素(不僅只有一種類型的Element)
  • TypeParameterElement:帶有泛型參數(shù)的類、接口、方法或者構(gòu)造器。
  • VariableElement:表示字段、常量、方法或構(gòu)造函數(shù)。參數(shù)、局部變量、資源變量或異常參數(shù)。
  • QualifiedNameable:具有限定名稱的元素
  • ExecutableElement:表示類或接口的方法、構(gòu)造函數(shù)或初始化器(靜態(tài)或?qū)嵗?,包括注釋類型元素?/li>
  • 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文件(這種結(jié)構(gòu)化文本一樣)。

既然在掃描的時候,源文件是一種結(jié)構(gòu)化的數(shù)據(jù),那么我們能不能獲取一個元素的父元素和子元素呢?。當然是可以的啦,舉例來說,假如我們有個public class Person的TypeElement元素,那么我們可以遍歷它的所有的孩子元素。

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

其中getEnclosedElements()getEnclosingElement()Element中接口的聲明,想了解更多的內(nèi)容,大家可以查看一下源碼。

元素種類判斷

現(xiàn)在我們已經(jīng)了解了Element元素的分類,但是我們發(fā)現(xiàn)Element有時會代表多種元素。例如TypeElement代表類或接口,那有什么方法具體區(qū)別呢?我們繼續(xù)看下面的例子:

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注解的元素,通過調(diào)用element.getKind()具體判斷當前元素種類,其中具體元素類型為ElementKind枚舉類型ElementKind枚舉聲明如下表所示:

枚舉類型 種類
PACKAGE
ENUM 枚舉
CLASS
ANNOTATION_TYPE 注解
INTERFACE 接口
ENUM_CONSTANT 枚舉常量
FIELD 字段
PARAMETER 參數(shù)
LOCAL_VARIABLE 本地變量
EXCEPTION_PARAMETER 異常參數(shù)
METHOD 方法
CONSTRUCTOR 構(gòu)造函數(shù)
OTHER 其他
省略... 省略...

元素類型判斷

那接下來大家又會有一個問題了,既然我們在掃描的是獲取的元素且這些元素代表著源文件中的結(jié)構(gòu)化數(shù)據(jù)。那么假如我們想獲得元素更多的信息怎么辦呢?例如對于某個類,現(xiàn)在我們已經(jīng)知道了其為ElementKind.CLASS種類,但是我想獲取其父類的信息,需要通過什么方式呢?對于某個方法,我們也同樣知道了其為ElementKind.METHOD種類,那么我想獲取該方法的返回值類型、參數(shù)類型、參數(shù)名稱,需要通過什么方式呢?

當然Java已經(jīng)為我們提供了相應(yīng)的方法啦。使用mirror API就能解決這些問題啦,它能使我們在未經(jīng)編譯的源代碼中查看方法、域以及類型信息。在實際使用中通過TypeMirror來獲取元素類型??聪旅娴睦樱?/p>

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;
    }

}

觀察上述代碼我們可以發(fā)現(xiàn),當我們使用注解處理器時,我們會先找到相應(yīng)的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 數(shù)組類型
省略... 省略...

元素可見性修飾符

在注解處理器中,我們不僅能獲得元素的種類和信息,我們還能獲取該元素的可見性修飾符(例如public、private等)。我們可以直接調(diào)用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;
}

錯誤處理

在注解處理器的自定義中,我們不僅能調(diào)用相關(guān)方法獲取源文件中的元素信息,還能通過處理器提供的Messager來報告錯誤、警告以及提示信息??梢灾苯邮褂?code>processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);需要注意的是它并不是處理器開發(fā)中的日志工具,而是用來寫一些信息給使用此注解庫的第三方開發(fā)者的。也就是說如果我們像傳統(tǒng)的Java應(yīng)用程序拋出一個異常的話,那么運行注解處理器的JVM就會崩潰,并且關(guān)于JVM中的錯誤信息對于第三方開發(fā)者并不是很友好,所以推薦并且強烈建議使用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;
    }
}

同時,在官方文檔中,描述了消息的不同級別,關(guān)于更多的消息級別,大家可以通過從Diagnostic.Kind枚舉中查看。

錯誤信息顯示界面

如果你需要使用處理器提供的 Messager 來打印日志,那么你需要在如下界面中查看輸出的信息:

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

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

messager錯誤展示界面.png

在每次編譯代碼的時候,如果你使用了 Messager 來打印日志,那么就會顯示。

文件生成

到了現(xiàn)在我們已經(jīng)基本了解整個APT的基礎(chǔ)知識?,F(xiàn)在來講講APT技術(shù)如何生成新的類的定義(也就是創(chuàng)建新的源文件)。對于創(chuàng)建新的文件,我們并不用像基本文件操作一樣,通過調(diào)用IO流來進行讀寫操作。而是通過JavaPoet來構(gòu)造源文件。(當然當你使用JavaPoet時,在gradle中你需要添加依賴compile 'com.google.auto.service:auto-service:1.0-rc2'),JavaPoet的使用也非常簡單,就像下面這樣:

當進行注釋處理或與元數(shù)據(jù)文件(例如,數(shù)據(jù)庫模式、協(xié)議格式)交互時,JavaPoet對于源文件的生成可能非常有用。通過生成代碼,消除了編寫樣板的必要性,同時也保持了元數(shù)據(jù)的單一來源。

@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) {
        //創(chuàng)建main方法
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//設(shè)置可見性修飾符public static
                .returns(void.class)//設(shè)置返回值為void
                .addParameter(String[].class, "args")//添加參數(shù)類型為String數(shù)組,且參數(shù)名稱為args
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加語句
                .build();
        //創(chuàng)建類
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)//將main方法添加到HelloWord類中
                .build();

        //創(chuàng)建文件,第一個參數(shù)是包名,第二個參數(shù)是相關(guān)類
        JavaFile javaFile = JavaFile.builder("com.jennifer.andy.aptdemo.domain", helloWorld)
                .build();

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

    }

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

}

當我們build上述代碼后,我們可以在我們的build目錄下得到下列文件:


生成文件結(jié)果.png

關(guān)于JavaPoet的更多的詳細使用,大家可以參考官方文檔-------->JavaPoet

分離處理器和項目

在上文中描述的APT使用規(guī)則中,我們是將注解聲明庫注解處理器庫分成了兩個庫,具體原因我也做了詳細的解釋,現(xiàn)在我們來思考如下問題。就算我們把兩個庫都抽成了兩個獨立的庫,但是如果有開發(fā)者想把我們自定義的注解處理器用于他的項目中,那么他整個項目的編譯就必須也要把注解處理器與注解聲明庫包括進來。對于開發(fā)者來說,他們并不希望已經(jīng)編譯好的項目中有包含注解處理器的相關(guān)代碼。所以將注解聲明庫與注解處理器庫不打包進入項目是非常有必要的?。?/code>換句話說,注解處理器只在編譯處理期間需要用到,編譯處理完后就沒有實際作用了,而主項目添加了這個庫會引入很多不必要的文件。

因為作者我本身是Android開發(fā)人員,所以以下都是針對Android項目展開討論。

使用android-apt

anroid-apt是Hugo Visser開發(fā)的一個Gradle插件,該插件的主要作用有如下兩點:

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

但是 Google爸爸看到別人這個功能功能不錯,所以為自己的Android Gradle 插件也添加了名為annotationProcessor 的功能來完全代替 android-apt,既然官方支持了。那我們就去看看annotationProcessor的使用吧。

annotationProcessor使用

其實annotationProcessor的使用也非常簡單,分為兩種類型,具體使用如下代碼所示:

 annotationProcessor project(':apt_compiler')//如果是本地庫
 annotationProcessor 'com.jakewharton:butterknife-compiler:9.0.0-rc1'//如果是遠程庫

總結(jié)

整個APT的流程下來,自己也查閱了非常多的資料,也解決了許多問題。雖然寫博客也花了非常多的時間。但是自己也發(fā)現(xiàn)了很多有趣的問題。我發(fā)現(xiàn)查閱的相關(guān)資料都會有一個通病。也就是沒有真正搞懂android apt與annotationProcessor的具體作用。所以這里這里也要告誡大家,對于網(wǎng)上的資料,自己一定要帶著懷疑與疑問的態(tài)度去瀏覽

同時個人覺得Gradle這一塊的知識點也非常重要。因為關(guān)于怎么不把庫打包到實際項目中也是構(gòu)建工具的特性與功能。希望大家有時間,一定要學習下相關(guān)Gradle知識。作者最近也在學習呢。和我一起加油吧~

該文章中涉及的代碼,我已經(jīng)提交到GitHub上了,大家按需下載---->源碼

最后

該文章參考以下博客與圖書,站在巨人的肩膀上??梢钥吹酶h。

ANNOTATION PROCESSING 101

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容