Kotlin版注解处理器Annotation Processor
文章目录
- 什么是注解
- 创建注解
- 什么是注解处理器
- 编写注解处理器
- 使用注解处理器生成代码
- 在Android项目中使用注解处理器
- 调试注解处理器
- 在处理器中记录日志和处理错误
- 分析种类、数据类型和可见性修饰符的代码元素
注解处理是为 Java程序生成代码的强大工具。在本文中,将开发一个注解和一个注解处理器,为给定的 Activity 类根据路由参数自动生成初始化路由信息的代码。
注意:本文代码全部是 Kotlin 语言编写。
什么是注解
引用《Java编程思想》第20章的注解定义:
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。
Java 内置了三种标准注解:
@Override
:表示当前的方法定义将覆盖超类中的方法;@Deprecated
:使用此注解使编译器发出警告;@SuppressWarnings
:关闭不当的编译器警告信息;
Java还另外提供了四种注解,专门负责新注解的创建,这些注解也被称为元注解(meta-annotation)。
注解 | 含义 |
---|---|
@Target
|
表示该注解可以用于什么地方。可能的 ElementType 包括:CONSTRUCTOR :构造器的声明,FIELD :域声明(包括enum实例),LOCAL_VARIABLE :局部变量声明 ,METHOD :方法声明 ,PACKAGE :包声明 ,PARAMETER :参数声明,TYPE :类、接口(包括注解类型)、或者enum声明
|
@Retention
|
表示需要在什么级别保存该注解信息。可选的 RetentionPolicy 参数包括:SOURCE :注解将被编译器丢弃,CLASS :注解在class文件中可用,但会被VM丢弃,RUNTIME :VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息
|
@Documented
|
将此注解包含在Javadoc中。 |
@Inherited
|
允许子类继承父类中的注解。 |
创建注解
第一步创建一个新模块来保存我们的注解。
将注解和处理器保存在单独的模块中是一种常见的做法。
选择 File ▸ New ▸ New Module,然后选择 Java or Kotlin library ,模块名称为annotations
,类名Router
,语言记得选择 Kotlin,填写信息后完成创建。
创建了一个注解Router
,代码如下:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Router(val url: String)
@Target
使用TYPE
代表我们将只在类上使用Router
,@Retention
使用SOURCE
代表 Router
只需要在源代码编译阶段存在。
注解可以有参数,允许我们添加更多信息。
什么是注解处理器
注解处理器可以帮助我们事半功倍,也就是说,更少的代码(注解)神奇地变成了更多的功能。以下是注解处理器的介绍:
注解处理是javac内置的一个工具,用于在编译时扫描和处理注解。
它可以创建新的源文件;但是,它不能修改现有的。
它是轮流完成的。当编译到达预编译阶段时,第一轮开始。如果这一轮生成任何新文件,则下一轮以生成的文件作为其输入开始。这种情况一直持续到处理器处理完所有新文件。
javac 是java语言编程编译器。全称java compiler。javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。
下图说明了该过程:
编写注解处理器
再次创建一个Java or Kotlin library 模块,模块名称为processor
,类名Processor
,语言依旧选择Kotlin,处理器需要用到我们的自定义注解。因此,打开 processor/build.gradle
并在依赖项块中添加以下内容:
implementation project(':annotations')
打开 Processor.kt 并将导入和类声明替换为以下内容:
import com.guiying712.annotations.Router
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedSourceVersion
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement@SupportedSourceVersion(SourceVersion.RELEASE_8) // 1
class Processor : AbstractProcessor() { // 2override fun getSupportedAnnotationTypes() = mutableSetOf(Router::class.java.canonicalName) // 3override fun process(annotations: MutableSet<out TypeElement>?,roundEnv: RoundEnvironment): Boolean { // 4// TODOreturn true // 5}
}
下面解释下上面的代码:
@SupportedSourceVersion
指定此处理器支持 Java 8所有注解处理器都必须继承
AbstractProcessor
类。getSupportedAnnotationTypes()
定义了该处理器在运行时查找的一组注解。如果目标模块中的任何元素都没有使用该集合中的注解进行注释,则处理器将不会运行。process
是在每个注解处理轮次中调用的核心方法。如果一切顺利,
process
必须返回true
。
接下来我们将注册这个注解处理器,为此,我们必须创建一个特殊文件:
我们必须使用 javac 注册处理器,以便编译器知道在编译期间调用它。
展开 processor
▸ src
▸ main
,添加一个名为 resources
的新目录。然后在资源中添加一个子目录并将其命名为 META-INF
(必须大写字母)。最后,在META-INF
添加一个命名为 services
的子目录。在services
添加一个空文件并将其命名为 javax.annotation.processing.Processor
。
打开文件javax.annotation.processing.Processor
并将我们创建的处理器的完全限定名称作为其内容。如下所示:
com.guiying712.processor.Processor
现在编译器知道我们的自定义处理器,并将在其预编译阶段运行它。
当然上面的方式实在是过于繁琐,因此Google给我开发了自动注册的工具 AutoService ,打开 processor/build.gradle
依赖项块中添加以下内容:
implementation "com.google.auto.service:auto-service:1.2.1"
打开 Processor.kt 并添加注解 @AutoService(Processor.class)
,如下所示:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class Processor : AbstractProcessor() {
AutoService 将在输出类文件夹中生成文件 META-INF/services/javax.annotation.processing.Processor
。该文件将包含:
com.guiying712.processor.Processor
使用注解处理器生成代码
创建一个Android library 模块用来保存路由框架相关的类,模块名称为router
,语言依旧选择Kotlin。
然后新建一个类RouterAnnotationHandler,用于处理路由表,所有的路由信息都会交由此类处理,代码如下:
package com.guiying712.routerinterface RouterAnnotationHandler {fun register(url: String, target: String)}
再新建一个类RouterAnnotationInit ,用于初始化Router注解标记的路由信息,然后交由handler去处理,代码如下:
package com.guiying712.routerinterface RouterAnnotationInit {fun init(handler: RouterAnnotationHandler)}
注解处理器最终生成的代码如下:
package com.guiying712.router.generatedimport com.guiying712.router.RouterAnnotationHandler
import com.guiying712.router.RouterAnnotationInitclass AnnotationInit_efb0660a2fd741f3a44a1d521a6f6b18 : RouterAnnotationInit {override fun init(handler: RouterAnnotationHandler) {handler.register("/demo", "com.guiying712.demo.MainActivity")handler.register("/demo2", "com.guiying712.demo.LoginActivity")}
}
接下来我们就编写注解处理器生成以上代码。
我们将使用 KotlinPoet 生成 Kotlin 源代码文件,打开 processor/build.gradle
并添加以下依赖项:
implementation 'com.squareup:kotlinpoet:1.10.2'
注解处理创建的源代码文件必须位于一个特殊的文件夹中,这个文件夹的路径是 kapt/kotlin/generated
。
为了告诉注解处理器将其生成的文件放在那里,在 Processor
类中添加一个伴生对象, 并添加以下代码:
companion object {const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
然后在 process
方法第一行中添加以下代码:
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ?: return false
这行代码的意思检查处理器是否能够找到必要的文件夹并将文件写入其中。如果可以,处理器将返回提供的使用路径。否则,处理器将中止并从 process
方法返回 false
。
新建一个文件,并将其命名为 RouterCodeBuilder.kt
:
class RouterCodeBuilder(private val kaptKotlinGeneratedDir: String,private val fileName: String,private val code: CodeBlock
) {}
构造函数有三个参数:要生成代码的路径,正在编写的类的名称、代码块。然后在此类中添加一些常量:
private val routerAnnotationInit = ClassName("com.guiying712.router", "RouterAnnotationInit") // 1
private val routerAnnotationHandler = ClassName("com.guiying712.router", "RouterAnnotationHandler") // 2
private val generatedPackage = "com.guiying712.router.generated" // 3
ClassName
是一个 KotlinPoet API 类,它包装了一个类的完全限定名称,在生成的 Kotlin 源文件的顶部创建必要的导入。
- 代表要导入的 RouterAnnotationInit 类;
- 代表要导入的 RouterAnnotationHandler 类;
- 生成类的包名;
fun buildFile() = FileSpec.builder(generatedPackage, fileName) // 1.addInitClass() // 2.build().writeTo(File(kaptKotlinGeneratedDir)) // 3
解释下上面的代码::
- 定义一个包名
generatedPackage
,名称为fileName
的文件; - 向文件中添加一个名为
fileName
的类; - 将生成的文件写入 kaptKotlinGeneratedDir 文件夹。
KotlinPoet 使用 TypeSpec
来定义类代码。
一个有用的技巧是在 FileSpec.Builder 上创建一个私有扩展函数,以便我们可以将代码片段整齐地插入到在上面创建的buildFile()
方法调用链中。
private fun FileSpec.Builder.addInitClass() = apply { // 1addType(TypeSpec.classBuilder(fileName) // 2.addSuperinterface(routerAnnotationInit) // 3.addInitMethod(code) // 3.build())}
解释下上面的代码::
addInitClass
是FileSpec.Builder
的一个扩展,它对其执行以下操作:- 向文件中添加一个名为
fileName
的类; - 此 类实现了
routerAnnotationInit
接口;
private fun TypeSpec.Builder.addInitMethod(code: CodeBlock) = apply { // 1addFunction(FunSpec.builder("init") // 2.addModifiers(KModifier.OVERRIDE) // 3.addParameter("handler", routerAnnotationHandler) // 4.returns(UNIT) // 5.addCode(code) // 6.build())}
解释下上面的代码::
addInitMethod
是TypeSpec.Builder
的一个扩展,它对其执行以下操作:- 向类中添加一个名为
init
的方法; - 该方法重写了一个抽象方法;
addParameter
将参数添加到函数定义中,此方法覆盖的init
方法有一个参数:handler
;- 返回一个
UNIT
; - 将代码块添加到方法体中;
最后一步是将其插入处理器。打开 Processor.kt 并将 process
方法的 TODO
替换为:
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {if (annotations == null || annotations.isEmpty()) return false // 1val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ?: return falseval codeBuilder = CodeBlock.Builder() // 2val fileName = "AnnotationInit" + "_" + UUID.randomUUID().toString().replace("-", "") // 3roundEnv.getElementsAnnotatedWith(Router::class.java).forEach { element -> // 4 val annotation = element.getAnnotation(Router::class.java) // 5val url = annotation.urlval className = element.simpleName.toString() // 6val packageName = processingEnv.elementUtils.getPackageOf(element).toString() // 7val target = "$packageName.$className" // 8codeBuilder.addStatement("handler.register(%S, %S)", url, target) // 9}RouterCodeBuilder(kaptKotlinGeneratedDir, fileName, codeBuilder.build()).buildFile() // 10return true }
解释下上面的代码:
- 如果这一轮次的根元素没有注解,则 annotation 集合将为空,返回
false
,表示我们自定义的注解处理器将不在处理; - CodeBlock 是
init
方法的方法体,由于一个模块中可能会有多个Activity
被Router
注解标注,因此我们需要将所有被Router标注的Activity都收集起来; - 要生成文件的名称是
AnnotationInit
加一个随机的UUID,防止多个模块之间生成的文件重复; - element 是用 Router 注解标注的元素,在本例中是 Activity 类;
- 获取到此元素的注解,即
Router
注解,用于获取 Router 中的参数 url; - 获取到此元素的简单名称,即 Activity 的类名;
- 获取到此元素的包名,即 Activity 的包名;
- 生成目标Activity的全限定名称,例如:
com.guiying712.android.MainActivity
- 组装方法体。KotlinPoet 有自己的 字符串格式化标志;
- 根据以上信息生成
RouterInit_X
类 ,切记每个模块只应该生成一个RouterInit_X
类。
当发出包含字符串文字的代码时,我们可以使用
%S
发出一个字符串,并带有引号和转义。
现在自定义处理器就能找到使用Router注解的代码元素,从中提取数据,然后根据该信息生成新的Kotlin源文件。
在Android项目中使用注解处理器
打开 app/build.gradle
并将下面的依赖项添加到其中:
implementation project(':annotations') // 1
implementation project(':router') // 2
kapt project(':processor') // 3
打开 MainActivity.kt 并使用 Router注解该类:
@Router("/mian")
class MainActivity : AppCompatActivity() {}
最后构建并运行项目,等结束后打开build文件夹,按照下图查找生成的文件:
调试注解处理器
1、选择编辑配置:
2、然后新建 Remote 配置,命名并保存:
3、打开 AndroidStudio 的 Terminal ,由于我们是在 Kotlin Kapt 编译期调试代码,所以执行以下命令:
gradlew clean build --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy="in-process" -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n"
如果是 Java AnnotationProcessor 编译期调试代码,则执行以下命令:
gradlew.bat --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
4、当命令行执行到 Starting Daemon 时,在需要调试的地方打上断点,然后运行 debug 按钮,稍等一下(速度比较慢耐心点) 就会在AbstractProcessor 中进入到断点所在位置,然后就可以一步步进行调试了。
在处理器中记录日志和处理错误
在注解处理时我们可以通过添加日志来打印调试说明、警告和错误,这些消息将与其他构建任务一起显示在构建输出窗口中。
新建一个 ProcessorLogger.kt,向其添加以下代码:
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.Element
import javax.tools.Diagnosticclass ProcessorLogger(private val env: ProcessingEnvironment) {fun n(message: String, element: Element? = null) { print(Diagnostic.Kind.NOTE, message, element) // 1}fun w(message: String, element: Element? = null) {print(Diagnostic.Kind.WARNING, message, element) // 1}fun e(message: String, element: Element? = null) {print(Diagnostic.Kind.ERROR, message, element) // 1}private fun print(kind: Diagnostic.Kind, message: String, element: Element?) {print("\n")env.messager.printMessage(kind, message, element) // 2}
}
解释下上面的代码:
- 提供三种级别的日志记录方法:
n
表示注释,w
表示警告,e
表示错误; - 三种方法都使用
print
,在element
的位置上打印指定类型的消息。
接下来打开 Processor.kt
。在类中添加以下代码:
private val logger by lazy { ProcessorLogger(processingEnv) }
现在就可以用 logger
在注解处理时打印日志了。
分析种类、数据类型和可见性修饰符的代码元素
开始前,先简单介绍下一些基础的代码元素知识。
Element
表示一个程序元素,比如包、类或者方法。每个元素都表示一个静态的语言级构造(不是虚拟机的运行时构造)。
ExecutableElement
|
表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。 |
PackageElement
|
表示一个包程序元素。提供对有关包及其成员的信息的访问。 |
TypeElement
|
表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注释类型是一种接口。 |
TypeParameterElement
|
表示一般类、接口、方法或构造方法元素的形式类型参数。 |
VariableElement
|
表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。 |
可以通过 使用 element.getKind()
方法返回此元素的类型 ElementKind
,以下是 ElementKind
的全部枚举:
类型枚举 | 类型含义 |
---|---|
PACKAGE
|
一个包 |
CLASS
|
没有用更特殊的种类(如 ENUM)描述的类 |
INTERFACE
|
没有用更特殊的种类(如 ANNOTATION_TYPE)描述的接口。 |
ENUM
|
一个枚举类型 |
ENUM_CONSTANT
|
一个枚举常量 |
ANNOTATION_TYPE
|
一个注解类型 |
CONSTRUCTOR
|
一个构造方法 |
FIELD
|
没有用更特殊的种类(如 ENUM_CONSTANT)描述的字段 |
METHOD
|
方法 |
PARAMETER
|
方法或构造方法的参数 |
LOCAL_VARIABLE
|
局部变量 |
EXCEPTION_PARAMETER
|
异常处理程序的参数 |
INSTANCE_INIT
|
一个常量初始化程序 |
STATIC_INIT
|
一个静态初始化程序 |
TYPE_PARAMETER
|
一个类型参数 |
OTHER
|
一个为实现保留的元素 |
由于本文的目的是为了使用 Router
注解标注Activity
来生成路由表单,因此我们需要检验Router
标注的元素是否符合要求。
在 Processor.kt 添加一个方法来验证 @Router
标注的类是否满足条件:
private fun validateActivity(element: Element): Boolean {(element as? TypeElement)?.let { // 1if (!processingEnv.typeUtils.isSubtype(element.asType(), processingEnv.elementUtils.getTypeElement("android.app.Activity").asType())) { // 2logger.e("Router注解只能标注Activity", element)return false}val modifiers = it.modifiersif (Modifier.ABSTRACT in modifiers) { // 3logger.e("Activity不可以是抽象类", element)return false}return true} ?: return false}
此方法验证三个条件:
- 首先,通过检查元素是否为 TypeElement来检查元素是否为类;
- 然后检查元素的 类型 是否是 Activity类型,这需要
typeUtils
和elementUtils
; - 最后,确保Activity的修饰符不是 ABSTRACT 。
在 process
方法中使用这个方法:
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {if (annotations == null || annotations.isEmpty()) return falseval kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ?: return falseval codeBuilder = CodeBlock.Builder()val fileName = "AnnotationInit" + "_" + UUID.randomUUID().toString().replace("-", "")roundEnv.getElementsAnnotatedWith(Router::class.java) // 1.forEach { element -> // 2if (!validateActivity(element)) return falseval annotation = element.getAnnotation(Router::class.java)val className = element.simpleName.toString()val url = annotation.urlval packageName = processingEnv.elementUtils.getPackageOf(element).toString()val target = "$packageName.$className"codeBuilder.addStatement("handler.register(%S, %S)", url, target)logger.n("Router located: $target \n") // 5}RouterCodeBuilder(kaptKotlinGeneratedDir, fileName, codeBuilder.build()).buildFile()return true // 5}
最后可以写一个类,然后使用@Router
标注此类,构建并运行,看下编译结果。
本文中的源代码如下:
https://github.com/guiying712/KotlinRouter
Kotlin版注解处理器Annotation Processor相关推荐
- JAVA 注解 processor_注解处理器(Annotation Processor)简析
概念 注解处理器(Annotation Processor)是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具.简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件 ...
- Java注解处理器使用详解
文章转自:http://www.race604.com/annotation-processing/ 在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor).在这篇教程 ...
- java 注解处理器的作用_Java注解处理器
Java中的注解(Annotation)是一个很神奇的东西,特别现在有很多Android库都是使用注解的方式来实现的.一直想详细了解一下其中的原理.很有幸阅读到一篇详细解释编写注解处理器的文章.本文的 ...
- Java注解处理器(编译时注解)
https://race604.com/annotation-processing/ Java中的注解(Annotation)是一个很神奇的东西,特别现在有很多Android库都是使用注解的方式来实现 ...
- Android 自定义注解处理器
之前我们可能用过dagger.hilt之类的注解,使用这些注解可以方便我们的工作,减少我们的代码编写量.因此,本文主要是介绍如何自定义一个注解处理器.可以分为2个部分,一.定义注解和注解处理器:二.注 ...
- 【Android 组件化】路由组件 ( 注解处理器参数选项设置 )
文章目录 一.注解处理器 接收参数设置 二.注解处理器 生成路由表 Java 代码 三.博客资源 组件化系列博客 : [Android 组件化]从模块化到组件化 [Android 组件化]使用 Gra ...
- 【错误记录】Android 编译时技术报错 ( 注解处理器 process 方法多次调用问题 )
文章目录 一.报错信息 二.问题分析 三.解决方案 注解处理器 AbstractProcessor 中的 process 方法可能会调用多次 , 在生成代码时 , 一定要注意 , 检测到 注解节点 后 ...
- 【Android 组件化】路由组件 ( 注解处理器获取被注解的节点 )
文章目录 一.设置支持的注解类型 二.注解处理器中打印日志 三.主应用中使用注解 四.注解处理器 获取注解节点 五.博客资源 组件化系列博客 : [Android 组件化]从模块化到组件化 [Andr ...
- 【Android APT】注解处理器 ( 根据注解生成 Java 代码 )
文章目录 一.生成 Java 代码 二.实现 IButterKnife 接口 三.视图绑定主要操作 四.完整注解处理器代码 五.博客资源 Android APT 学习进阶路径 : 推荐按照顺序阅读 , ...
最新文章
- 【Visual C++】游戏开发笔记二十七 Direct3D 11入门级知识介绍
- [转载] 百科全说——王晓斋:解析中西医应对肝肾问题(10-10-12)
- pythondataframe如何替换值_如何将pandas dataframe中的字符串值替换为整数?
- Qt Console Application 与 Qt GUI Application互转
- 张文宏:WHO新型冠状病毒的传言和事实
- SQL Server Alwayson概念总结
- 椒盐噪声 Python实现
- Flutter进阶—实现动画效果(七)
- BABOK - BA计划和监控(BA Planning Monitoring)概要
- HttpClient3.x之Get请求和Post请求示例
- android社交软件源码,Android 社交App+java web后台整套源代码
- 数据库 ER图、关系模式相互转换 关系代数表达式 查询树,优化查询树 SQL题目
- 怎么看matlab程序运行到哪了,MATLAB运行程序时怎么查看还有多久完成
- 安卓TV开发遇到的那些坑
- 中北计算机组成原理期末,中北大学《计算机组成原理》选择题和填空题试题大集合(含答案).pdf...
- 多元逻辑回归 · 数学推导过程及代码实现完全解析
- B站喷子脚本,bilibili喷人
- 1-十八烷基-3-三乙氧基丙基硅烷咪唑溴盐离子液体([ODTIm]Br)修饰Fe3O4磁性纳米颗粒
- python判断对错题_Python爬虫自动化获取华图和粉笔网站的错题(推荐)
- 《中国化工贸易》征稿函
热门文章
- 什么是数据安全,为什么需要保证数据安全
- 第2关:Pandas创建透视表和交叉表
- 深度学习笔记(四十一)循环神经网络及正反向传播
- 学习如何合理的配置服务器
- [NACOS HTTP-POST] The maximum number of tolerable server reconnection errors has been reached
- Windows Identity Foundation-- Windows身份验证基本框架
- 怎么从扫描的PDF文档/图片里提取文字
- ios开发者联系方式
- Notes 20180311 : String第三讲_深入了解String
- 1079 延迟的回文数 (20 分) java题解