關(guān)于編譯時注解(APT)由淺入深有三部分,分別是:
- 自定義注解處理器 : 例如 ButterKnife、Room 根據(jù)注解生成新的類;
- 利用 JcTree 在編譯時修改代碼:像 Lombok 自動往類中新增 getter/setter 方法、往方法中插入代碼行等;
- 自定義 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");
...
}