本文代码可以在 giagor/AptGo - github 找到
最近在学习 Dagger
的时候,发现写几个注解然后编译,Dagger
就可以生成一些类给我们使用,感觉很神奇,所以就找了些资料学习一波。这种处理的技术被称作 Annotation Processing Tool
(APT),即注解处理器。
处理注解有两种方法:
Retrofit
就是通过该方法处理注解。Dagger
使用的是这种方法。注解处理器(APT):javac 的一种处理注解工具,用来在编译期扫描和处理注解,通过注解来生成 Java 文件,它只能生成新的源文件而不能修改已经存在的源文件。通过这种方式,可以让我们编程中减少很多的代码,解放生产力。
在实现注解处理器之前,需要先了解 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); }
功能:实现一个可以对类进行标注的注解,在编译的时候可以自动生成一个类,该类含有原来类的成员变量,并且对外提供 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
文件,用于记录一些日志信息,方便调试,实际开发中不需要这个。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
里面使用,它提供注解处理的环境,代表了注解处理器框架提供的一个上下文环境,例如它可以提供下面的信息:
Messager
在编译时给出错误信息。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 开发 Android 应用,使用注解处理器的话是在 build.gradle
文件中使用 annotationProcessor
引入相关的注解处理器 。使用 Kotlin 开发 Android 应用时,要引入注解处理器就得使用 kapt
,kapt
也是 APT 工具的一种,使用 kapt
需要引入对应的 plugin
:
plugins { ... id 'kotlin-kapt' }
前面 app
模块引用注解处理器的 compiler
模块就是使用了 kapt
。