Kotlin学习手记——注解注解处理器编译器插件

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin学习手记——注解注解处理器编译器插件相关的知识,希望对你有一定的参考价值。

注解定义:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Api(val url: String)

@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Path(val name: String = "")

@Target(AnnotationTarget.FUNCTION)
annotation class Get(val name: String)

使用annotation关键字修饰calss前面,比java的 @interface更人性化,其中@Retention有三种:

public enum class AnnotationRetention 
    /** Annotation isn't stored in binary output */
    SOURCE,
    /** Annotation is stored in binary output, but invisible for reflection */
    BINARY,
    /** Annotation is stored in binary output and visible for reflection (default retention) */
    RUNTIME

分别表示作用时机是在源码级、编译期、还是运行时,跟java基本类似。

@Target指定限定标注对象,取值如下:

public enum class AnnotationTarget 
    /** Class, interface or object, annotation class is also included */
    CLASS,
    /** Annotation class only */
    ANNOTATION_CLASS,
    /** Generic type parameter (unsupported yet) */
    TYPE_PARAMETER,
    /** Property */
    PROPERTY,
    /** Field, including property's backing field */
    FIELD,
    /** Local variable */
    LOCAL_VARIABLE,
    /** Value parameter of a function or a constructor */
    VALUE_PARAMETER,
    /** Constructor only (primary or secondary) */
    CONSTRUCTOR,
    /** Function (constructors are not included) */
    FUNCTION,
    /** Property getter only */
    PROPERTY_GETTER,
    /** Property setter only */
    PROPERTY_SETTER,
    /** Type usage */
    TYPE,
    /** Any expression */
    EXPRESSION,
    /** File */
    FILE,
    /** Type alias */
    @SinceKotlin("1.1")
    TYPEALIAS


注解类的参数是有限的,必须是能在编译期确定的类型。

简单使用:

@Api("https://api.github.com")
interface GitHubApi 

    @Get("/users/name")
    fun getUser(@Path name: String): User


class User


第一个标注注解的注解主要是指前面的@Retention@Target之类的,是写在注解类上的注解。


@file:JvmName("KotlinAnnotations")
@file:JvmMultifileClass
package com.bennyhuo.kotlin.annotations.builtins

import java.io.IOException

@Volatile
var volatileProperty: Int = 0

@Synchronized
fun synchronizedFunction()



val lock = Any()
fun synchronizedBlock()
    synchronized(lock) 

    


@Throws(IOException::class)
fun throwException()


@Synchronized @Throws注解都是比较好用的,替代java的相应关键字,比较人性化了。其中 @file:JvmName("KotlinAnnotations")@file:JvmMultifileClass比较有意思,能让多个文件中的kotlin代码最终生成到一个类里面,假如还有一个文件如下:

@file:JvmName("KotlinAnnotations")
@file:JvmMultifileClass
package com.bennyhuo.kotlin.annotations.builtins

fun hello()


那经过编译之后,这个文件会和上面的文件合并到一起,生成到一个kotlin类文件当中。

实例:仿 Retrofit 反射读取注解请求网络

data class User(
    var login: String,
    var location: String,
    var bio: String)

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Api(val url: String)

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Path(val url: String = "")

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Get(val url: String = "")

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class PathVariable(val name: String = "")

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Query(val name: String = "")

@Api("https://api.github.com")
interface GitHubApi 

    @Api("users")
    interface Users 

        @Get("name")
        fun get(name: String): User

        @Get("name/followers")
        fun followers(name: String): List<User>

    

    @Api("repos")
    interface Repos 

        @Get("owner/repo/forks")
        fun forks(owner: String, repo: String)

    


object RetroApi 
    const val PATH_PATTERN = """(\\(\\w+)\\)"""

    val okHttp = OkHttpClient()
    val gson = Gson()

    val enclosing = 
        cls: Class<*> ->
        var currentCls: Class<*>? = cls
        sequence 
            while(currentCls != null)
                // enclosingClass获取下一个class
                // yield将对象添加到正在构建的sequence序列当中
                currentCls = currentCls?.also  yield(it) ?.enclosingClass
            
        
    

    //内联特化
    inline fun <reified T> create(): T 
        val functionMap = T::class.functions.map it.name to it .toMap() //【函数名,函数本身】的Pair转成map
        val interfaces = enclosing(T::class.java).takeWhile  it.isInterface .toList() //拿到所有接口列表

        println("interfaces= $interfaces")// 输出 [GitHubApi$Users,  GitHubApi]

        //foldRight从interfaces序列的右边开始拼
        val apiPath = interfaces.foldRight(StringBuilder()) 
            clazz, acc ->
            // 拿到每个接口类的Api注解的url参数值,如果url参数为空,则使用类名作为url值
            acc.append(clazz.getAnnotation(Api::class.java)?.url?.takeIf  it.isNotEmpty()  ?: clazz.name)
                .append("/")
        .toString()

        println("apiPath= $apiPath") // https://api.github.com/users/

        //动态代理
        return Proxy.newProxyInstance(RetroApi.javaClass.classLoader, arrayOf(T::class.java)) 
            proxy, method, args ->
            //所有函数中的抽象函数 即接口的方法
            functionMap[method.name]?.takeIf  it.isAbstract ?.let 
                function ->

                //方法的参数
                val parameterMap = function.valueParameters.map 
                    //参数名和参数的值放在一起
                    it.name to args[it.index - 1] //valueParameters包含receiver 因此需要index-1来对应args
                .toMap()

                println("parameterMap= $parameterMap") //name=bennyhuo

                //name 拿到Get注解的参数 如果注解参数不为空就使用注解参数,如果为空使用方法名称
                val endPoint = function.findAnnotation<Get>()!!.url.takeIf  it.isNotEmpty()  ?: function.name

                println("endPoint= $endPoint") //name/followers

                //正则找到endPoint中的所有符合"owner/repo/forks"其中xxx的结果
                val compiledEndPoint = Regex(PATH_PATTERN).findAll(endPoint).map 
                    matchResult ->
                    println("matchResult.groups= $matchResult.groups") // [MatchGroup(value=name, range=0..5), MatchGroup(value=name, range=0..5), MatchGroup(value=name, range=1..4)]
                    println("matchResult.groups1.range= $matchResult.groups[1]?.range") // 0..5
                    println("matchResult.groups2.value= $matchResult.groups[2]?.value") // name
                    matchResult.groups[1]!!.range to parameterMap[matchResult.groups[2]!!.value]
                .fold(endPoint) 
                    acc, pair ->
                    //acc的初始值就是endPoint即name/followers
                    println("acc= $acc") //  name/followers
                    println("pair= $pair") // (0..5, bennyhuo) pair是一个 range to name
                    acc.replaceRange(pair.first, pair.second.toString()) // 把name/followers中的0到5的位置的字符串name替换成bennyhuo
                

                println("compiledEndPoint= $compiledEndPoint") //bennyhuo/followers

                //拼接api和参数
                val url = apiPath + compiledEndPoint
                println("url ==== $url")
                println("*****************")

                okHttp.newCall(Request.Builder().url(url).get().build()).execute().body()?.charStream()?.use 
                    gson.fromJson(JsonReader(it), method.genericReturnType)//返回json的解析结果
                

            
         as T
    



fun main() 
    //interface com.bennyhuo.kotlin.annotations.eg.GitHubApi
    //println("enclosingClass=$GitHubApi.Users::class.java.enclosingClass")

    val usersApi = RetroApi.create<GitHubApi.Users>()
    val user = usersApi.get("bennyhuo")
    val followers = usersApi.followers("bennyhuo").map  it.login 
    println("user ====== $user")
    println("followers ======== $followers")

这个例子还是有点复杂,不太好理解,有些方法没接触过不知道啥意思,这里加了很多打印方法,把结果打印输出一下,这样能知道具体是代表的啥,就好理解一点了。

实例:注解加持反射版 Model 映射

这个例子是在前面反射一节实现的model映射例子的基础上,通过添加注解方式处理那些字段名称不是相同风格的情况,比如两个对象中的avatar_urlavatarUrl的相互映射。

//不写默认是RUNTIME
//@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class FieldName(val name: String)

@Target(AnnotationTarget.CLASS)
annotation class MappingStrategy(val klass: KClass<out NameStrategy>)

interface NameStrategy 
    fun mapTo(name: String): String


//下划线转驼峰
object UnderScoreToCamel : NameStrategy 
    // html_url -> htmlUrl
    override fun mapTo(name: String): String 
        //先转成字符数组,然后fold操作
        return name.toCharArray().fold(StringBuilder())  acc, c ->
            when (acc.lastOrNull())  //上一次的acc不是空
                '_' -> acc[acc.lastIndex] = c.toUpperCase() //上一次结果的最后一个字符是下划线就把下划线位置替换成当前字符的大写字母
                else -> acc.append(c) // 否则直接拼接
            
            //返回acc
            acc
        .toString()
    


//驼峰转下划线
object CamelToUnderScore : NameStrategy 
    override fun mapTo(name: String): String 
        //先转成字符数组,然后fold操作
        return name.toCharArray().fold(StringBuilder())  acc, c ->
            when 
                c.isUpperCase() -> acc.append('_').append(c.toLowerCase()) //如果是大写字母直接拼一个下划线再拼上小写
                else -> acc.append(c)
            
            //返回acc
            acc
        .toString()
    


//使用定义的策略注解,驼峰转下划线
@MappingStrategy(CamelToUnderScore::class)
data class UserVO(
    val login: String,
    //@FieldName("avatar_url") //这种是单个字段上面添加注解,只能一个一个添加
    val avatarUrl: String,
    var htmlUrl: String
)

data class UserDTO(
    var id: Int,
    var login: String,
    var avatar_url: String,
    var url: String,
    var html_url: String
)

fun main() 
    val userDTO = UserDTO(
        0,
        "Bennyhuo",
        "https://avatars2.githubusercontent.com/u/30511713?v=4",
        "https://api.github.com/users/bennyhuo",
        "https://github.com/bennyhuo"
    )

    val userVO: UserVO = userDTO.mapAs()
    println(userVO)

    val userMap = mapOf(
        "id" to 0,
        "login" to "Bennyhuo",
        "avatar_url" to "https://api.github.com/users/bennyhuo",
        "html_url" to "https://github.com/bennyhuo",
        "url" to "https://api.github.com/users/bennyhuo"
    )

    val userVOFromMap: UserVO = userMap.mapAs()
    println(userVOFromMap)


inline fun <reified From : Any, reified To : Any> From.mapAs(): To 
    return From::class.memberProperties.map  it.name to it.get(this) 
        .toMap().mapAs()


inline fun <reified To : Any> Map<String, Any?>.mapAs(): To 
    return To::class.primaryConstructor!!.let 
        it.parameters.map  parameter ->
            parameter to (this[parameter.name]
                    // let(this::get)等价于letthis[it] userDTO["avatar_url"]
                ?: (parameter.annotations.filterIsInstance<FieldName>().firstOrNull()?.name?.let(this::get))
                    // 拿到UserVO类的注解MappingStrategy的kclass即CamelToUnderScore,它是一个object calss, objectInstance获取实例,然后调用mapTo把avatarUrl转成avatar_url,最后调用userDTO["avatar_url"]
                ?: To::class.findAnnotation<MappingStrategy>()?.klass?.objectInstance?.mapTo(parameter.name!!)?.let(this::get)
                ?: if (parameter.type.isMarkedNullable) null
                else throw IllegalArgumentException("$parameter.name is required but missing."))
        .toMap().let(it::callBy)
    

这里如果注解上不写@Retention(AnnotationRetention.RUNTIME)默认就是运行时类型。
下面两种写法是等价的:

parameter.annotations.filterIsInstance<FieldName>()
parameter.findAnnotation<FieldName>()

下面两种写法是等价的:

let(this::get)

let
	this[it]

mapAs()方法中做了几件事:

  1. 尝试直接从当前Map中获取To对象的同名参数值,
  2. 尝试从To对象的字段上面的注解来获取需要转换的参数名,再根据名字获取Map中的值
  3. 尝试获取To对象的类注解得到处理类,调用处理类方法驼峰转下划线,再根据名字获取Map中的值
  4. 以上大招都没有获取到,如果To对象的字段可接受空值,就赋值null, 否则就抛异常

驼峰和下划线转换那里稍微有点绕。。

实例:注解处理器版 Model 映射




这个例子会用到一些著名的代码生成库:

上面两个都是square公司出品的开源库,

以上是关于Kotlin学习手记——注解注解处理器编译器插件的主要内容,如果未能解决你的问题,请参考以下文章

社区说 | Kotlin 编译器插件:我们究竟在期待什么?

Kotlin中使用注解框架不起作用

Kotlin编译时注解,简单实现ButterKnife

自定义Gradle plugin Java AnnotationProcessor 和 Kotlin Kapt 断点调试

告别KAPT,使用KSP为Android编译提速

简单几招提速 Kotlin Kapt编译