注解:给代码打上“标签”的艺术
说实话,刚学 Kotlin 那会儿,我觉得注解这东西挺玄乎的。不就是个 @Override 嘛,有啥好讲的?直到后来我在一个大型项目里,被一堆重复的模板代码折磨得不行,才真正意识到——注解这玩意儿,用好了简直是生产力神器,用不好嘛……嗯,就是给自己挖坑。
今天咱们就来聊聊 Kotlin 里的注解。我会从定义、使用,到元注解、注解处理器,一步步带你吃透它。
注解到底是什么?
说白了,注解就是一种“元数据”。它不直接影响代码逻辑,但能给编译器、框架、工具提供额外的信息。你可以把它想象成给代码贴的标签——比如“这个函数已废弃”、“这个字段需要序列化”、“这个方法会在测试前执行”。
我个人习惯把注解分成两类:
- 编译时注解:编译器会读取并处理,比如
@Deprecated、@Suppress - 运行时注解:通过反射在运行时读取,比如
@Test、@Retrofit里的注解
Kotlin 的注解语法和 Java 很像,但更简洁,也更强大。
定义注解:从零开始造一个
先看个最简单的例子。假设我想定义一个注解,用来标记哪些函数需要做日志记录:
annotation class Loggable
就这么简单?对,就一行。但实际项目中,我们往往需要给注解加点“料”——也就是参数。
annotation class Loggable(
val level: String = "INFO",
val tag: String = "Default"
)
这里要注意,注解的参数类型是有限制的:只能使用基本类型、String、枚举、Class、其他注解,以及它们的数组。我在项目中就踩过这个坑——想把一个自定义对象传进去,结果编译器直接报错。
另外,Kotlin 允许注解的参数有默认值。这个设计很贴心,你想想看,如果大部分场景都用默认值,调用方就不用每次都写一堆参数了。
使用注解:贴标签的艺术
定义好了,怎么用?直接往目标上一贴就行:
class UserService {
@Loggable(level = "DEBUG", tag = "UserService")
fun getUser(id: Long): User {
// 业务逻辑
return User(id, "张三")
}
@Loggable
fun saveUser(user: User) {
// 默认使用 INFO 级别
}
}
你看,getUser 用了自定义参数,saveUser 用了默认值。灵活又清晰。
注解可以贴在哪些地方?Kotlin 支持的目标比 Java 更多:
- 类、接口、对象
- 函数、属性
- 构造函数
- Lambda 表达式
- 甚至类型(这个 Java 没有)
举个例子,如果你想给一个 Lambda 表达式加注解:
val action: @Loggable () -> Unit = {
println("执行操作")
}
嗯,这个用法比较少见,但确实存在。我一般只在需要精细控制类型检查时才会这么用。
元注解:注解的注解
元注解,就是用来定义注解的注解。听起来有点绕,但理解起来不难。
Kotlin 提供了几个内置的元注解,我挑几个最常用的说说:
| 元注解 | 作用 | 我的经验 |
|---|---|---|
@Target |
指定注解可以贴在哪些地方 | 不指定的话,默认几乎所有地方都能贴,容易误用 |
@Retention |
指定注解的生命周期 | 编译时处理用 SOURCE,运行时反射用 RUNTIME |
@Repeatable |
允许同一个目标贴多个相同注解 | 我用来做权限检查,一个函数可以贴多个角色注解 |
@MustBeDocumented |
让注解出现在 API 文档中 | 库作者建议加上,方便使用者查阅 |
看个实际例子。我想定义一个只能用在函数上、只在编译期生效的注解:
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class DebugOnly
这样,如果有人试图把这个注解贴在类上,编译器就会报错。我曾经在一个团队里见过有人把 @Override 贴到变量上——虽然 Kotlin 不允许,但 Java 早期版本确实可以,结果就是代码逻辑完全不对。所以,@Target 一定要用好。
注解处理器:让注解真正干活
定义和使用注解只是第一步。真正让注解发挥威力的,是注解处理器。
Kotlin 里处理注解主要有两种方式:
- 编译时处理:用 KSP(Kotlin Symbol Processing)或 KAPT(Kotlin Annotation Processing Tool)
- 运行时处理:通过反射读取注解信息
先看运行时处理。这个比较简单,适合小规模场景:
fun processLoggable(target: Any) {
target::class.members.forEach { member ->
member.annotations.forEach { annotation ->
if (annotation is Loggable) {
println("函数 ${member.name} 需要日志记录,级别:${annotation.level}")
}
}
}
}
但说实话,反射性能开销不小。我在项目中一般只在框架代码里用运行时注解,业务代码里尽量不用。
编译时处理才是重头戏。以 KSP 为例,你可以写一个处理器,在编译阶段生成代码:
class LoggableProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.Loggable")
symbols.filterIsInstance<KSFunctionDeclaration>().forEach { function ->
// 生成日志代码
println("为函数 ${function.simpleName} 生成日志代码")
}
return emptyList()
}
}
这样做的好处是:运行时零开销,所有代码都在编译期生成好了。像 Dagger、Room 这些库,底层都是这么干的。
核心要点:注解本身不干活,干活的是注解处理器。处理器在编译期或运行时读取注解信息,然后执行相应的逻辑。
避坑指南
这些年用注解,我踩过不少坑。挑几个典型的说说:
我曾经犯过的错:
- 把
@Retention设成SOURCE,却想在运行时通过反射读取——结果啥也读不到,排查了半天 - 注解参数用了可变列表
vararg,结果 Kotlin 不支持,编译报错 - 在 Lambda 表达式上贴注解,但忘了加
@Target(AnnotationTarget.EXPRESSION),编译器直接忽略
我的建议:
- 能用编译时处理就别用运行时反射,性能差距很大
- 注解参数尽量用默认值,减少调用方的负担
- 团队里统一注解命名规范,别搞出
@Log、@Logger、@Loggable这种混用的情况
知识体系总览
下面这张图,帮你把今天讲的内容串起来:
注解这东西,说白了就是给代码加标签。定义好标签,贴对位置,再配上处理器——一套完整的代码增强方案就出来了。我刚开始用注解时总觉得它很“魔法”,后来写多了才发现,它其实就是一种约定,一种让工具和框架理解你意图的方式。
好了,注解这块就聊到这儿。记住:注解是工具,不是目的。别为了用注解而用注解,那样只会让代码变得复杂难懂。该用时果断用,不该用时别硬上。