Kotlin 18:任务注解为什么不会自动校验
给任务草稿标上 @TaskCategory("draft"),运行时可以读到这份元数据;再标一个仅供源码阶段使用的提示,运行时却读不到。更容易误会的是:类上写着“需要校验”,是否意味着创建一个正文全为空白的草稿会被自动拦住?本篇沿用 17 的 TaskDraft,比较注解目标、保留期和实际调用 Class.getAnnotation 的结果,明确哪些逻辑仍需要普通函数执行。
实验组合是 Kotlin/JVM 2.2.21、JDK 21、Gradle Wrapper 8.14。出处定位在 Kotlin 注解文档与 反射文档;本章只用 JDK 的 Class.getAnnotation 和 Kotlin 标准库的 ::class.java,没有添加 kotlin-reflect 依赖,也没有编译器插件或扫描容器。实验中的类型、编译器诊断和字节码都可在 18 证据目录核对。
注解贴在哪里,留下多久
两个注解都只能标在类上,但保留期不同:
1 | |
TaskCategory 的 RUNTIME 指定运行期仍可获取;TaskDraftHint 的 SOURCE 不承诺运行时可见。官方文档对目标和保留期分开定义:前者约束可以贴在何种声明上,后者描述注解在哪个阶段保留。单独放在默认源码集之外的 @TaskCategory("invalid-target") fun invalidMetadataTarget() 编译退出 1,实际诊断 This annotation is not applicable to target 'top level function'. Applicable targets: class。因此不是靠约定“大家不要贴错位置”,而是这个目标声明被当前编译器拒绝;误贴成函数不是运行时才发现。
反射读到了什么,没读到什么
本章的读取入口只有一条:
1 | |
taskCategoryOf(TaskDraft::class.java) 的测试结果是 draft;同一类上的 TaskDraftHint 用 getAnnotation 读取为 null。未标注 TaskCategory 的 TaskState.Queued::class.java 也读到 null。这证明特定类、特定反射调用能取得运行期保留的类注解,不能从 null 推断整个项目不存在同名提示,或推断别的保留期都能被同一 API 读出。当前 javap -p -v TaskDraft 的原始输出还显示 RuntimeVisibleAnnotations 下的 example.tasks.TaskCategory,与实际运行观察一致;这是本次 JDK 21 / Kotlin 2.2.21 生成物,不是对其他后端的字节码承诺。
若使用 Kotlin 反射 API 枚举属性、参数或调用函数,还需要独立核对 Kotlin 反射依赖与功能边界。这里没有引用 kotlin-reflect 或运行 classpath 扫描。Class.getAnnotation 只查询给定的 Class 对象;它不会自动发现所有任务类,更不会把 TaskCategory 的字符串转成任务状态。将来真要引入 kotlin-reflect,须先冻结与本项目 Kotlin 版本兼容的精确依赖,并重新记录解析/运行结果,不能拿本章的 Java 反射测试充当 Kotlin 反射覆盖。
“写了提示”为什么还得主动校验
测试直接构造 TaskDraft(TaskId("task-1"), " ", listOf(" ")):TaskId 本身有效,草稿正文与标签却都是空白。读取这个草稿类型的 TaskCategory 仍返回 draft,草稿的 text 仍是空白;显式执行 makeTaskDraft(raw.id, raw.text, raw.labels) 才抛 IllegalArgumentException。上一章普通构造函数的校验是实际负责筛除无效草稿的代码,@TaskDraftHint 只是源码上的一条提示,没有绑定校验动作。
这个负例也划出了 data class 生成成员的边界:公开的主构造器本身没有 require,因此直接构造可以绕开 makeTaskDraft。若要保证所有创建路径都拒绝空白,需要改变构造/可见性设计,而非只加一个注解。注解处理器、编译器插件或运行时容器可以在别的工程里赋予标记额外行为,但本项目没有实现这些机制;本篇不能把可能性写成已经生成、已经扫描或已经执行。
实际复核与练习
bash examples/kotlin-language/verification/18.sh 运行累计 00–18 十九组测试、旧 CLI 正反回归、隔离目标错误编译和 javap -v,保留环境、命令、退出码、原始 XML 和断言;旧 CLI 不读取元数据,命令行成功不代表反射集成。下载含完整Wrapper/累计工程与原始证据的快照(SHA-256校验)在本篇同名素材目录提供解包离线复跑命令。
练习:把 TaskDraftHint 的保留期仍设为 SOURCE,然后只把读取代码改成 Java 反射,能否得到 validate-before-submission?本章同一读取方法的测试结果是 null;先判断保留期,不能靠换反射调用名凭空造出运行期没有承诺的元数据。把 TaskCategory 改标在一个顶层函数上能否绕开校验?不能;本章的 CLASS 目标已经使该源码无法编译,但即使改成合法类注解,它也不会自动验证任务文本。
[PATTERN] 对一条注解相关主张依次问:能贴在哪儿、会保留多久、谁实际读取、读取后谁执行动作。目标由编译器核对,运行期可见性由保留期和平台/API 决定,业务动作必须有可定位的调用者;缺了最后一步,注解只是数据。
| 观察层 | 本篇已验证 | 本篇未证明 |
|---|---|---|
| 注解目标 | 类合法、顶层函数编译拒绝 | 任意声明都可贴同一标记 |
| 运行时读取 | RUNTIME 类注解为 draft;SOURCE 提示为 null |
已扫描整个 classpath |
| 任务校验 | 显式 makeTaskDraft 拒绝空白 |
注解触发编译期插件或自动验证 |
下一篇进入 Java 返回值与 Kotlin 空安全的交界,重点处理 Java 实际返回 null 时 Kotlin 看见的 platform type,而不是复述基本可空语法。






