Kotlin 与 Ktor 小结

最近 4 天花了不少时间研究 Kotlin + Ktor 生态,发现基于 Ktor 生态的 CRUD 生态,JB 官方本身也忙不过来。而且 Ktor 本身是插件形式的框架,其复杂和麻烦的依赖包也是非常恶心的一点,结合自身的 ORM 框架用起来极其恶心。

为什么选 Kotlin

总的来说,为了在手机端发送动态,脱离微信或者 QQ,实现在自己的个人站上发布个人动态。为什么非要在个人站上发布呢?因为国内的软件本质上都是政策下的监控软件,这点没有必要多说。

而选择 Kotlin,理由也很简单,因为 Kotlin 最终可以用来编写 Android,而且我个人是比较偏向原生开发的。即不喜欢什么 WebView,去折腾这个没有必要。做些 CRUD 的操作,我喜欢秒启动,非常反感在安卓中嵌入网页。理由就是我自己在 Web 领域的经验告诉我,Web 的性能就是不好,做不到秒开。更不用说,类似于 App 的开屏广告实际上就是藏拙——避免启动 App 等待过程中加载慢,而在启动过程植入广告的恶心人的操作。

不论是什么原因,类似于微信这样的巨无霸,我很讨厌。这些软件监控我的个人行为,收集我的言论,这让我十分反感。更何况群众中还有工贼。

Kotlin 范式

基于以上目的,在 2026-03-25 前后那段时间在看 Kotlin。很早之前我就在考虑使用 Kotlin,在很早前,我想尝试直接入坑 Android,甚至买了两本书,想以此来实现我的安卓端发布动态的目的。但是发现 Android 的内容太多,我在了解过程中渐渐在 Android 这个大迷宫中丧失了对 Kotlin 语言本身的理解。

后来找到工作后,就渐渐丢掉了这部分的进度,把大部分时间投入了工作内容中。但是对于工作也不是没有收获,更多是利用工作学到了 SSR 和前端工程化的内容:大仓和 Rspack 以及 npm 包的实践经验。无代码是我既爱又恨的东西,爱它是因为它是个很有挑战性的工作,且十分有意义;恨它是因为这不是简单的工程,对于开发资源不够的公司来说,这个方向本身靠几个人撑起从业务到底层的链路本身是个笑话。从可维护性来讲,几个人负责开发这一整套严格又有系统性关系的数据流,就是会充满不规范性问题。

在一年的工作过后,我又开始往这个方向着手实施了,因此又花了几天回顾 Kotlin 的文档。

Kotlin 的包

Kotlin 的包和 Java 的管理方式大部分一样,都是在文件顶部声明此文件属于哪个包,例如:

// 包声明
package dev.luchador.dao

// 包内容
class User {}

但是不同于 Java 的文件,Kotlin 的包比 Java 灵活太多。一个包可以把所有的类和函数都放在一个文件中1

// 包声明
package dev.luchador.dao

// 包内容
data class User {}

data class Contract {}

fun getUserContract(user: User): Array<Contract> {
    // do something
}

当然如果内容十分多,按照 Kotlin 官方的建议,超过 400 行的文件应该被拆解,将包内容切割到不同的文件中。这点和 TS/JS 基本是一致的。这是我喜欢 Kotlin 的第一点。

在一个文件中引入包的行为和 Java 是一致的。例如我想在其他包或文件中引入包的对象、函数,亦或常量。这里需要注意的是,如果已经在引入包了,那说明一定在本文件中不会存在。

这里就需要提到 Kotlin 包管理模式了。

在一个 Kotlin 文件中,顶部声明的以 . 分割的标识符号,会在 Java 编译和运行时用来作为引用依据,或者理解为文件内容组成的命名空间。

如何理解 Java 的组织逻辑?

要想运行 Java,一般是需要先编译后运行的。

编译的时候,会扫描所有的文件,然后读取文件顶部的包声明,按照包声明的 . 分隔符切割,并通过这些切割后的前缀一致的情况分组,形成树形结构。然后编译器根据这些包的组织结构分别编译。

第一个编译阶段就是包的组织阶段,即上述所涉及到的树形化组织。这个树形化分析就包含了外部引入的包。关于外部包的引入,其实就是 JAR 的内容解包后形成的目录树,再次按照 package 声明分析并加入到相应的组织树形节点中。

第二个阶段是分析每个包的导入。现在 Java 编译器有了包的整体树形结构,按照他们自身的包声明方式。因此下一个阶段将查找他们的引入包,即 import 的包,是否存在于这个树形结构中。如果存在则通过此阶段。

基本上包的分析都是这样了。

另外值得一提的是,包的导入逻辑是 package 声明的标识 + 文件顶级声明的方式,例如:

plus.kt:

package dev.luchador.utils // 包名

fun plus(a: Int, b: Int) {  // 声明:函数名称 plus
    a + b 
}

user.kt:

package dev.luchador

import dev.luchador.utils.plus // 导入包的标识:dev.luchador.utils + 包内的顶级声明:plus

fun main() {
    plus(1, 2) 
}

工厂函数和作用域

Kotlin 工厂函数一般包含调用作用域。如果你熟悉 JS,一定听过 with。Kotlin 的工厂函数非常多,可以说贯穿 Kotlin 所有流行库。这些工厂函数用起来就跟一个特定领域的 DSL 一般,让你写起来就跟传统编程范式不太一样了。当然,你可以按照传统编程的范式去编写它,但是如果你用了,就会感觉 Kotlin 写起来很爽。甚至有的库依赖于这些特性,你按照传统的写法反而很麻烦。

这是这门语言的魅力点。当然因为这个特性的存在,导致很多库都用了此特性,很多时候你不能按照大部分语言的范式去使用它,否则你将无法享受到这个库和语言带来的快乐。反过来说,这正是 Kotlin 所带来的门槛2

Ktor

Ktor 这个框架是 Kotlin 官方推荐的。如果看完所有的语言特性后,下一步很显然就是实践了。Ktor 作为出现在 Kotlin 官方文档中的库,当然是新手第一个接触的内容。

很显然 Ktor 的野心很大,JB 的野心也很大。光 Kotlin 的 KMP + 附加的一众包,都足以凸显其工作量内容。想要颠覆的野心,我想这些野心正是后来与 Exposed 结合的困境:他们没空研究这些细节问题。正如大部分商业公司一样,凸显的是”我有这个东西”,而不是”用起来是否真的解决了客观用户体验问题”。

首先说 Ktor,因为其主打轻量化的目标,包括内存占用和最小依赖的目标,致使使用 Ktor 启动时是非常轻松的。重要的是,实际使用上需要安装大量的依赖,这本身不是什么问题。但是问题在于 Ktor 想再做轻量化时,还有多平台的野心。例如不依赖 Tomcat 我赞成,脱离 Servlet 我也赞成,毕竟 Java 是什么时代的产物,其辉煌年代所主张的标准现在早已被社区和开发者的标准以及追求所不同。

因为这些原因,对特定环境的开发充满了各种兼容性问题。比如 CIO 引擎,在 Java 生态中就容易水土不服。这是多平台带来的问题,正如现世的多平台 UI 开发一样。如果光光是运行在不同平台的 CRUD 完全没有问题,但是一旦遇到稍微复杂点的图形化或者帧率问题,他就一点办法也没有。最终遇到这类问题逼不得已又回到了原生开发的事情上。虽然他们的野心也有,但是现在 Ktor 出来已经 9 年了,虽然相比原来改进很多、重构很多,这些问题逐步完善,但是在完美的 DX 体验上仍然存在非常多的问题。

Ktor 与 Exposed

这个是问题的集中爆发点。Exposed 在提供一般简单表格上是一流的,但是对于像 UUID 这样原生数据库字段类型时,就不太妙了。正如 PostgreSQL 众多的列类型来说,Kotlin 肯定没功夫打理。正如 Exposed 提供的 UuidTable 一样,在使用其自带的 exposed-daoUuidEntity 之间存在不可调和的矛盾:一面是 Java 的原生 UUID,一面是 Kotlin 自带的 UUID。之间的问题,过了 5 年之久都还没有解决。

只能通过特殊注解或默认值的方式生成,才能解决这样原生支持但是又和框架的设计思路完全格格不入的情况。

结果

我在通过 Gemini 查询相关问题时,决定放弃 Ktor。现在我认为我已经了解了 Kotlin 的框架范式,就是工厂函数部分的内容。这些库喜欢使用作用域 + 工厂函数做到了类似 DSL 语言效果,且可读性很强。

因此,我想我已经可以放弃利用 Ktor 作为熟悉 Kotlin 的桥梁了,下一步应该投入到 Android 中去了。

正是基于这几天的折腾,与其如此浪费在他们自身的历史遗留问题上,我不如趁早投入到开发体验极佳的 Node.js,或者说前端主导的开发体验生态中去,当然主要是轻量级后端而已。

然后研究 Android。也许我会发现 Android 开发也是一团糟,可能又会去了解跨平台的 Dart。但是我现在只想参与到 Android 中去。兴许,我以后作为闲暇时光又会回来写 Ktor,但是现在应该告一段落了。直到我达到更高的层次。

接下来,我需要详细了解下基于 PostgreSQL 的生态,正如 Drizzle + Supabase 的组合。我选择相信他们的品味,正如我喜欢 Postgres 一样。也许我会发现那是另外一个坑,但我绝不后悔,那是我的选择。我学到了更多,让我对我所需要的内容更加了解,面对选择时更加从容。

参考资料

Android 开发

参考资料

Footnotes

  1. Java 通常也可以这么做,但实际上不是 Java 标准做法,而且开发者大部分也不会这么干。

  2. Kotlin Functions - Kotlin