Kotlin之注解的浅入浅出

前言

注解对于Android开发者来说肯定不陌生,Java的Api中自带了很多注解类,我们常用的Gson和Retrofit库也经常用到注解。好久以前学过Java的注解,但在开发中很少自己去写一个注解类使用,对于注解这一方面的自定义和使用还是比较薄弱的,所以这篇文章只能记录一下小弟对Kotlin注解的浅入浅出...

一、注解的基本概念

注解是什么?

注解是对程序的附加信息说明,可以对类、函数、函数参数、属性等做标注,其信息可用于源码级、编译期以及运行时,而我们则可以通过类似于反射这些方式来获取这些信息去处理代码的业务逻辑。

注解的定义

注解也是一个类,用annotation class修饰,标记为注解类。注解不会直接影响到我们代码的执行,只是把一些额外的信息依附到这个类上面。

annotation class Api
复制代码

注释限定标注对象,这个时候要对注解类再添加注解:

@Target(AnnotationTarget.CLASS) //限定作用于类
annotation class Api
复制代码

这里表示Api这个注解指定标注给Class,而AnnotationTarget则是一个内置的注解

注解指定作用时机,比如指定运行时使用这个注解:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Api
复制代码

@Retention(AnnotationRetention.SOURCE)表示作用于源码级,@Retention(AnnotationRetention.BINARY)表示作用于编译器,他们的关系是:SOURCE < BINARY < RUNTIME,这意味这标记运行时,编译时和源码级都可以获得到,标记编译时,源码级也可获得到。

给注解添加参数

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

注解的参数类型仅支持以下类型及其数组:基本类型、KClass、枚举以及其他注解。这是因为注解类是一个受限制的类,它支持类型的值基本都是在编译期就能确定的类型,自定义的类就不行了。

注解的使用

@Api("https://api.github.com")
class GithubApi{
    
}
复制代码

二、内置注解

  • kotlin.annotation.*用于标注注解的注解
  • kotlin.*标准库的一些通用用途的注解
  • kotlin.jvm.*用于与Java虚拟机交互的注解

标注注解的注解常用的是:Retention、Target、AnnotationRetention、AnnotationTarget,Retention和Target作为限定注解的使用范围和时机,AnnotationRetention和AnnotationTarget作为Retention和Target的参数。

标准库的通用注解:

  • Metadata -> Kotlin反射的信息通过该注解附带在元素上
  • UnsafeVariance -> 泛型用来破除限制
  • Suppress -> 用来去除编译器警告,警告类型作为参数传入

Java虚拟机相关注解:

Java虚拟机相关注解的参数有很多,这里只列举其中一些常用的:

  • JvmField:生成Java Field
  • JvmName:指定类、函数等生成的Jvm名字
  • JvmOverloads:函数默认参数生成函数重载
  • JavaStatic:生成静态成员
  • Synchronized:标记函数为同步函数
  • Throws:标记函数抛出的异常类型
  • Volatile:生成volatile的Filed

三、注解案例学习

3.1、注解加反射版的model映射

案例学习我们还是以Bennyhuo大神model映射的例子为基础来延伸学习,案例的目标主要是了解注解在运行时的使用场景,掌握反射获取注解的方法。

先看下面代码:

data class UserVO(
    val login: String,
    val avatarUrl: String,
)

data class UserDTO(
    var id: Int,
    var login: String,
    var avatar_url: String,
    var url: String,
    var html_url: String
)
复制代码

代码中两个数据类的分别是avatar_urlavatarUrl,根据我们之前写的mapAs()方法肯定是无法映射的,因为名字对不上,就无法从UserDTO中获取avatar_url的值。这个时候我们在不改变这个UserVO的情况如何获取到值并赋值呢?一种方法时直接给UserVO的avatarUrl加上注解指定名字为:avatar_url

@FieldName("avatar_url")
val avatarUrl: String,
复制代码

另外一种方法呢,就是不指定名字,而指定一种策略,就是一种类型结构都指定特定的方式。

指定名字方式:

声明一个注解类:

annotation class FieldName(val name:String)
复制代码

UserVO中的avatarUrl加上注解

data class UserVO(
    val login: String,
    @FieldName("avatar_url")
    val avatarUrl: String,
)
复制代码

接下来修改mapAs()方法,这里要修改的无非就是在使用avatarUrl作为key无法获取执的时候,去获取注解参数值作为key再获取一次能否得到UserDTO的值。看下面代码:

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] //如果通过name找不到 就那注解的name来找
                ?: (parameter.annotations.filterIsInstance<FieldName>().firstOrNull()?.name.let 
                    //filterIsInstance<FieldName> 表示有FieldName这个注解就拿它的name来获取
                    name ->
                    this[name]
                })
                ?: if(parameter.type.isMarkedNullable) null
            else throw IllegalArgumentException("${parameter.name} is required but missing."))
        }.toMap()
            .let(it::callBy)
    }
}
复制代码

稍微改了一下this[parameter.name]为空的情况下再尝试查询是否有注解,有就用注解的name获取。

调用:

fun main(){
    val userDTO = UserDTO(
        0,
        "Qisan",
        "https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image",
        "https://juejin.cn/user/1820446987653816",
        "https://juejin.cn/user/1820446987653816"
    )

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

打印:
UserVO(login=Qisan, avatarUrl=https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image)
复制代码

下面看一下指定策略的方式:

data class UserVO(
    val login: String,
    val avatarUrl: String,
    var htmlUrl: String
)
复制代码

现在UserVO多了一个htmlUrl字段,但是UserDTO的对应的是html_url字段,按照之前的做法就是htmlUrl加上@FieldName注解,但是如果我们的项目工程中还有其他类有这个需求或者比较多的字段,那一个一个加@FieldName还是比较麻烦的,这个时候就可以定义一个类去实现一种策略转换。

先定义一个接口:

interface NameStrategy {
    fun mapTo(name: String): String
}
复制代码

定义注解类:

@Target(AnnotationTarget.CLASS)
annotation class MappingStrategy(val kclass:KClass<out NameStrategy>)
复制代码

MappingStrategy接受一个KClass,并且限定KClass类型上限是NameStrategy,那我们的策略类都要实现NameStrategy,下面定义策略类:

//声明成object,即是单例,减少对象创建
object CameToUnderScore : NameStrategy{
    override fun mapTo(name: String): String {
        //kotlin的高级函数fold,StringBuilder()代表一个初始值,就是最初的acc是空字符
        return name.toCharArray().fold(StringBuilder()) {acc, c ->  
            when{
                //这里是判断是大写字母就先加一个下划线再加字符
                c.isUpperCase() ->acc.append("_").append(c.lowercaseChar())
                else ->acc.append(c)
            }
            acc
        }.toString()
    }
}
复制代码

接下来就是修改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] //如果通过name找不到 就那注解的name来找
                ?: (parameter.annotations.filterIsInstance<FieldName>().firstOrNull()?.name.let {
                    //filterIsInstance<FieldName> 表示有FieldName这个注解就拿它的name来获取
                        name ->
                    this[name]
                })
                //当没有@FieldName注解的时候就查找有没有指定策略方式的注解
                ?: To::class.findAnnotation<MappingStrategy>()?.kclass?.objectInstance?.mapTo(parameter.name!!)?.let { mappingStrategyName ->
                    this[mappingStrategyName]
                }
                ?: if (parameter.type.isMarkedNullable) null
                else throw IllegalArgumentException("${parameter.name} is required but missing."))
        }.toMap()
            .let(it::callBy)
    }
}
复制代码

运行:

fun main() {
    val userDTO = UserDTO(
        0,
        "Qisan",
        "https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image",
        "https://juejin.cn/user/1820446987653816",
        "https://juejin.cn/user/1820446987653816"
    )

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

打印结果:
UserVO(login=Qisan, avatarUrl=https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image, htmlUrl=https://juejin.cn/user/1820446987653816)
复制代码

至此,Kotlin注解的基本学习和简单使用就到此了,后续要在实际项目多多使用才能更进一步。

猜你喜欢

转载自juejin.im/post/7075735418406174751