终端打印出“任务已接收”,能证明什么?只能证明这次进程走到输出语句。若下一次启动还要查到它,就需要另一套保存状态的机制;若想确认它确实执行了,就还需要执行者和结果记录。把这几个结果混在一行成功提示里,后面讨论取消、事务时就很容易把“请求返回”误当“任务完成”。

本系列从一条不联网、不依赖真实数据的文本任务开始。00 只让命令行接受合法输入,并对错误输入明确失败;先回答更靠前的问题:究竟是哪一套编译器、依赖与 JVM 运行了这些代码?累计工程在 examples/kotlin-language/,本篇对应的完整快照和运行记录随文章素材一起交付。

一条任务走过哪些边界

工程入口是 src/main/kotlin/example/tasks/Main.kt。这一阶段没有任务队列,submit 只是一个语法约定:第一个参数是动词,第二个参数是合成任务文本。

1
2
3
4
5
6
7
8
9
10
fun submitTask(args: Array<String>): String {
require(args.size == 2 && args[0] == "submit" && args[1].isNotBlank()) {
"Usage: submit <task text>"
}
return args[1]
}

fun main(args: Array<String>) {
println("TASK input accepted (not persisted): ${submitTask(args)}")
}

Array<String> 是传给入口函数的参数序列;args.size == 2 检查长度,args[0] 检查命令,isNotBlank() 排除空串及全空白文本。&& 从左到右短路,所以缺参数时不会访问不存在的 args[1]。require 在条件不成立时抛出 IllegalArgumentException;这里不把错误吞掉,进程会以非零状态结束。返回值只是经过最小输入校验的字符串,既不是 Task 类型,也没有业务状态转换。

上图中有两条容易混淆的路径。Gradle 负责调用 Kotlin Gradle Plugin、组织依赖、测试和运行;Kotlin 编译器把 Kotlin 源码变成 JVM class;JVM 加载这些 class 并运行入口。stdlib 是运行时需要的 Kotlin 标准库,它不是编译器,也不是 Gradle 自带 Kotlin 的别名。Kotlin 命令行编译文档展示了从 .kt 文件编译并运行 JVM 程序的最小路径;这里选择 Wrapper 管理同一条路径,免得依赖机器上碰巧安装的 kotlinc。

可迁移的判断:看到“编译成功”,先列清编译器、产物、运行时三个不同的对象;看到“任务成功”,再单独问有没有保存或执行的证据。对象没对齐,单次输出无法替另一层背书。

固定版本,不把滚动文档当实验

本次环境是 Ubuntu OpenJDK 21.0.12.1+1-1-24.04.4-Ubuntu,x86_64;Wrapper 是 Gradle 8.14,应用使用 Kotlin Gradle Plugin、编译器及 stdlib 2.2.21,languageVersion=2.2、apiVersion=2.2、jvmTarget=21。Kotlin 上游 v2.2.21 tag 指向 2146684dcba708e5a304758b41a9e4ec9c7eff71;这只是后续源码定位的版本锚点,本篇没有声称读完编译器实现。

Kotlin 官方 Gradle 兼容表在 2026-10-07 查阅时给 Kotlin 2.2.20–2.2.21 列出 Gradle 7.6.3–8.14;这个表支持选型,不能替代实际编译。./gradlew --version 的输出中还有一行 Kotlin: 2.0.21:那是 Gradle 8.14 自带的 Kotlin,用来执行 Kotlin DSL 等构建逻辑,并不是正在编译应用的 Kotlin 2.2.21。应用的 runtimeClasspath 解析到 org.jetbrains.kotlin:kotlin-stdlib:2.2.21。把两个版本号只留一个,会使“我究竟检查过哪个编译器”无法回答。

jvmToolchain(21) 让 Gradle 选 JDK 21 工具链;jvmTarget=21 指定生成 JVM 21 级别的字节码;languageVersion 和 apiVersion 限制允许使用的语言/API 版本。它们不是同一个开关。本次 javap -verbose 对生成的 example.tasks.MainKt 显示 major version: 65,对应 JVM 规范的 class 版本表中 Java SE 21 的 class 格式。MainKt 是本次构建观察到的生成类名,不把这个名字升级为所有 Kotlin 代码永久不变的语言承诺。

Wrapper 的 Gradle 8.14 分发包 SHA-256 为 61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa。本环境直连官方分发地址最终重定向到无法解析的 GitHub 域名,因此从镜像下载,按官方 SHA 文件核验后,才用它生成并运行 Wrapper;gradle-wrapper.properties 固定相同的 distributionSha256Sum。Gradle Wrapper 文档解释了这项完整性检查。校验说明镜像提供的字节与官方公布的摘要相同,不等于断言镜像本身是官方发布入口。

运行一次,再让错误也可复现

在工程目录执行:

下载包含工程、Wrapper、版本与原始证据的 00 快照(SHA-256 校验文件)。云端站点不保留仓库 `examples/` 时,按压缩包内 `RUN.md` 复跑;解包实测记录在 `writing-plans/kotlin/verification/00/unpack.txt`。
1
2
3
./gradlew --no-daemon clean test
./gradlew --no-daemon run --args='submit nightly-index'
./gradlew --no-daemon run --args=submit

实际证据来自 verification/00.sh,它记录每条命令、UTC 环境、原始输出、退出码及断言,而不是把“期望输出”伪装为运行输出。clean test 退出 0;测试报告显示 tests="2" skipped="0" failures="0" errors="0"。一次合法调用退出 0,输出 TASK input accepted (not persisted): nightly-index。少给文本参数的调用输出 Usage: submit <task text>,进程及 :run 退出 1;这条非零退出是预期的反例通过,不是测试红灯。

测试直接调用 submitTask,包含普通文本、中文、缺少参数、空串和未知命令;命令行再覆盖真实进程边界。两层不是重复:单元测试找逻辑错误,真实 run 才能确认从参数进入入口到进程退出的组合路径。实验断言还独立检查 stdlib 版本与字节码 major 65。原始记录在仓库的 writing-plans/kotlin/verification/00/,素材快照包含对应记录。未测试大量并发输入、任何网络请求或数据库重启行为。

--args='submit nightly-index' 把一段文本交给 Gradle 的 run 任务解析成两个参数。引号先由 shell 处理;若改成包含空格的任务文本,需要按 Gradle 的 --args 解析规则正确引用。这里选一个无空格的合成输入,避免把 shell 引号与 Kotlin 的参数解析混成同一件事。

可迁移的判断:验收一种 CLI 时,把“单次函数行为、进程退出、依赖解析、生成物”拆为独立证据。只改一个条件(此处删去第二个参数),保留反例的非零退出与诊断文本,才知道校验真正拦住了什么。

当前程序没有完成什么

这行“input accepted”只保证程序在退出前读到了一个合格的参数。submitTask 不保存输入,重新启动进程不能查询历史任务;没有任务执行者,不能谈 Job 状态、取消、完成顺序或事务提交。后续 12 才定义领域状态,24–32 才验证协程任务树,34–35 才验证 HTTP 与持久化。命令名叫 submit,不等于背后已有队列。

这也决定了本篇不演示“把 suspend 加到函数上”来制造异步任务:函数目前完全同步、没有等待点。以后遇到异步代码,仍要分别检查挂起、线程阻塞、Job 生命周期和业务状态;一次命令行退出无法替代那些实验。

练习与答案

练习一:把 submit nightly-index 改成 submit,预测参数检查和退出结果。答案:长度先不等于 2,&& 让后续对 args[1] 的访问不发生;require 抛 IllegalArgumentException,main 不执行输出语句,Gradle 的 :run 非零退出。原始日志中可找到 Usage: submit <task text> 和 EXIT_CODE=1。

练习二:./gradlew --version 显示 Kotlin: 2.0.21,能否据此推断应用由 2.0.21 编译?答案:不能。查 build.gradle.kts 中 Kotlin 插件 2.2.21,再查 clean 编译、runtimeClasspath 的 stdlib 以及生成物。本篇观察到的 Gradle 自带 Kotlin 与应用编译器属于不同层;不能从前者替后者背书。

练习三:已有 major version: 65 和合法输入的输出,是否足以宣布“提交到队列后可重试”?答案:不够。class 版本只说明本次字节码格式;进程输出只证明走到 println。要证明可重试,还需要可查询的任务标识、存储、重启后的读取和重复请求的行为契约;这些都不在 00 的实验里。

本篇验收结果:构建、两条行为测试、合法/非法 CLI、stdlib 解析与 JVM class 版本均有实际输出;页面与素材检查另见系列状态账本。下一篇会把 val/var 放在同一任务配置里,区分“引用不能重新赋值”与“引用指向的对象不可变”这两个不同命题。

参考资料