Android APT的学习和使用 Kotlin
前言
在掘金看到一篇学习APT系列的文章,对APT有了一定的认识,在实践的过程中遇到一些小问题,主要是原文章用的是Java写的,在kotlin有些许不同,特地记录一下。
原文地址:
Android APT 系列 (一):APT 筑基之反射
Android APT 系列 (二):APT 筑基之注解
Android APT 系列 (三):APT 技术探究
前两个系列主要是了解反射和注解的基本知识,这里主要是针对第三篇文章在Kotlin环境下的实现。
正文
(一)APT工程
(1)建立工程
右键建立新的Module,在弹出的界面分别选择Java Or Kotlin Library以及Android Library,其中:
apt-annotation: 声明注解类,类型选择Java Or Kotlin Library
apt-processor: 处理注解并自动生成代码,类型选择Java Or Kotlin Library
apt-api: 反射获取生成的类,给上层调用,类型选择Android Library
如下图:
(2)处理每个Module的依赖
分别进入每个module的build.gradle文件,
app:
这里主要不同的地方是使用kapt而不是使用apt或annotationProcessor,否则我们自定义的Processor无法执行,最终并不会生成我们想要的输出文件
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jetbrains.kotlin.kapt'
defaultConfig {
...省略
javaCompileOptions {
annotationProcessorOptions {
arguments = [MODULE_NAME: project.getName()]
}
}
}
dependencies {
...省略
implementation project(path: ':apt-api') //需要调用apt-api封装好的类,存在依赖关系
implementation project(path: ':apt-annotation') //需要使用apt-annotation声明的注解,存在依赖关系
kapt project(path: ':apt-processor') //Annotation Processing for Kotlin ,即用于kotlin的apt, 不使用apt和annotationProcessor
}
apt-annotation:
plugins {
id 'java-library'
id 'kotlin'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
apt-processor:
- 同理,这里要使用kapt而不是使用apt或annotationProcessor,否则我们自定义的Processor无法执行,最终并不会生成我们想要的输出文件;
此外 - 原文链接使用的是javaPoet来生成我们想要的代码,因为现在使用的是kotlin,所以使用kotlinPoet这个库,Github地址
plugins {
id 'java-library'
id 'kotlin'
id 'org.jetbrains.kotlin.kapt'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.google.auto.service:auto-service:1.0-rc6'
//annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
kapt 'com.google.auto.service:auto-service:1.0-rc6'
//implementation 'com.squareup:javapoet:1.13.0'
implementation "com.squareup:kotlinpoet:1.9.0"
implementation project(path: ':apt-annotation')
}
apt-api:
这个没有修改,用创建Android Module自动生成的build.gradle就好了,不过相关设置,例如最低版本这些要和app保持一致,否则编译不通过
plugins {
id 'com.android.library'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.1"
defaultConfig {
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
(二)在apt-annotation模块下定义我们想要的注解
新建一个AptTestAnnotation注解
@Inherited
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class AptTestAnnotation(val desc: String = "")
(三)在apt-processor模块下处理注解
(1)新建一个类继承AbstractProcessor
- getSupportedAnnotationTypes()、getSupportedSourceVersion()和getSupportedOptions()这三个方法用注解方式提供
- 用AutoService这个库为我们自动注册注解处理器
@AutoService(Processor::class)
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.wzy.apt_annotation.AptTestAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class AptTestAnnotationProcessor: AbstractProcessor() {
...省略
}
- 重写init方法获取moduleName以及filer
/**
* 节点工具类(类、函数、属性都是节点)
*/
private var mElementUtils: Elements? = null
/**
* 类信息工具类
*/
private var mTypeUtils: Types? = null
/**
* 文件生成器
*/
private var mFiler: Filer? = null
/**
* 日志信息打印器
*/
private var mMessager: Messager? =null
//模块名
private var mModuleName: String? = null
/**
* 做一些初始化的工作
*
* @param processingEnvironment 这个参数提供了若干工具类,供编写生成 Java 类时所使用
*/
override fun init(processingEnvironment: ProcessingEnvironment?) {
super.init(processingEnvironment)
processingEnvironment?.apply {
mElementUtils = elementUtils
mTypeUtils = typeUtils
mFiler = filer
mMessager = messager
mModuleName = options["MODULE_NAME"]
}
...省略
}
- 最主要的还是重写processor方法,完成我们获取注解节点下的信息并利用该信息用kotlinPoet生成我们所需的代码
- 目标:希望在用注解标注过的类或方法能够打印出来
例如:
@AptTestAnnotation( desc = "我是 Activity 上面的注解")
class AptActivity : AppCompatActivity() {
@AptTestAnnotation("我是 onCreate 上面的注解")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_apt)
AptTestAnnotationApi.init()
}
}
那么要生成的代码应该是:
println("我是 Activity 上面的注解")
println("我是 onCreate 上面的注解")
当然还需要方法体以及类,并且我们可以输出多一些内容。
(2)重写processor方法
在processor方法中我们获取当前注解下的节点信息,先看一下我最终希望生成的代码:
public class Test {
public fun test(param: String): {
println("输入参数: $param ---- 模块: app")
println("输入参数: $param ---- 节点: AptActivity 描述: 我是 Activity 上面的注解")
println("输入参数: $param ---- 节点: onCreate 描述: 我是 onCreate 上面的注解")
}
}
很正经的kotlin代码,那么现在就需要利用kotlinPoet生成这些代码,
override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {
if (p0.isNullOrEmpty()) return false
//获取当前注解下的节点信息
val rootElements = p1?.getElementsAnnotatedWith(AptTestAnnotation::class.java)
// 构建 test 函数
val builder = FunSpec.builder("test")
.addModifiers(KModifier.PUBLIC)// 指定方法修饰符
.returns(Unit::class)// 指定返回类型
.addParameter("param", String::class)// 添加参数
.addStatement("println(%P)", "输入参数: \$param ---- 模块: $mModuleName")
if (!rootElements.isNullOrEmpty()) {
rootElements.forEach {
//当前节点名称
val elementName = it.simpleName.toString()
//当前节点下注解的参数的值
val desc = it.getAnnotation(AptTestAnnotation::class.java).desc
// 编写打印输出的代码
builder.addStatement("println(%P)", "输入参数: \$param ---- 节点: $elementName 描述: $desc")
}
}
val testFunction = builder.build()
// 构建 Test 类
val testClass = TypeSpec.classBuilder("Test")
.addModifiers(KModifier.PUBLIC) // 指定类修饰符
.addFunction(testFunction) // 添加方法
.build()
// 指定包路径,构建AnnotationFile文件体
val ktFile = FileSpec
.builder("com.wzy.demo", "AnnotationFile")
.addType(testClass)
.build()
try {
mFiler?.let {
ktFile .writeTo(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
return true
}
(3)重新编译一下工程,生成代码
- 生成代码的位置和那篇文章的有些不同
- 看一下实际生成文件
这里若是不熟悉kotlinPoet的使用,可能一开始生成的代码不对甚至报错,多试几次就可以了。
(四)调用新生成的代码
- 原文那篇文章用的是反射,在apt-api模块下编写代码通过反射获取Test类,然后封装好上层api给AptActivity调用
object AptTestAnnotationApi {
fun init() {
try {
val c = Class.forName("com.wzy.demo.Test")
val declaredConstructor = c.getDeclaredConstructor()
val o: Any = declaredConstructor.newInstance()
val test: Method = c.getDeclaredMethod("test", String::class.java)
test.invoke(o, "这是反射")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
- 直接调用也可以
@AptTestAnnotation( desc = "我是 Activity 上面的注解")
class AptActivity : AppCompatActivity() {
@AptTestAnnotation("我是 onCreate 上面的注解")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_apt)
AptTestAnnotationApi.init()
Test().test("这是直接new对象")
}
}
- 在logcat可以看到
大功告成
(五)小结
- 用kotlin编写的processor,若是不在该模块的build.gradle指定
kapt 'com.google.auto.service:auto-service:1.0-rc6'
并且添加plugin
apply plugin: 'org.jetbrains.kotlin.kapt'
则用kotlin编写的processor不会被AutoService识别到,导致最终没有生成我们想要的代码,因为该Processor根本没有执行。打开AutoService为我们注册的文件就可以发现并没有kotlin写的那个类
而用了kapt后,该META-INF文件则在这个位置,里面有我们用kotlin写的类
具体的解释可以看这篇文章: 传送门
- app模块下的build.gradle里可以不使用kapt而继续使用
//kapt project(path: ':apt-processor')
annotationProcessor project(path: ':apt-processor')
区别就是annotationProcessor 最终生成代码的位置在
而kapt在
(注意,HelloWorld这几个例子用的是JavaPoet写的)