序文
APT、注釈処理ツール、注釈プロセッサ
これはAndroidプログラマーにとってはなじみのあるものであり、なじみのないものです。ほとんどの人が知っていますが、日常の開発作業で使用されることはめったにありませんが、よく知られているフレームワークのほとんどすべてに搭載されています。
コンパイル時の注釈付きのファイルを生成したり、パフォーマンスの低下を減らしたりできる注釈プロセッサであることは誰もが知っています。しかし、それは何でしょうか。
APTを使用する前に理解しておくべき多くの前提条件の概念があります。
コンパイル時とは何ですか?
手順は大きく3つの期間に分けることができます。
- 開発中に記述されたソースコード、Javaまたはkotlinコード
- コンパイル時に、javaまたはkotlinコードがクラスバイトコードファイルにコンパイルされます
- ランタイム、プログラムが実行され、バイトコードファイルがJava仮想マシンにロードされます
コンパイル期間は、javaソースコードからクラスバイトコードへのプロセスでありxxx.java — xxx.class
、javacツールによってます。
javacコンパイルプロセスを体験してください
- Javaクラスを作成し、
Test.java
何気なく - 現在のJavaクラスが配置されているディレクトリでcmdを開きます
- 書き込みコマンド
javac Test.java
- エラーがある場合は、エラーによってプロンプトが表示され、成功を示すプロンプトは表示されません。
- ディレクトリを表示した後、さらにファイルがあることがわかりました
Test.class
上記のプロセスはコンパイルプロセスです。Java仮想マシンに基づくプログラムが上記のステップを実行する限り、開発ツールは日常の開発でコンパイルを完了します。
コンパイルプロセスはそれほど単純であるだけでなく、多くのオプションがあります
コマンドラインでjavacと入力すると、javacの使用に関するドキュメントが表示されます。
javac
- オプションのオプション
- ソースファイルソースファイル
集中processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
processor
コンパイル中に注釈プロセッサを追加することを表します。これは、注釈ドキュメントの注釈に変換されます。
用法: javac <options> <source files>
其中, 可能的选项包括:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件和注释处理程序的位置
-cp <路径> 指定查找用户类文件和注释处理程序的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖所安装扩展的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
-parameters 生成元数据以用于方法参数的反射
-d <目录> 指定放置生成的类文件的位置
-s <目录> 指定放置生成的源文件的位置
-h <目录> 指定放置生成的本机标头文件的位置
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-encoding <编码> 指定源文件使用的字符编码
-source <发行版> 提供与指定发行版的源兼容性
-target <发行版> 生成特定 VM 版本的类文件
-profile <配置文件> 请确保使用的 API 在指定的配置文件中可用
-version 版本信息
-help 输出标准选项的提要
-A关键字[=值] 传递给注释处理程序的选项
-X 输出非标准选项的提要
-J<标记> 直接将 <标记> 传递给运行时系统
-Werror 出现警告时终止编译
@<文件名> 从文件读取选项和文件名
复制代码
注釈プロセッサの概要
注釈プロセッサを使用すると、開発者はコンパイル中に特定の要素に対する特定の注釈の使用を監視できます。
クラスの代わりに要素を使用する理由は、アノテーションがクラスだけでなく、メソッド、プロパティなどの他の要素にもマークされるためです。
AbstractProcessor
アノテーションプロセッサの実装では、抽象クラスを継承する必要があります。javax.annotation.processing.AbstractProcessor
この抽象クラスにはprocess()
、抽象メソッドが1つだけあり、実装する必要のある重要なメソッドは他に2つあります。
- 処理する
- パラメータセット、この注釈プロセッサによって処理される注釈セットを取得します
- 参数 RoundEnvironment ,当前编译轮次上下文环境,分析如下:
- 为什么说是当前轮次呢,因为可能存在多轮编译 由返回值控制
- 重要方法
roundEnvironment.getRootElements();
观察日志发现,方法返回集合,包含当前编译轮次的所有类 - 重要方法
roundEnvironment.getElementsAnnotatedWith();
传入注解,筛选出所有被注解标记的元素,此次不仅仅是类,方法,属性等等都会被返回 - 返回值
- true 表明当前注解处理器 新生成类,需要再次进行注解检查,因为新生成的类也可能包含注解也要处理,所以可能会产生多轮次
- 返回false 表明没有新生成类,代码没有变化,无需再次处理
- 如果注解处理器工作多轮,第一轮已经处理过的类,第二轮不处理,只处理新类
- getSupportedAnnotationTypes()
- 指定当前注解处理器可以处理那些注解
- getSupportedSourceVersion
- 指定处理那个版本的代码
- 重要的实例变量
ProcessingEnvironment
- 提供了一些非常实用的工具类 在注解处理器初始化的时候创建(init()方法执行的时候)
- getOptions 接收外部参数
- getMessager 日志输出工具
- getFiler 用于创建新类
- getElementUtils 对元素进行操作的工具类
- getTypeUtils 对类型进行操作的工具类
Element 介绍
以下内容转载自 Android APT 系列 (三):APT 技术探究 - 掘金 (juejin.cn
实际上,Java 源文件是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element ,例如包,类,字段,方法等等:
package com.dream; // PackageElement:包元素
public class Main<T> { // TypeElement:类元素; 其中 <T> 属于 TypeParameterElement 泛型元素
private int x; // VariableElement:变量、枚举、方法参数元素
public Main() { // ExecuteableElement:构造函数、方法元素
}
}
复制代码
Java 的 Element 是一个接口,源码如下:
public interface Element extends javax.lang.model.AnnotatedConstruct {
// 获取元素的类型,实际的对象类型
TypeMirror asType();
// 获取Element的类型,判断是哪种Element
ElementKind getKind();
// 获取修饰符,如public static final等关键字
Set<Modifier> getModifiers();
// 获取类名
Name getSimpleName();
// 返回包含该节点的父节点,与getEnclosedElements()方法相反
Element getEnclosingElement();
// 返回该节点下直接包含的子节点,例如包节点下包含的类节点
List<? extends Element> getEnclosedElements();
@Override
boolean equals(Object obj);
@Override
int hashCode();
@Override
List<? extends AnnotationMirror> getAnnotationMirrors();
//获取注解
@Override
<A extends Annotation> A getAnnotation(Class<A> annotationType);
<R, P> R accept(ElementVisitor<R, P> v, P p);
}
复制代码
我们可以通过 Element 获取如上一些信息(写了注释的都是一些常用的)
由 Element 衍生出来的扩展类共有 5 种:
1、PackageElement 表示一个包程序元素
2、TypeElement 表示一个类或者接口程序元素
3、TypeParameterElement 表示一个泛型元素
4、VariableElement 表示一个字段、enum 常量、方法或者构造方法的参数、局部变量或异常参数
5、ExecuteableElement 表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)
可以发现,Element 有时会代表多种元素,例如 TypeElement 代表类或接口,此时我们可以通过 element.getKind() 来区分:
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
for (Element element : elements) {
if (element.getKind() == ElementKind.CLASS) {
// 如果元素是类
} else if (element.getKind() == ElementKind.INTERFACE) {
// 如果元素是接口
}
}
复制代码
ElementKind 是一个枚举类,它的取值有很多,如下:
PACKAGE //表示包
ENUM //表示枚举
CLASS //表示类
ANNOTATION_TYPE //表示注解
INTERFACE //表示接口
ENUM_CONSTANT //表示枚举常量
FIELD //表示字段
PARAMETER //表示参数
LOCAL_VARIABLE //表示本地变量
EXCEPTION_PARAMETER //表示异常参数
METHOD //表示方法
CONSTRUCTOR //表示构造函数
OTHER //表示其他
复制代码
参数传递
在 app 模块下 build.gradle 文件中 defaultConfig
下声明
javaCompileOptions{
annotationProcessorOptions{
arguments = [
key1: "value1",
key2: "value2"
]
}
}
复制代码
在Processor的init方法中获取参数
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Map<String,String> options = processingEnv.getOptions();
}
复制代码
使用过程
环境搭建
- 创建java工程(必须是java工程) apt-complier 用于处理注解
- 创建新类 继承
AbstractProcessor
- 创建 在main文件夹下创建
resources — META-INF — services — javax.annotation.processing.Processor
- 在文件
javax.annotation.processing.Processor
中声明 刚刚创建的注解处理器,继承AbstractProcessor
的类 会有自动提示
- 创建新类 继承
- 创建Java工程 apt-annotation 用于定义注解
- apt-complier 引入 apt-annotation的依赖
- Android 工程 引用上述两个工程
- 如果是kotlin 工程 需要使用 kapt 不然无法解析 被注解标记的 kotlin文件
implementation project(":apt-annotation")
annotationProcessor project(":apt-complier")
// kapt project(":apt-complier")
复制代码
上述套路固定
编写代码
相比于理解apt ,环境搭建和写代码反倒简单。 在动态生成java类之前,肯定会用硬编码将功能实现好,将其中繁琐的重复性逻辑单独抽取出来,结合注解解耦代码。
动态生成代码 肯定是存在一个模板,实现好的逻辑,只需要利用javaPoat 或 字符串拼接 加一点点逻辑改动就好。
只能说这个过程很繁琐麻烦,容易出错,也还是正常开发 而且逻辑都确定了 写好了一个硬编码的版本,不过写代码的方式变了。
生成一个TestActivity,用字符串拼接 和 javapoat 两种方式生成新类,代码如下:
public class RouteProcessor extends AbstractProcessor {
Filer filer;
Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// testCreateFile();
// testJavaPoet();
return false;
}
/**
*使用JavaPoet
*/
private void testJavaPoet() {
ClassName AppCompatActivity = ClassName.get("androidx.appcompat.app", "AppCompatActivity");
ClassName bundle = ClassName.get("android.os", "Bundle");
ClassName Nullable = ClassName.get("androidx.annotation", "Nullable");
ClassName Override = ClassName.get("java.lang", "Override");
//创建一个方法参数
ParameterSpec savedInstanceState = ParameterSpec.builder(bundle, "savedInstanceState")
.addAnnotation(Nullable)
.build();
//创建一个方法
MethodSpec onCreate = MethodSpec.methodBuilder("onCreate")
.addAnnotation(Override)
.addModifiers(Modifier.PROTECTED)
.returns(TypeName.VOID)
.addParameter(savedInstanceState)
.addStatement("super.onCreate(savedInstanceState)")
.build();
//创建一个类
TypeSpec testActivity = TypeSpec.classBuilder("TestActivity")
.addModifiers(Modifier.PUBLIC)
.addMethod(onCreate)
.superclass(AppCompatActivity)
.build();
//创建文件
JavaFile file = JavaFile.builder("com.whl215.aptdemo", testActivity).build();
try {
file.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*原生api创建类
*/
private void testCreateFile() {
BufferedWriter writer = null;
try {
JavaFileObject sourceFile = filer.createSourceFile("com.xxx.TestActivity");
writer = new BufferedWriter(sourceFile.openWriter());
writer.write("package com.whl215.aptdemo;\n\n");
writer.write("import android.os.Bundle;\n\n");
writer.write("import androidx.annotation.Nullable;\n");
writer.write("import androidx.appcompat.app.AppCompatActivity;\n");
writer.write("public class TestActivity extends AppCompatActivity {\n\n");
writer.write(" @Override\n");
writer.write(" protected void onCreate(@Nullable Bundle savedInstanceState) {\n");
writer.write(" super.onCreate(savedInstanceState);\n");
writer.write(" }\n");
writer.write("}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
*可以处理哪些注解
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(Route.class.getCanonicalName());
}
/**
*处理那个版本的代码
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
复制代码
参考文章
Android APT 系列 (三):APT 技术探究 - 掘金 (juejin.cn)
Java进阶--编译时注解处理器(APT)详解 - 掘金 (juejin.cn)
(30条消息) 【Android APT】注解处理器 ( Element 注解节点相关操作 )_韩曙亮的博客-CSDN博客