This project has two subprojects built on common infrastructure and library but each subproject can be deployed optionally as a ktor module . Subproject has its own
application {
modules = [
fanpoll.infra.ApplicationKt.main,
fanpoll.ops.OpsProjectKt.opsProject,
fanpoll.club.ClubProjectKt.clubProject
]
}
app.infra.i18n.langs = ["zh-TW", "en"]
lang()
Authentication and Role-Based Authoriation, like Spring Security
authorize(ClubAuth.Admin) {
put<UUIDEntityIdLocation, UpdateUserForm, Unit>(ClubOpenApi.UpdateUser) { _, form -> clubUserService.updateUser(form) call.respond(HttpStatusCode.OK) } }
Type-safe and Validatable Configuration
validate()
function to validate the configuration values
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" } } }
Ktor 是 JetBrains 開發的 Web 框架,其特性是
其中 Unopinionated 的特性,Ktor 並沒有預先整合常用的第三方框架及函式庫,例如 Dependency Injection, ORM, Logging...等,而是讓開發者能自由選擇整合至 Ktor。但也因為如此,開發者不僅需要花時間進行整合的工作,對於新手或是習慣 Spring Boot 全家桶的開發者,更是存在不小的門檻。
另一方面,Ktor 的架構設計是讓開發者透過實作 Feature (官方未來將改名為 Plugin),把 intercepting function 加入至 request pipeline 之中,藉此增加功能或改變行為。這種方式與 Spring Boot 規定開發者實作特定的介面或繼承特定類別,再透過 dependency injection 的方式注入有所不同,再加上如果開發者不熟 Lambda 與 DSL 寫法,就不知該如何動手實作 Feature。
由於 Ktor 是一個很年輕的框架,所以使用人數不多,網路上的範例也很少,而且大多為展示簡單的單一功能,缺乏將各個功能整合起來的完整後端服務範例。另一方面,Ktor 本身是 unopinionated 的微框架,官方文件也沒有建議的開發指南,所以開發者必須根據自身經驗,事先思考如何規劃專案的檔案結構及程式架構,才能建構容易維護的專案。
綜合以上原因,本專案使用以下技術且包含常見網站後端服務功能的範例,供大家參考學習。截至 2021-07-21,codebase 累計已有 241 個 kt 檔,不含空白行超過 13000 行
Ktor 是一個微框架,缺乏與常用第三方框架或函式庫之間的整合,例如 DI, ORM,甚至連 request data validation 及 i18n 也沒有實作。所以如果要直接使用 Ktor 開發專案,就無法像 Spring Boot 設定後立即使用,必須要先花時間自行開發缺少的功能及整合,所以本專案對 Ktor 進行了以下增強改善
app.infra.i18n.langs = ["zh-TW", "en"]
lang()
進行操作Authentication and Role-Based Authoriation
authorize(ClubAuth.Admin) {
put<UUIDEntityIdLocation, UpdateUserForm, Unit>(ClubOpenApi.UpdateUser) { _, form -> clubUserService.updateUser(form) call.respond(HttpStatusCode.OK) } }
Type-safe and Validatable Configuration
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" } } }
近年微服務架構興起,對於規模較小的開發團隊而言,一開始就規劃拆分為多個微服務是個沉重的負擔,所以大多還是從單體式架構 monolithic 出發,往後再視情況逐步拆分為微服務。雖然這種開發方式廣泛被採用,但實際上並不是所有團隊都能無痛轉換至微服務架構,這取決於單體式架構裡的各個功能實作上是否低耦合,甚至模組化。 另一方面,即使往後將各個功能拆分成微服務,因為通常拆分的時間點不一, 或是由不同的工程師負責實作,如果沒有事先規劃及規定統一的作法,就容易導致各個微服務在基礎設施功能方面的實作方式上有所差異,提高了往後開發維護及維運的成本。
本專案雖然是單體式架構,但是以模組化開發方式為準則,並且在共同的函式庫及基礎設施功能上建立子專案。規劃子專案可以遵循 DDD 的概念, 將子專案對應到一個 Bounded Context。為了確保各個 Context 獨立不耦合,在架構設計上,每個子專案可以定義自己的使用者及角色,還有事件通知。另外在實作方面,每個子專案都是一個 Ktor Module,所以可以透過調整設定檔決定要部署那些模組。如果一個模組沒有被部署,則不會進行任何初始化動作,也就不會部署任何 API,這與一般 Gradle multi-projects 只在專案結構方面模組化建置的作法相比,可以更進一步節省執行期的運行資源。
application {
modules = [
fanpoll.infra.ApplicationKt.main,
fanpoll.ops.OpsProjectKt.opsProject,
fanpoll.club.ClubProjectKt.clubProject
]
}
每個子專案各自擁有以下項目
為了整合 DevOps 流程,本專案內建 Ops 子專案,目前包含 Operation Team 及 AppTeam 2種使用者角色,另外還有 Root 及 Monitor 2種服務角色。每種角色只可以呼叫有其權限的 API,可以進行權限控管。
Club 為展示功能的範例專案, 目前包含 Admin 及 Member 2種使用者角色,以 iOS, Android App 作為使用者客戶端
val devBranchName = "dev"
val releaseBranchName = "main"
val branchToEnvMap: Map<String, String> = mapOf(devBranchName to "dev", releaseBranchName to "prod")
deploy/config/${env}/application-${env}.conf
檔案。deploy/config/dev/application-dev.conf