注解的使用(一):APT,編譯時注解處理器

關(guān)于編譯時注解(APT)由淺入深有三部分,分別是:

  1. 自定義注解處理器 : 例如 ButterKnife、Room 根據(jù)注解生成新的類;
  2. 利用 JcTree 在編譯時修改代碼:像 Lombok 自動往類中新增 getter/setter 方法、往方法中插入代碼行等;
  3. 自定義 Gradle 插件在編譯時修改代碼 :例如一些代碼插樁框架,以及我司一些應(yīng)用使用了這種方式。

這篇文章以Demo的形式,介紹如何從零開始創(chuàng)建一個自定義的注解處理器,并生成一個新的類。這個類中有一個靜態(tài)方法,方法返回添加了自定義注解的所有類。 看懂這篇文章,你就能寫出自己的 ButterKnife 啦~

本文中的源代碼可以在這里查看: https://github.com/Sino-Snack/APT-Source-Code


1. 環(huán)境搭建和 Gradle 配置

1.1 創(chuàng)建注解 Module
我們在工程中新建一個 Java Library,Module 名稱定義為 Annotation。再定義一個自定義的注解類:

@Target(ElementType.TYPE)
public @interface DemoAnnotation {
}

第一步就完啦~ (如果不清楚元注解的使用,可以搜索其它文章了解)

1.2 創(chuàng)建注解處理器 Module
在工程中再創(chuàng)建一個 Java Library,名稱定義為 AnnotationProcessor,并在 build.gradle 中加入如下依賴:

import org.gradle.internal.jvm.Jvm

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 剛才定義的 Annotation 模塊
    implementation project(":Annotation")

    // 谷歌的 AutoService 可以讓我們的注解處理器自動注冊上
    implementation 'com.google.auto.service:auto-service:1.0-rc4'

    // 用于生成新的類、函數(shù)
    implementation "com.squareup:javapoet:1.9.0"

    // 谷歌的一個工具類庫
    implementation "com.google.guava:guava:24.1-jre"

    implementation files(Jvm.current().toolsJar)
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

1.3 配置項目級的 build.gradle
再在項目級的 build.gradle 中增加 android-apt 的依賴:

buildscript {

    repositories { ... }

    dependencies {
        ...
        classpath "com.neenbedankt.gradle.plugins:android-apt:1.8"
    }

    ...
}


2. 實現(xiàn)自定義注解處理器

所有的自定義注解處理器都應(yīng)該繼承自 AbstractProcessor 類。
我們也定義一個處理器,并實現(xiàn)幾個模板方法:

@AutoService(Processor.class)
public class DemoProcessor extends AbstractProcessor {

    /* ======================================================= */
    /* Fields                                                  */
    /* ======================================================= */

    /**
     * 用于將創(chuàng)建的類寫入到文件
     */
    private Filer mFiler;

    /* ======================================================= */
    /* Override/Implements Methods                             */
    /* ======================================================= */

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 這個方法是注解處理器的核心,稍后單獨分析這個方法如何實現(xiàn)
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 這個方法返回當(dāng)前處理器 能處理哪些注解,這里我們只返回 DemoAnnotation
        return Collections.singleton(DemoAnnotation.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 這個方法返回當(dāng)前處理器 支持的代碼版本
        return SourceVersion.latestSupported();
    }
}

2.1 process() 方法詳解
我們的需求是生成一個新的類,類中有一個靜態(tài)方法,方法返回添加了 @Annotation 注解的所有類。這些操作都需要我們在 process() 方法中去實現(xiàn)。步驟:
(1) 獲取所有添加了注解的元素;
(2) 生成一個方法,方法的代碼塊是返回(1)中獲取到的列表。
(3) 生成一個類,類中加入(2)中生成的方法;
(4) 將(3)中生成的類寫入文件。

所以我們得到這個方法的實現(xiàn):

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {

    // 獲取所有被 @DemoAnnotation 注解的類
    Set<? extends Element> elements = environment.getElementsAnnotatedWith(DemoAnnotation.class);

    // 創(chuàng)建一個方法,返回 Set<Class>
    MethodSpec method = createMethodWithElements(elements);

    // 創(chuàng)建一個類
    TypeSpec clazz = createClassWithMethod(method);

    // 將這個類寫入文件
    writeClassToFile(clazz);

    return false;
}

接下來就讓我們看看這三個關(guān)鍵的方法分別是怎么實現(xiàn)的:

2.2 如何創(chuàng)建新的方法

/**
 * 創(chuàng)建一個方法,這個方法返回 elements 中的所有類信息。
 */
private MethodSpec createMethodWithElements(Set<? extends Element> elements) {

    // "getAllClasses" 是生成的方法的名稱
    MethodSpec.Builder builder = MethodSpec.methodBuilder("getAllClasses");

    // 為這個方法加上 "public static" 的修飾符
    builder.addModifiers(Modifier.PUBLIC, Modifier.STATIC);

    // 定義返回值類型為 Set<Class>
    ParameterizedTypeName returnType = ParameterizedTypeName.get(
            ClassName.get(Set.class),
            ClassName.get(Class.class)
    );
    builder.returns(returnType);

    // 經(jīng)過上面的步驟,
    // 我們得到了 public static Set<Class> getAllClasses() {} 這個方法,
    // 接下來我們實現(xiàn)它的方法體:

    // 方法中的第一行: Set<Class> set = new HashSet<>();
    builder.addStatement("$T<$T> set = new $T<>();", Set.class, Class.class, HashSet.class);

    // 上面的 "$T" 是占位符,代表一個類型,可以自動 import 包。其它占位符:
    // $L: 字符(Literals)、 $S: 字符串(String)、 $N: 命名(Names)

    // 遍歷 elements, 添加代碼行
    for (Element element : elements) {

        // 因為 @Annotation 只能添加在類上,所以這里直接強(qiáng)轉(zhuǎn)為 ClassType
        ClassType type = (ClassType) element.asType();

        // 在我們創(chuàng)建的方法中,新增一行代碼: set.add(XXX.class);
        builder.addStatement("set.add($T.class)", type);
    }

    // 經(jīng)過上面的 for 循環(huán),我們就把所有添加了注解的類加入到 set 變量中了,
    // 最后,只需要把這個 set 作為返回值 return 就好了:
    builder.addStatement("return set");

    return builder.build();
}

2.3 如何創(chuàng)建新的類

/**
 * 創(chuàng)建一個類,并把參數(shù)中的方法加入到這個類中
 */
private TypeSpec createClassWithMethod(MethodSpec method) {
    // 定義一個名字叫 OurClass 的類
    TypeSpec.Builder ourClass = TypeSpec.classBuilder("OurClass");

    // 聲明為 public
    ourClass.addModifiers(Modifier.PUBLIC);

    // 為這個類加入一段注釋
    ourClass.addJavadoc("這個類是自動創(chuàng)建的哦~\n\n @author ZhengHaiPeng");

    // 為這個類新增一個方法
    ourClass.addMethod(method);

    return ourClass.build();
}

2.4 如何將創(chuàng)建的類寫入文件

/**
 * 將一個創(chuàng)建好的類寫入到文件中參與編譯
 */
private void writeClassToFile(TypeSpec clazz) {
    // 聲明一個文件在 "me.moolv.apt" 下
    JavaFile file = JavaFile.builder("me.moolv.apt", clazz).build();

    // 寫入文件
    try {
        file.writeTo(mFiler);
    } catch (IOException e) {
        e.printStackTrace();
    }
}


3. 使用自定義注解處理器

在要使用的 Module 中,例如 app,的 build.gradle 中加入依賴:

apply plugin: 'com.android.application'

android {
    ...
}

dependencies {
    ...
    annotationProcessor project(":AnnotationProcessor")
    implementation project(path: ':Annotation')
}

執(zhí)行 Android Studio 的 Build > Make Project, 就能在 app Module 的 build/source/apt 路徑下找到生成的類文件了:

/**
 * 這個類是自動創(chuàng)建的哦~
 *
 * @author ZhengHaiPeng
 */
public class OurClass {
    public static Set<Class> getAllClasses() {
        Set<Class> set = new HashSet<>();
        set.add(MainActivity.class);
        return set;
    }
}

這樣我們就實現(xiàn)了 自定義注解處理器,并生成代碼啦,有疑問留言就好~


4. 如何為注解處理器傳遞參數(shù)?

APT 中的 Processor 可能會用到一些參數(shù),這些參數(shù)可以在 gradle 中配置。

設(shè)置參數(shù)

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {

            annotationProcessorOptions {
                // 下面定義要傳遞的參數(shù)
                argument "key1", "value1"
                argument "key2", "value2"
            }
        }
    }

獲取參數(shù)
在 Processor 的 init 方法中可以獲取參數(shù):

@Override
public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    ...

    String value1 = env.getOptions().get("key1");

    ...
}
最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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