【Android】APT——注解處理器(一):初窺

在上一篇文章《注解實例 - 實現(xiàn)一個簡單的@Autowired運行時注解》中染乌,介紹了如何通過一個運行時注解來實現(xiàn)一個簡單的依賴注入工具队塘。
雖然使用方便,但是運行時注解是有一個硬傷的,那就是使用時需要進行大量掃描和反射操作欧引,會對運行效率造成一定影響漓拾。同時,一些功能需要自動生成代碼來提供拱层,這時候,就需要用到APT了宴咧。當(dāng)然根灯,這里的APT指的不是信息安全中的APT,而是Annotation Processor Tool掺栅,即注解處理器工具烙肺。

本文將介紹如何寫出一個最簡單的APT demo,通過APT處理注解并自動生成文件氧卧,其中關(guān)于注解的知識就不再介紹了桃笙。

首先明確Demo目標(biāo):

  1. 創(chuàng)建一個類注解MyAnnotation以及自定義注解處理器MyProcessor
  2. 通過MyProcessor自動生成類文件假抄,通過這個類中的函數(shù)可以打印出所有標(biāo)注了MyAnnotation注解的類怎栽。

一丽猬、準備工作

1.1 新建工程模塊

首先在Android Studio中新建工程。

新建App模塊apt-app熏瞄,為應(yīng)用主模塊脚祟。
新建Java lib模塊apt-annotation,為自定義注解模塊强饮。
新建Java lib模塊apt-processor由桌,為自定義注解處理器模塊。

1.2 添加依賴

apt-processor的build.gradle依賴中添加如下依賴:

    implementation "com.google.auto.service:auto-service:1.0-rc6"
    annotationProcessor "com.google.auto.service:auto-service:1.0-rc6"
    implementation 'com.squareup:javapoet:1.13.0'

    implementation project(":apt-annotation")

其中google auto service的作用是輔助自定義的注解處理器的注冊邮丰。
javapoet的作用是自動生成代碼行您。

apt-app模塊中添加如下依賴:

    implementation project(":apt-annotation")
    annotationProcessor project(":apt-processor")

注意這行annotationProcessor project(":apt-processor"),是在Java中使用注解處理器剪廉;如果需要在kotlin中使用娃循,則將annotationProcessor修改為kapt即可(同時需要在腳本文件最上方添加kapt插件)。

1.3 創(chuàng)建注解

這個注解不需要任何參數(shù)斗蒋,只作為一個標(biāo)記:

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
}

二捌斧、自定義注解處理器

2.1 創(chuàng)建自定義注解處理器類MyProcessor

在apt-processor模塊新建繼承自AbstractProcessor的類MyProcessor,內(nèi)容如下:

@SupportedOptions("my_param")  // 接收外來參數(shù)的key
@SupportedAnnotationTypes("top.littlefogcat.apt_annotation.MyAnnotation") // 支持的注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 支持的Java版本
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> types, RoundEnvironment rEnv) {}
}

其中類上面三個注解依次代表了注解處理器接受外部參數(shù)的key(暫時沒有用到)泉沾、注解處理器支持的注解類型(即1.3中創(chuàng)建的注解全名)捞蚂、注解處理器支持的Java版本。這三個配置也可以通過重寫類中的方法來實現(xiàn)跷究,不過通過注解更加簡便明了姓迅。

2.2 重寫init方法

AbstractProcessorinit方法提供了一個環(huán)境對象pEnv,從這個對象中可以得到一系列的工具以及獲取到外部傳入的參數(shù)俊马。這里通過pEnv.getFiler()獲取到文件工具丁存,以便之后創(chuàng)建文件。

    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment pEnv) {
        super.init(pEnv);
        mFiler = pEnv.getFiler();
    }

2.3 重寫process方法

process是注解處理器的核心方法潭袱,需要在其中實現(xiàn)注解的處理柱嫌。

2.3.1 前置

process方法的參數(shù)

process方法有兩個參數(shù):Set<? extends TypeElement> typesRoundEnvironment rEnv。其中前者表示了需要處理的注解的集合屯换,即創(chuàng)建自定義注解處理器時SupportedAnnotationTypes中所定義的類;后者則是APT框架提供的查詢程序元素的工具与学,如通過rEnv.getElementsAnnotatedWith可以查詢到程序中所有標(biāo)注了某注解的類彤悔。

Element

眾所周知,對于靜態(tài)的Java語言(源文件級別)索守,是由包晕窑、類、方法等程序元素組成的卵佛;在對Java源碼的處理中杨赤,各種程序元素對應(yīng)了javax.lang.model.element.Element接口敞斋。這個概念在之后的處理中會用到。

javapoet

javapoet是一個輔助自動生成java代碼的工具疾牲,可以方便的生成代碼植捎。其中關(guān)鍵類包括:JavaFile(對應(yīng).java文件)、TypeSpec(對應(yīng)類)阳柔、MethodSpec(對應(yīng)方法)焰枢、FieldSpec(對應(yīng)成員變量)、ParameterSpec(對應(yīng)參數(shù))舌剂、AnnotationSpec(對應(yīng)注解)等济锄。之后會使用javapoet來生成代碼。

2.3.2 通過javapoet生成方法

明確目標(biāo)

首先確定需要生成方法的格式霍转。目標(biāo)是這樣的荐绝,即打印所有標(biāo)注了@MyAnnotation的類的名稱:

  public void print() {
    System.out.println("以下是標(biāo)注了@MyAnnotation注解的類");
    System.out.println("Class1");
    System.out.println("Class2");
    System.out.println("Class3");
  }

創(chuàng)建method builder

在javapoet中,方法對應(yīng)的類是MethodSpec避消。首先通過建造者模式來創(chuàng)建一個builder同時指定方法名很泊、通過addModifiers指定可見性、通過returns指定返回值類型:

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("print") // 函數(shù)名
                .addModifiers(Modifier.PUBLIC) // 添加限定符
                .returns(void.class); // 返回類型

通過addStatement方法沾谓,可以在方法體中添加語句委造。這里打印一句話“以下是標(biāo)注了@MyAnnotation注解的類”

methodBuilder.addStatement("$T.out.println(\"以下是標(biāo)注了@MyAnnotation注解的類\")", System.class); // 添加語句

掃描所有標(biāo)注了@MyAnnotation的類并打印出來

在2.3.1中已經(jīng)介紹到,可以通過process方法的第二個參數(shù)獲取所有標(biāo)注了某個標(biāo)記的類均驶。然后再在方法中添加打印語句昏兆,將這些類的名稱打印出來:

    // 標(biāo)注了@MyAnnotation的節(jié)點
    Set<? extends Element> rootElements = rEnv.getElementsAnnotatedWith(MyAnnotation.class);
    // 查詢所有標(biāo)注了@MyAnnotation的類,并打印出來
    if (rootElements != null && !rootElements.isEmpty()) {
        for (Element element : rootElements) {
            String name = element.getSimpleName().toString();
            methodBuilder.addStatement("$T.out.println($S)", System.class, name); // 添加打印語句
        }
    }

完成構(gòu)造

    MethodSpec method = methodBuilder.build(); // 完成構(gòu)造

到這里妇穴,目標(biāo)方法就已經(jīng)構(gòu)建完成了爬虱。

2.3.3 通過javapoet生成類

與生成方法類似,生成類也是通過構(gòu)造者模式腾它,并可以通過addMethod將之前生成的方法添加到這個類中:

    // 生成類
    TypeSpec myClass = TypeSpec.classBuilder("AptGeneratedClass") // 類名
            .addModifiers(Modifier.PUBLIC) // public類
            .addMethod(method) // 添加上述方法
            .build(); // 構(gòu)造類

這里將生成的類名命名為AptGeneratedClass跑筝。

2.3.4 生成Java文件

生成Java文件分為兩步,第一步是通過2.3.3中的類對象生成JavaFile類型的文件對象瞒滴,第二步是通過2.2中獲取的Filer文件工具將其寫入到.java文件曲梗。

    // 生成文件
    JavaFile javaFile = JavaFile.builder("top.littlefogcat.apt", myClass) // 包名、類對象
            .build();
    try {
        javaFile.writeTo(mFiler); // 通過文件工具創(chuàng)建文件
    } catch (IOException e) {
        e.printStackTrace();
    }

至此妓忍,就通過APT完成了一個最簡單的可以自動生成文件的注解處理器虏两。

2.4 包含注釋的MyProcessor完整代碼

/**
 * 自定義APT類
 * <p>
 * TypeElement:類元素
 * <p>
 * 對于Java語言來講,將其看做結(jié)構(gòu)化的語言模型世剖,那么就分為了:
 * PackageElement包元素定罢,
 * TypeElement類元素,
 * TypeParameterElement泛型元素旁瘫,
 * VariableElement變量元素
 * ExecutableElement可執(zhí)行元素(方法)
 * <p>
 * 見{@link javax.lang.model.element.Element}
 */
@AutoService(Processor.class)
@SupportedOptions("my_param")  // 接收外來參數(shù)的key
@SupportedAnnotationTypes("top.littlefogcat.apt_annotation.MyAnnotation") // 支持的注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 支持的Java版本
public class MyProcessor extends AbstractProcessor {
    /*
     * 一些工具祖凫,在init方法中通過環(huán)境對象獲取
     */
    private Types mTypeUtils;
    private Elements mElementUtils;
    private Messager mMessager;
    private Filer mFiler; // 文件工具

    private String mParam;

    /**
     * 做一些初始化的工作琼蚯,可以通過pEnv參數(shù)獲取一些工具類。
     * 同時惠况,通過`SupportedOptions`配置的參數(shù)也可以在這里獲取遭庶。
     *
     * @param pEnv 環(huán)境對象,提供一些工具
     */
    @Override
    public synchronized void init(ProcessingEnvironment pEnv) {
        super.init(pEnv);
        mTypeUtils = pEnv.getTypeUtils();
        mElementUtils = pEnv.getElementUtils();
        mMessager = pEnv.getMessager();
        mFiler = pEnv.getFiler();

        mParam = pEnv.getOptions().get("env_param"); // 獲取外界傳入?yún)?shù)
    }

    /**
     * @param types 需要處理的注解集合
     * @param rEnv  運行環(huán)境售滤?通過這個對象查詢節(jié)點信息
     * @return 處理成功返回true罚拟,否則返回false
     */
    @Override
    public boolean process(Set<? extends TypeElement> types, RoundEnvironment rEnv) {
        if (types == null || types.isEmpty()) {
            return false;
        }


        // 生成一個函數(shù)格式如:
        // public void print() {
        //     System.out.println("以下是標(biāo)注了@MyAnnotation注解的類");
        //     System.out.println("AnnotedClass1");
        //     System.out.println("AnnotedClass2");
        // }
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("print") // 函數(shù)名
                .addModifiers(Modifier.PUBLIC) // 添加限定符
                .returns(void.class) // 返回類型
                .addStatement("$T.out.println(\"以下是標(biāo)注了@MyAnnotation注解的類\")", System.class); // 添加語句

        // 標(biāo)注了@MyAnnotation的節(jié)點
        Set<? extends Element> rootElements = rEnv.getElementsAnnotatedWith(MyAnnotation.class);
        // 查詢所有標(biāo)注了@MyAnnotation的類,并打印出來
        if (rootElements != null && !rootElements.isEmpty()) {
            for (Element element : rootElements) {
                String name = element.getSimpleName().toString();
                methodBuilder.addStatement("$T.out.println($S)", System.class, name);
            }
        }
        MethodSpec method = methodBuilder.build(); // 完成構(gòu)造

        // 生成類
        TypeSpec myClass = TypeSpec.classBuilder("AptGeneratedClass") // 類名
                .addModifiers(Modifier.PUBLIC) // public類
                .addMethod(method) // 添加上述方法
                .build(); // 構(gòu)造類

        // 生成文件
        JavaFile javaFile = JavaFile.builder("top.littlefogcat.apt", myClass) // 包名完箩、類型
                .build();
        try {
            javaFile.writeTo(mFiler); // 通過文件工具創(chuàng)建文件
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 接受外來參數(shù)赐俗,比如在build.gradle中的javaCompileOptions.annotationProcessorOptions配置
     * <p>
     * 也可以通過`@SupportedOptions`注解來配置
     */
    @Override
    public Set<String> getSupportedOptions() {
        return Collections.singleton("my_param");
    }

    /**
     * 返回當(dāng)前注解處理器支持的注解類型
     * <p>
     * 也可以通過`@SupportedAnnotationTypes`注解來配置
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton("top.littlefogcat.apt_annotation.MyAnnotation");
    }

    /**
     * 返回當(dāng)前注解處理器支持的JDK版本
     * <p>
     * 也可以通過`@SupportedSourceVersion`注解來配置
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }

}

三、測試效果

apt-app模塊中創(chuàng)建MainActivity弊知,將其添加@MyAnnotation注解阻逮。這里使用的kotlin,注意build.gradle中需要將annotationProcessor改成kapt秩彤。

@MyAnnotation
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Build一下叔扼,可以看到AptGeneratedClass.java已經(jīng)生成完畢了,并且打印出了標(biāo)注了@MyAnnotation注解的類(MainActivity)漫雷。

生成文件

生成文件

四瓜富、參考資料

《Android APT 系列 (三):APT 技術(shù)探究》
《JavaPoet - 優(yōu)雅地生成代碼》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市降盹,隨后出現(xiàn)的幾起案子与柑,更是在濱河造成了極大的恐慌,老刑警劉巖蓄坏,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件价捧,死亡現(xiàn)場離奇詭異,居然都是意外死亡涡戳,警方通過查閱死者的電腦和手機结蟋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渔彰,“玉大人嵌屎,你說我怎么就攤上這事「炱瘢” “怎么了编整?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乳丰。 經(jīng)常有香客問我,道長内贮,這世上最難降的妖魔是什么产园? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任汞斧,我火速辦了婚禮,結(jié)果婚禮上什燕,老公的妹妹穿的比我還像新娘粘勒。我一直安慰自己,他們只是感情好屎即,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布庙睡。 她就那樣靜靜地躺著,像睡著了一般技俐。 火紅的嫁衣襯著肌膚如雪乘陪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天雕擂,我揣著相機與錄音啡邑,去河邊找鬼。 笑死井赌,一個胖子當(dāng)著我的面吹牛谤逼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仇穗,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼流部,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纹坐?” 一聲冷哼從身側(cè)響起枝冀,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恰画,沒想到半個月后宾茂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡拴还,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年跨晴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片片林。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡端盆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出费封,到底是詐尸還是另有隱情焕妙,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布弓摘,位于F島的核電站焚鹊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏韧献。R本人自食惡果不足惜末患,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一研叫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧璧针,春花似錦嚷炉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至隧膏,卻和暖如春哗讥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背私植。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工忌栅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曲稼。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓索绪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贫悄。 傳聞我的和親對象是個殘疾皇子瑞驱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容