avant-propos
APT, Annotation Processing Tool, processeur d'annotations
C'est vraiment une chose familière et inconnue pour les programmeurs Android.Presque tout le monde le sait, mais il est rarement utilisé dans le travail de développement quotidien, mais presque tous les frameworks bien connus l'ont.
Nous savons tous qu'il s'agit d'un processeur d'annotations, qui peut générer des fichiers avec des annotations au moment de la compilation, réduire la perte de performances, etc. Mais qu'est-ce que c'est ?
Il existe de nombreux concepts préalables à comprendre avant d'utiliser APT.
Qu'est-ce que le temps de compilation ?
La procédure peut être grossièrement divisée en trois périodes :
- Code source, code Java ou kotlin écrit pendant le développement
- Au moment de la compilation, le code java ou kotlin est compilé dans un fichier bytecode de classe
- Runtime, le programme s'exécute et le fichier de bytecode est chargé dans la machine virtuelle Java
La période de compilation est le processus du code source Java au bytecode de classe, qui est implémenté xxx.java — xxx.class
par l' outil javac
Découvrez le processus de compilation javac
- Créez une classe Java et écrivez du code
Test.java
avec désinvolture - Ouvrez cmd dans le répertoire où se trouve la classe Java actuelle
- commande d'écriture
javac Test.java
- S'il y a une erreur, il sera invité par une erreur et il n'y a pas d'invite pour indiquer le succès
- Après avoir consulté le répertoire, on constate qu'il y a plus de fichiers
Test.class
Le processus ci-dessus est le processus de compilation. Tant que le programme basé sur la machine virtuelle Java aura les étapes ci-dessus, les outils de développement termineront la compilation pour nous dans le développement quotidien.
Le processus de compilation n'est pas seulement aussi simple, il existe de nombreuses options
Tapez javac sur la ligne de commande et il y aura de la documentation sur l'utilisation de javac
javac
- options en option
- fichiers sources fichiers sources
Se concentrerprocessor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
processor
Représente l'ajout d'un processeur d'annotations lors de la compilation, qui est traduit en annotations dans le document d'annotation
用法: 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 出现警告时终止编译
@<文件名> 从文件读取选项和文件名
复制代码
Introduction aux processeurs d'annotation
Les processeurs d'annotation permettent aux développeurs de surveiller l'utilisation d'annotations spécifiques sur certains éléments lors de la compilation.
La raison d'utiliser des éléments au lieu de classes est que les annotations ne sont pas seulement marquées sur les classes mais aussi sur d'autres éléments tels que les méthodes, les propriétés, etc.
RésuméProcesseur
L'implémentation du processeur d'annotations doit hériter de la classe abstraite. javax.annotation.processing.AbstractProcessor
Cette classe abstraite n'a qu'une seule méthode abstraite process()
et il y a deux autres méthodes importantes à implémenter.
- traiter
- Jeu de paramètres, obtenir le jeu d'annotations à traiter par ce processeur d'annotations
- 参数 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博客