Ktor 的架构设计及开发风格是我所喜欢的,但相对地使用 Ktor 开发也要付出代价。因为 Ktor 以 unopinionated 的原则进行设计,所以很多功能不像 Spring 框架开箱即用,必须要先花时间自行开发缺少的功能及整合其它函式库,这使得许多开发者犹豫是否要使用 Ktor 开发。另一方面,Ktor 是一个很年轻的框架,使用人数不多,网路上的范例也很少,而且大多为展示简单的单一功能,缺乏将各个功能整合起来的完整後端服务范例,所以开发者必须根据自身经验,事先思考如何规划专案的档案结构及程序架构,才能建构大型且容易维护的专案。
虽然 Ktor 不像 Spring 是一间什麽都有能马上入住的样品屋,但却是一间小而美的毛胚屋,我可以按照我的想法进行内部隔间及装潢,符合我的客制化需求,最後打造一间拥有个人风格的房屋。
以下是我想要整合的框架及实作的功能清单,最後完成一个後端服务范例,供大家参考学习。截至目前为止,codebase 累计已有 241 个 kt 档,不含空白行超过 13000 行。後续我会逐一说明如何实作,读者可直接挑有兴趣的主题阅读。
Technique Stack
- Kotlin 1.5.30
- Ktor 1.6.3
- Gradle 7.2
- PostgreSQL 13.4
- Redis 6.2.1
- Kotlinx Serialization, Kotlinx Coroutine
- Koin DI
- Exposed ORM, HikariCP, Flyway database migration tool
Ktor Enhancement
-
Ktor Plugin
- Ktor Plugin 的开发惯例是使用 DSL 语法进行设定,但实务上,许多参数设定必须由外部设定档或环境变数提供。所以我实作的所有 Plugin 都支援以上2种方式,并且以外部设定档为优先
-
i18n
- 在设定档指定系统支援的语言
app.infra.i18n.langs = ["zh-TW", "en"]
- 多国语言讯息档支援 HOCON 及 Java Properties 2 种格式
- 可从 cookie 或 Accept-Language header 取得 HTTP 请求的语言,再使用 Ktor ApplicationCall 的 extension function
lang()
进行操作
-
OpenAPI Document Generator
- 自行实作文件产生器,最佳化自动产出 API 文件
- 以编程方式撰写 OpenAPI Definition,并且集中在专属的 kt 档案方便管理,让 route function 看起来更简洁
- 每个子专案可各自产生文件,避免将所有不同功能的 API 都集中在一份文件
- 支援 Http Basic Authentication,保护 API 文件不外流
- 整合 Gradle Git Plugin,将 Git 版本资讯、建置部署时间…等资讯加进文件中
-
Authentication and Role-Based Authoriation (类似 Spring Security)
- Ktor 本身仅实作 authentication 机制,并没有定义使用者及其角色。我允许每个子专案定义自己的使用者及其角色,并且整合至原有的 Ktor authentication 机制,达到类似 Spring Security 的功能
authorize(ClubAuth.Admin) {
put<UUIDEntityIdLocation, UpdateUserForm, Unit>(ClubOpenApi.UpdateUser) { _, form ->
clubUserService.updateUser(form)
call.respond(HttpStatusCode.OK)
}
}
-
Type-safe and Validatable Configuration
- Ktor 读取设定档的方式是透过 ApplicationConfig 物件,但只能使用 getString() 函式取值。我使用 Config4k 将设定值转换至 Kotlin data class,不仅可以达到 type-safe 的效果,直接操作物件的写法也更简洁易懂。除此之外,我也在 config4k 转换时插入验证函式
validate()
,类别可实作此函式检查设定值是否合法
data class SessionConfig(
val expireDuration: Duration? = null,
val extendDuration: Duration? = null
) : ValidateableConfig {
override fun validate() {
require(if (expireDuration != null && extendDuration != null) expireDuration > extendDuration else true) {
"expireDuration $expireDuration should be greater than extendDuration $extendDuration"
}
}
}
-
Request Data Validation
- Ktor 没有实作对请求资料进行验证的功能,我透过自定义 route extension fuction 的方式,先将 request body, path parameter, query parameter 转为 data class 之後,随即进行资料验证,最後再传入 route DSL function 作为参数进行操作。目前我使用 Konform,以 type-safe DSL 的方式撰写验证逻辑,未来再考虑是否支援 JSR-303 annotation
Infrastructure
-
Logging
- 使用 coroutine channel 非同步地写入 log 至档案、资料库或 AWS Kinesis stream
- 目前包含 request log, error log, login log, notification log,每一种 log 都可以各自设定写入的目的地
-
Authentication Methods
- Service 验证: 支援 API key authentication 及信任 ip 白名单
- User 验证: 使用 bcrypt password authentication。预计未来支援 OAuth
-
Redis
-
Notification Service
- 使用 coroutine channel 非同步地发送通知至多个管道,包含 email(AWS SES), push(Firebase), sms(尚未串接)
- 整合 freemarker template engine 处理 email 内容
- 可根据使用者偏好语言发送通知
-
Mobile App Management
- 支援管理多个 app
- 验证客户端 app 版本,检查是否有新版本,甚至强迫升级
- 管理使用者装置的推播 token
-
Performance Tunning
- 所有的 Coroutine Channel 及 Java ExeuctorService threadpool 参数都可以透过设定档进行调整,我们可以根据每个环境的效能需求及限制给予不同的设定值。
Multi-Project
每个子专案各自拥有以下项目
- 使用者类型及其角色
- 验证 API 请求的方式
- 事件通知
- OpenAPI 文件
Ops Project
为了整合 DevOps 流程,我内建 Ops 子专案,目前包含 Operation Team 及 AppTeam 2种使用者角色,另外还有 Root 及 Monitor 2种服务角色。每种角色只可以呼叫有其权限的 API,可以进行权限控管。
- Root: 管理 Ops 专案的使用者
- Monitor: 实作类似 spring-actuator 的监控功能,目前支援 healthCheck,预计未来将提供更多系统状态的资讯
- Operation Team:
- 可以填写任意文字作为 email 的内容,传送给符合查询条件的使用者
- 给定查询条件将资料库里符合的资料汇出成 Excel 档案,再寄送至指定的 email
- App Team: App 版本管理
- User: 登入、登出、变更密码
Club Project
Club 为展示功能的范例专案, 目前包含 Admin 及 Member 2种使用者角色,以 iOS, Android App 作为使用者客户端
- Admin
- 管理 Club 使用者
- 可以填写任意文字作为 email 及推播通知的内容,传送给符合查询条件的使用者
- Member: 目前没有 Member 才能执行的功能
- User: 登入、登出、变更密码