注解处理器
作者:互联网
本文代码可以在 giagor/AptGo - github 找到
研究的原因
最近在学习 Dagger
的时候,发现写几个注解然后编译,Dagger
就可以生成一些类给我们使用,感觉很神奇,所以就找了些资料学习一波。这种处理的技术被称作 Annotation Processing Tool
(APT),即注解处理器。
处理注解有两种方法:
- 应用运行时通过反射获取注解的信息,对运行时的性能有损失,
Retrofit
就是通过该方法处理注解。 - 通过 APT 在编译时获取并处理注解的信息,这种方法因为要在编译时通过 IO 生成额外的类,会导致编译时间变长,
Dagger
使用的是这种方法。
注解处理器(APT):javac 的一种处理注解工具,用来在编译期扫描和处理注解,通过注解来生成 Java 文件,它只能生成新的源文件而不能修改已经存在的源文件。通过这种方式,可以让我们编程中减少很多的代码,解放生产力。
了解 Element
在实现注解处理器之前,需要先了解 java 中的一个概念 Element
:它是一个接口,可以表示程序中的包、类、方法、字段等元素。Element
的子接口有下面这些
PackageElement:表示包元素,提供对有关包及其成员的信息的访问。
TypeElement:表示一个类或者接口程序元素,提供对有关类型及其成员信息的访问。
TypeParameterElement:表示一个泛型元素。
VariableElement:表示一个字段、enum常量、方法或者构造器的参数、局部变量、资源变量或异常参数。
ExecutableElement:表示类或者接口的方法、构造器、初始代码块(静态或实例)。
......
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);
}
自己实现一个 APT
功能:实现一个可以对类进行标注的注解,在编译的时候可以自动生成一个类,该类含有原来类的成员变量,并且对外提供 get
、set
方法。
注解模块
新建 annotation
模块:在 AS 中 File -> New -> New Module -> Java or Kotlin Library 语言选择 Kotlin。这个 Module
主要用来存放我们定义的注解。
在 annotation/build.gradle
中添加下面的依赖:
implementation 'androidx.annotation:annotation:1.2.0'
在 annotation
模块中创建一个注解:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ExtractField()
处理器模块
新建 compiler
模块:在 AS 中 File -> New -> New Module -> Java or Kotlin Library 语言选择 Kotlin。这个 Module
主要存放注解处理器,并且对注解处理器进行注册。
现在应用程序中有三个模块:
在 compiler/build.gradle
中添加下面两个依赖:
// 我们需要对之前定义的注解进行处理
implementation project(path: ':annotation')
// 主要用于生成 kotlin 文件
implementation 'com.squareup:kotlinpoet:0.7.0'
创建一个 Processor
类对注解进行处理,该类继承至 AbstractProcessor
:
class Processor : AbstractProcessor() {...}
接着我们要将注解处理器注册到编译器当中,创建下面的目录:
javax.annotation.processing.Processor
文件中写的是 Processor
的包名加类名,在这里就是:
com.example.compiler.Processor
这一步也可以采用
auto-service
这个库进行自动注册
接着主要是编写 Processor
这个类,分为下面几个步骤:
- 初始化工作:这里我会在本地初始化一个
log.txt
文件,用于记录一些日志信息,方便调试,实际开发中不需要这个。 - getSupportedAnnotationTypes():返回一个当前注解处理器所有支持的注解的集合。当前注解处理器需要处理哪种注解就加入哪种注解,如果类型符合,就会调用
process()
方法。 - getSupportedSourceVersion(): 需要通过哪个版本的 jdk 来进行编译。
- process:核心方法,在这里对注解进行处理,并生成相应的类。
日志文件的维护
这一部分非核心流程,不感兴趣的话,可以跳过
一开始是写在代码中使用 print
查看一些变量信息,但是没找到 print
的信息最终输出到了哪里,所以就想到了在电脑本地去创建一个文件,把想要知道的信息直接写到文件里就好了。
日志文件的路径(路径可以自行替换):
companion object {
...
// 日志文件的路径
const val LOG_FILE_PATH = "D:\\AndroidStudioProjects\\demo\\log.txt"
}
初始化时就创建好日志文件:
override fun init(processingEnv: ProcessingEnvironment?) {
super.init(processingEnv)
// 创建日志文件
createLogFile()
}
...
// 创建日志文件
private fun createLogFile() {
kotlin.runCatching {
val logFile = File(LOG_FILE_PATH)
if (logFile.exists()) {
logFile.delete()
}
logFile.createNewFile()
}
}
// 把信息写到日志文件中,每写入一条信息,就换行一次
private fun logInfo(vararg info: String) {
kotlin.runCatching {
val logFile = File(LOG_FILE_PATH)
if (logFile.exists()) {
info.forEach {
logFile.appendText(it)
logFile.appendText("\n")
}
}
}
}
接着在代码中就可以通过 logInfo("info1", "info2")
这样的形式记录调试信息,在电脑本地打开 log.txt
文件就可以方便查看输出的调试信息了。
后面发现,好像可以使用
processingEnv.messager.printMessage
来打印信息到Build
窗口中
支持的注解及源码版本
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(ExtractField::class.java.name)
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
对注解进行处理
process
方法如下:
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment
): Boolean {
logInfo("Processor.process 方法被调用")
val set = roundEnv.getElementsAnnotatedWith(ExtractField::class.java)
if (set == null || set.isEmpty()) {
return false
}
set.forEach { element ->
if (element.kind != ElementKind.CLASS) {
processingEnv.messager.printMessage(
Diagnostic.Kind.ERROR,
"Only classes can be annotated"
)
return@forEach
}
processAnnotation(element)
}
return true
}
RoundEnvironment:表示当前或是之前的运行环境,可以通过该对象查找指定注解下的节点信息。
process 方法返回值:如果返回 true,则这些注解已处理,后续的「注解处理器」无需再处理它们;如果返回 false,则这些注解未处理并且可能要求后续「注解处理器」处理它们。
父类 AbstractProcessor
提供了一个 processingEnv
实例,可以直接在我们定义的 Processor
里面使用,它提供注解处理的环境,代表了注解处理器框架提供的一个上下文环境,例如它可以提供下面的信息:
- getMessager():返回一个消息器,其可以用于报告错误、警告、或者其它通知。例如原本我们的注解应该只能使用在类上,但是业务方却错误地将它使用在了方法上,就可以使用
Messager
在编译时给出错误信息。 - getElementUtils():返回一个类,这个类具有可以操作 Element 的一些工具方法。
process
方法的逻辑:首先通过 RoundEnvironment
的 getElementsAnnotatedWith
方法获取到被 ExtractField
注解的元素,若获取到的集合为空,则返回 false,否则调用 processAnnotation
方法对 Element 进行处理并返回 true。processAnnotation
方法如下:
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
...
}
...
private fun processAnnotation(element: Element) {
// 获取元素类名、包名
val className = element.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(element).toString()
// 生成类的类名
val fileName = "ExtractField$className"
// 表示一个 kotlin 文件,指定包名和类名
val fileBuilder = FileSpec.builder(pack, fileName)
// 表示要生成的类
val classBuilder = TypeSpec.classBuilder(fileName)
logInfo("className:$className", "pack:$pack", "fileName:$fileName")
// 获取 Element 的子节点,只对字段进行处理
for (childElement in element.enclosedElements) {
if (childElement.kind == ElementKind.FIELD) {
// 向类里添加字段
addProperty(classBuilder, childElement)
logInfo("FieldType:${childElement.asType().asTypeName().asNullable()}")
// 向类里添加字段的 get 方法
addGetFunc(classBuilder, childElement)
// 向类里添加字段的 set 方法
addSetFunc(classBuilder,childElement)
}
}
// 向 fileBuilder 表示的 kotlin 文件中写入 classBuilder 类
val file = fileBuilder.addType(classBuilder.build()).build()
// 获取生成的文件所在的目录
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
logInfo("kaptKotlinGeneratedDir:$kaptKotlinGeneratedDir")
// 将 file 表示的文件写入目录
file.writeTo(File(kaptKotlinGeneratedDir))
}
用到的辅助方法:
// 往 classBuilder 代表的类中添加 element 字段,其中类型为可空的,初始值为 null
private fun addProperty(classBuilder: TypeSpec.Builder, element: Element) {
classBuilder.addProperty(
PropertySpec.varBuilder(
element.simpleName.toString(),
element.asType().asTypeName().asNullable(),
KModifier.PRIVATE
)
.initializer("null")
.build()
)
}
// 往 classBuilder 代表的类中添加 element 的 getter 方法
private fun addGetFunc(classBuilder: TypeSpec.Builder, element: Element) {
classBuilder.addFunction(
FunSpec.builder("getThe${element.simpleName}")
.returns(element.asType().asTypeName().asNullable())
.addStatement("return ${element.simpleName}")
.build()
)
}
// 往 classBuilder 代表的类中添加 element 的 setter 方法
private fun addSetFunc(classBuilder: TypeSpec.Builder, element: Element) {
classBuilder.addFunction(
FunSpec.builder("setThe${element.simpleName}")
.addParameter(
ParameterSpec.builder(
"${element.simpleName}",
element.asType().asTypeName().asNullable()
).build()
)
.addStatement("this.${element.simpleName} = ${element.simpleName}")
.build()
)
}
验证
在 app/build.gradle
中声明如下:
plugins {
...
id 'kotlin-kapt'
}
...
dependencies {
...
// 声明自己定义的注解处理器
kapt project(':compiler')
// 代码中要使用自己定义到的注解
implementation project(path: ':annotation')
}
在 app
模块中声明下面两个类:
@ExtractField
data class Rectangle(val length : Int, val width : Int)
@ExtractField
class Boy {
val age : Int = 3
}
编译项目,接着就可以自动生成下面的两个类:
ExtractFieldRectangle:
class ExtractFieldRectangle {
private var length: Int? = null
private var width: Int? = null
fun getThelength(): Int? = length
fun setThelength(length: Int?) {
this.length = length
}
fun getThewidth(): Int? = width
fun setThewidth(width: Int?) {
this.width = width
}
}
ExtractFieldBoy:
class ExtractFieldBoy {
private var age: Int? = null
fun getTheage(): Int? = age
fun setTheage(age: Int?) {
this.age = age
}
}
顺便看下编译时生成的日志文件 log.txt
:
编译之后就可以在代码中使用刚刚生成的类了:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rectangle : ExtractFieldRectangle = ExtractFieldRectangle()
rectangle.setThelength(20)
rectangle.setThewidth(15)
Log.d("abcde", "length:${rectangle.getThelength()},width:${rectangle.getThewidth()}")
val boy : ExtractFieldBoy = ExtractFieldBoy()
boy.setTheage(18)
Log.d("abcde", "age:${boy.getTheage()}")
}
}
输出结果如下:
D/abcde: length:20,width:15
D/abcde: age:18
原理浅析
我们在代码中注解某些元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor
的子类,调用其 process
方法,方法的参数是添加了该注解的所有代码元素,我们接着在 process
方法中根据注解元素在编译期输出对应的 Java 代码。
看看 java source -> dex
的过程:
其中 JavaCompiler
参与的阶段再细分:
注解处理器 处理的主要步骤:
- 在 java 编译器中构建
- 编译器开始执行未执行过的注解处理器
- 循环处理注解元素(Element),找到被该注解所修饰的类、方法、或者属性
- 生成对应的类,并写入文件
- 判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)
注解的处理是一轮一轮的。当编译到达预编译阶段时,第一轮开始,如果这一轮生成任何带有注解的新文件,则下一轮以生成的文件作为其输入开始。这种情况一直持续到处理器生成的新文件中不含有注解。
kapt
使用 Java 开发 Android 应用,使用注解处理器的话是在 build.gradle
文件中使用 annotationProcessor
引入相关的注解处理器 。使用 Kotlin 开发 Android 应用时,要引入注解处理器就得使用 kapt
,kapt
也是 APT 工具的一种,使用 kapt
需要引入对应的 plugin
:
plugins {
...
id 'kotlin-kapt'
}
前面 app
模块引用注解处理器的 compiler
模块就是使用了 kapt
。
参考
- Idiomatic Kotlin: Annotation Processor and Code Generation - Tompee Balauag。
- Android 注解处理器 - 简书。
- Android注解处理器APT技术探究 - 掘金。
- 含有调试「注解处理器」的介绍:Kotlin版注解处理器Annotation Processor - 掘金。
标签:val,element,处理器,classBuilder,fun,注解 来源: https://www.cnblogs.com/giagor/p/16542367.html