Android APT的学习和使用(一)

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:

  1. 同理,这里要使用kapt而不是使用apt或annotationProcessor,否则我们自定义的Processor无法执行,最终并不会生成我们想要的输出文件;
    此外
  2. 原文链接使用的是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
  1. getSupportedAnnotationTypes()、getSupportedSourceVersion()和getSupportedOptions()这三个方法用注解方式提供
  2. 用AutoService这个库为我们自动注册注解处理器
@AutoService(Processor::class)
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.wzy.apt_annotation.AptTestAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class AptTestAnnotationProcessor: AbstractProcessor() {
	...省略
}
  1. 重写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"]
        }
        
        ...省略
    }
  1. 最主要的还是重写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)重新编译一下工程,生成代码
  1. 生成代码的位置和那篇文章的有些不同
    在这里插入图片描述
  2. 看一下实际生成文件
    在这里插入图片描述
    这里若是不熟悉kotlinPoet的使用,可能一开始生成的代码不对甚至报错,多试几次就可以了。

(四)调用新生成的代码

  1. 原文那篇文章用的是反射,在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()
            }
        }

}
  1. 直接调用也可以
@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对象")
    }
}
  1. 在logcat可以看到
    在这里插入图片描述
    大功告成

 

(五)小结

  1. 用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写的类
在这里插入图片描述
具体的解释可以看这篇文章: 传送门
 

  1. app模块下的build.gradle里可以不使用kapt而继续使用
    //kapt project(path: ':apt-processor')
    annotationProcessor project(path: ':apt-processor')

区别就是annotationProcessor 最终生成代码的位置在
在这里插入图片描述
而kapt在
在这里插入图片描述
(注意,HelloWorld这几个例子用的是JavaPoet写的)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值