1. 寫在前面
這次主要是介紹Kotlin在服務端應用的一個微服務異步web框架, 它可以快速構建異步非阻塞式web應用程序和微服務應用。主要底層借助于kotlin coroutine協(xié)程框架,具有很強的異步性和可伸縮性。下面將會從0到1教你如何構建Ktor應用。
2. 什么是Ktor
2.1 Ktor基本介紹
用Ktor官方(https://ktor.io/)一句話來介紹: Ktor是一個用于創(chuàng)建微服務、web應用程序等異步框架,它很簡單、有趣并且免費開源。它是由jetbrains官方開源,目前已經有8.2K+ star (https://github.com/ktorio/ktor),該框架在國內大家可能比較陌生但是在國外還是很受歡迎的,Ktor可以說是為Kotlin中異步而生的框架,它最底層基于Kotlin Coroutine協(xié)程框架,支持了Client、Server雙端異步特性并且在Client、Server雙端上對WebSocket、Socket有了很好的支持。此外它整體具有以下幾種特性:
- 輕量級
Ktor框架可以說是非常輕量級,僅僅有一些Ktor基礎引擎內容,并沒有冗雜一些其他的功能,甚至日志功能都沒有,但是你可以任意選擇定制你僅僅需要的功能,以構件形式可插拔地集成到Ktor框架中。
- 可擴展性強
可擴展性可以說是Ktor框架又一大亮點之一,Ktor框架的本質就Pipeline管道,任何的功能構件都可以可插拔方式集成在Pipeline中。比如Ktor官方提供一系列構件用于構建所需的功能,使用起來非常簡單方便。
- 多平臺
借助Kotlin Multiplatform技術構建,可以在任何地方部署Ktor應用程序.
- 異步
Ktor底層是基于Kotlin協(xié)程構建的,Ktor的異步具有很高的可伸縮性,并且利用其非阻塞式特性,從此擺脫了異步回調地獄。
2.2 Ktor的架構組成
Ktor Framework主要分為以下幾層,最底層核心是Kotlin協(xié)程和基本SDK,然后往上是Ktor核心基礎層,包括了引擎、管道、構件、路由、監(jiān)控等;再往上就是四大主要功能模塊分別是Client模塊、Server模塊、Socket模塊、WebSocket模塊。那么該專題主要是focus在Server模塊,主要利用Server模塊來構件web后端服務。關于WebSocket實際上Ktor分別在Client WebSocket和Server WebSocket兩個層面都給了很大的支持。后續(xù)會基于WebSocket使用構建一個實時IM應用的例子。所以整體上來看Ktor框架還是比較簡單和輕量級的,最為功能豐富在于它的功能構件(Feature), 幾乎后續(xù)所有web后端服務功能都可以看成作為它的一個功能構件(Feature)集成到Ktor中,比如序列化(gson、jackson)、日志、auth認證、template模版(freemarker、velocity)、CORS(解決跨域問題配置)、Session等功能
3. 如何構建一個簡單的Ktor Server應用
構建一個Ktor Server應用可以說是非常非常簡單,僅僅只需簡單十幾行代碼就構建一個Server服務。而構建Ktor Server應用主要分為兩種**: 一種是通過embeddedServer方式構建,另一種則是通過EngineMain方式構建。**
3.1 通過embeddedServer方式構建
通過embeddedServer函數構建Ktor Server應用是一種最為簡單的方式也是官方默認推薦使用的一種方式。embeddedServer函數是通過在代碼中配置服務器參數并快速運行應用程序的簡單方法,不需要額外配置文件。比如在下面的代碼段中,它接收服務器容器引擎類型和端口參作為參數,傳入Netty服務器容器引擎和端口8080,啟動應用后就會在8080端口監(jiān)聽。
- Application.kt
package com.mikyou.ktor.samplecom.mikyou.ktor.sample
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {//除了支持Netty還支持Jetty、Tomcat、CIO(Coroutine-based I/O)
routing {
get("/") {
call.respondText("Hello Ktor")
}
}
}.start(wait = true)
}
3.2 通過EngineMain方式構建
EngineMain方式則是選定的引擎啟動服務器,并加載外部一個 application.conf
文件中指定的應用程序模塊. 然后在 application.conf
配置文件中配置應用啟動參數,比如服務監(jiān)聽端口等
- Application.kt
package com.mikyou.ktor.sample
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
fun Application.module(testing: Boolean = false) {//該module函數實際上是Application的擴展函數,要想該函數運行需要通過application.conf中配置該函數
routing {
get("/") {
call.respondText("Hello Ktor")
}
}
}
- application.conf
ktor {
deployment {
port = 8080 //配置端口
}
application {
modules = [ com.mikyou.ktor.sample.ApplicationKt.module ] //配置加載需要加載的module函數
}
}
4. 如何架構一個成熟的Ktor應用
由上面可知構建一個簡單的Ktor Server可以說是非常簡單,然而要構建一個成熟的Ktor Server應用也是類似,主要是多了一些如何模塊化組織業(yè)務模塊和更清晰化去架構業(yè)務。
主要分為以下7個步驟:
4.1 選擇構建Server的方式
構建Ktor Server應用主要分為兩種**: 一種是通過embeddedServer方式構建,另一種則是通過EngineMain方式構建。**具體的選擇使用方式參考上面第3節(jié)
4.2 選擇Server Engine
要想運行Ktor服務器應用程序,就需要首先創(chuàng)建和配置服務器。服務器配置其中就包括服務器引擎配置,各種引擎特定的參數比如主機地址和啟動端口等等。 Ktor支持大多數目前主流的Server Engine,其中包括:
- Tomcat
- Netty
- Jetty
- CIO(Coroutine-based I/O)
此外Ktor框架還提供一種類型引擎TestEngine專門供測試時使用。
要想使用上述指定的Server Engine,就需要添加Server Engine相關的依賴,Ktor是既支持Gradle來管理庫的依賴也支持Maven來管理。
4.3 配置服務參數
配置服務引擎參數,由于構建Server方式不同,所以配置引擎參數也不一樣。對于embeddedServer函數方式構建的Ktor應用可以直接通過代碼函數參數方式指定,對于EngineMain方式則通過修改配置文件 application.conf
。
4.3.1 embeddedServer函數方式
fun main(args: Array<String>) {
embeddedServer(Tomcat, port = 8080) {//配置了服務器引擎類型和啟動端口
routing {
get("/") {
call.respondText("Hello Ktor")
}
}
}.start(wait = true)
}
//除了服務器引擎類型和啟動端口的配置,還支持一些參數的配置
fun main() {
embeddedServer(Netty, port = 8080, configure = {
connectionGroupSize = 2 //指定用于接收連接的Event Group的大小
workerGroupSize = 5 //指定用于處理連接,解析消息和執(zhí)行引擎的內部工作的Event Group的大小,
callGroupSize = 10 //指定用于運行應用程序代碼的Event Group的大小
}) {
routing {
get("/") {
call.respondText("Hello Ktor")
}
}
}.start(wait = true)
}
//設置可以定制一個EngineEnvironment用于替代默認的ApplicationEngineEnvironment,我們可以通過源碼可知,embeddedServer函數內部默認構建一個ApplicationEngineEnvironment。
fun main() {
embeddedServer(Netty, environment = applicationEngineEnvironment {
log = LoggerFactory.getLogger("ktor.application")
config = HoconApplicationConfig(ConfigFactory.load())
module {
main()
}
connector {
port = 8080
host = "127.0.0.1"
}
}).start(true)
}
4.3.2 EngineMain方式
- 如果是選擇EngineMain方式構建Server, 那么就需要通過修改
applicaton.conf
ktor {
application {
modules = [ com.mikyou.ktor.sample.ApplicationKt.module ] //配置加載需要加載的module模塊,這里配置實際上就是Application中module擴展函數
}
}
//除了可以配置需要加載module模塊,還可以配置端口或主機,SSL等
ktor {
deployment {
port = 8080 //配置端口
sslPort = 8443 //配置SSL端口
watch = [ http2 ]
}
application {
modules = [ com.mikyou.ktor.sample.ApplicationKt.module ] //配置加載需要加載的module模塊
}
security {//配置SSL簽名和密鑰
ssl {
keyStore = build/test.jks
keyAlias = testkey
keyStorePassword = test
privateKeyPassword = test
}
}
}
//application.conf文件包含一個自定義jwt(Json Web Token)組,用于存儲JWT設置。
ktor {
deployment {
port = 8080 //配置端口
sslPort = 8443 //配置SSL端口
watch = [ http2 ]
}
application {
modules = [ com.mikyou.ktor.sample.ApplicationKt.module ] //配置加載需要加載的module模塊
}
security {//配置SSL簽名和密鑰
ssl {
keyStore = build/test.jks
keyAlias = testkey
keyStorePassword = test
privateKeyPassword = test
}
}
jwt {//JWT配置
domain = "https://jwt-provider-domain/"
audience = "jwt-audience"
realm = "ktor sample app"
}
}
- 預定義屬性
- 命令行運行
可以使用command運行ktor的jar,并且指定端口
java -jar sample-app.jar -port=8080
可以通過config參數指定xxx.conf的路徑
java -jar sample-app.jar -config=xxx.conf
還可以通過-P指定運行應用程序代碼的Event Group的大小
java -jar sample-app.jar -P:ktor.deployment.callGroupSize=7
- 代碼中讀取application.conf中的配置
代碼中讀取application.conf中配置是一件很實用的操作,比如連接數據庫時配置都可以通過自定義屬性來實現。比如下面這個例子:
ktor {
deployment {//預定義屬性
port = 8889
host = www.youkmi.cn
}
application {
modules = [ com.mikyou.ApplicationKt.module ]
}
#LOCAL(本地環(huán)境)、PRE(預發(fā)環(huán)境)、ONLINE(線上環(huán)境)
env = LOCAL//自定義屬性
security {//把db相關配置放入security,日志輸出會對該部分內容用*進行隱藏處理
localDb {//自定義屬性localDb
url = "jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
driver = "com.mysql.cj.jdbc.Driver"
user = "xxx"
password = "xxx"
}
remoteDb {//自定義屬性remoteDb
url = "jdbc:mysql://192.168.0.101:3306/mydb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
driver = "com.mysql.cj.jdbc.Driver"
user = "xxx"
password = "xxx"
}
}
}
在appliction.conf自定義了屬性配置后,如何在Ktor代碼獲取呢?請看如下代碼:
const val KEY_ENV = "ktor.env"
//自定義屬性的key,就是根據配置中層級通過.連接,有點類似JSON的取值調用
const val KEY_LOCAL_DB_URL = "ktor.security.localDb.url"
const val KEY_REMOTE_DB_URL = "ktor.security.remoteDb.url"
const val KEY_LOCAL_DB_DRIVER = "ktor.security.localDb.driver"
const val KEY_REMOTE_DB_DRIVER = "ktor.security.remoteDb.driver"
const val KEY_LOCAL_DB_USER = "ktor.security.localDb.user"
const val KEY_REMOTE_DB_USER = "ktor.security.remoteDb.user"
const val KEY_LOCAL_DB_PWD = "ktor.security.localDb.password"
const val KEY_REMOTE_DB_PWD = "ktor.security.remoteDb.password"
fun Application.configureDb(vararg tables: Table) {
//獲取當前Env環(huán)境
//通過Application中environment實例對象拿到其config對象,通過config以key-value形式獲取配置中的值,不過只支持獲取String和List
val env = environment.config.propertyOrNull(KEY_ENV)?.getString() ?: "LOCAL"
val url = environment
.config
.property(if (env == "LOCAL") KEY_LOCAL_DB_URL else KEY_REMOTE_DB_URL)//如果是LOCAL環(huán)境就切換到本地數據庫連接方式
.getString()
val driver = environment
.config
.property(if (env == "LOCAL") KEY_LOCAL_DB_DRIVER else KEY_REMOTE_DB_DRIVER)
.getString()
val user = environment
.config
.property(if (env == "LOCAL") KEY_LOCAL_DB_USER else KEY_REMOTE_DB_USER)
.getString()
val pwd =environment
.config
.property(if (env == "LOCAL") KEY_LOCAL_DB_PWD else KEY_REMOTE_DB_PWD)
.getString()
//連接數據庫
Database.connect(url = url, driver = driver, user = user, password = pwd)
//創(chuàng)建數據庫表
transaction {
tables.forEach {
SchemaUtils.create(it)
}
}
}
4.4 通過Features添加必要功能構件
在Ktor中一個最典型的請求(Request)-響應(Response)管道模型大致是這樣的: 它從一個請求開始,該請求會被路由到特定的程序處理,并經由我們的應用程序邏輯處理,最后做出響應。然而在實際的應用開發(fā)中,并不會這么簡單的,但是本質上Pipeline是不變的。那么在Ktor如何更加將這個簡單管道模型給豐富起來呢? 那就是向管道模式添加各種各樣的Feature(功能構件或者功能插件)。
4.4.1 向管道模型添加功能構件
在許多應用開發(fā)中經常會用到一些基礎通用的功能,比如內容編碼、序列化、cookie、session等,這些基礎通用的功能在Ktor中統(tǒng)稱為**Features(功能構件)。**所有的Features構件都類似一個插件,插入在Request、application Logic和Response切面之間。
由上圖可知,當一個請求Request進來后,首先會通過Routing路由機制路由給一個特定的Handler進行處理;然而在把Request交由Handler處理之前可能會經過若干個Feature處理;然后Handler處理完這個Request請求,就會將Response響應返回給客戶端,然而在將響應發(fā)送給客戶端之前,它還是可能會經過若干個Feature處理,最終Response響應返回到客戶端??梢钥闯稣麠l從Request到Response鏈路就類似一個工廠流水線,每個Feature各司其職。
4.4.2 Routing本質上也是一個Feature
Feature的靈活性和可插拔性非常強大,它可以出現在Request/Response管道模型中任何一個節(jié)點部分。Routing雖然我們稱為路由,但其本質也是一個Feature
4.4.3 如何安裝Feature
一般都是在應用初始化的時候去安裝Feature即可,安裝Feature非常簡單。僅僅幾行 install
即可搞定,如果是非內置的 Feature
還需要自己引入相關lib依賴. 除了使用現有的Feature, 還可以自定義Feature,關于如何自定義Feature屬于Ktor高階命題,后續(xù)再展開。
import io.ktor.features.*
fun Application.main() {
install(Routing)
install(Gson)
//...
}
//除了在main函數中安裝,還可以在module入口函數中安裝
fun Application.module() {
install(Routing)
install(Gson)
//...
}
4.5 通過Routing處理請求
Routing本質上也是一個Feature,所以Routing也需要進行install,然后就可以定義Route Handler處理請求了。
4.5.1 安裝Routing路由
import io.ktor.routing.*
install(Routing) {
// ...
}
//或者直接調用Application的routing擴展函數
import io.ktor.routing.*
routing {
// ...
}
//因為Application的routing擴展函數內部做了處理,對于未安裝Routing會自動安裝Routing的容錯,可以稍微瞅下源碼
@ContextDsl
public fun Application.routing(configuration: Routing.() -> Unit): Routing =
featureOrNull(Routing)?.apply(configuration) ?: install(Routing, configuration)
//通過源碼可以發(fā)現,如果configuration沒有安裝Routing就會自動安裝Routing,所以大家一般看到的Routing都沒有手動install過程,而是直接類似下面的代碼。
fun main(args: Array<String>) {
embeddedServer(Tomcat, port = 8080) {
routing {//直接調用Application的擴展函數routing,內部做了對于未安裝Routing會自動安裝Routing的容錯處理
get("/") {
call.respondText("Hello Ktor")
}
}
}.start(wait = true)
}
4.5.2 定義路由處理的Handler
可以看下下面最簡單的一個get服務的定義,下面用get源碼來解讀:
fun main(args: Array<String>) {
embeddedServer(Tomcat, port = 8080) {
routing {
get("/") {//可以看到這個處理get請求的handler,它實際上是一個Route的擴展函數,一起來看看源碼
call.respondText("Hello Ktor")
}
}
}.start(wait = true)
}
//Route.get函數源碼,其實一個Route對象就是一個對應的Handler,
@ContextDsl
public fun Route.get(path: String, body: PipelineInterceptor<Unit, ApplicationCall>): Route {
return route(path, HttpMethod.Get) { //route函數本質上是一個Route的擴展函數
handle(body) //通過調用Route對象來處理的請求
}
}
//route函數本質上是一個Route的擴展函數
@ContextDsl
public fun Route.route(path: String, method: HttpMethod, build: Route.() -> Unit): Route {
val selector = HttpMethodRouteSelector(method)
return createRouteFromPath(path).createChild(selector).apply(build)//最終調用apply返回Route對象,build是傳入handle(body)執(zhí)行的lambda,
//也就是創(chuàng)建完child后返回一個Route對象,最終再調用它的handle函數
}
4.6 應用模塊化
為了使得Ktor應用更具有可維護性、靈活性以及,Ktor提供一種思路就是將應用按照業(yè)務維度進行模塊化設計。注意這里模塊化概念并不是在項目中的一個Module,而這里module本質上是一個 Application
的擴展函數。并且可以在 application.conf
指定某一個或若干個module進行可插拔式的部署和卸載。
然后一個Module又包括了一條或若干條Request/Response的管道模型。
應用模塊代碼例子如下:
//定義一個accountModule,實際上是一個Application的擴展函數
fun Application.accountModule() {
routing {
loginRoute()
bindPhoneRoute()
getSmsCodeRoute()
registerRoute()
}
}
//在application.conf配置加載對應的accountModule模塊
ktor {
#LOCAL、PRE、ONLINE
env = LOCAL
deployment {
port = 8889
host = www.youkmi.cn
}
application {
//可以在modules動態(tài)配置所需加載Module,第一個com.mikyou.ApplicationKt.module默認是主Module,用于加載一些基礎通用的Features,實現模塊的可插拔式的安裝和卸載
modules = [ "com.mikyou.ApplicationKt.module","com.mikyou.modules.account.AccountModuleKt.accountModule"]//配置accountModule,注意配置路徑,例如定義Account模塊的類文件是AccountModule.kt, 所以它對應類名稱就是AccountModuleKt,所以accountModule模塊類路徑就是com.mikyou.modules.account.AccountModuleKt.accountModule。
}
//...
}
4.7 應用結構化
Ktor在提供靈活性方面提供多種方式來組織和結構化應用。
4.7.1 以文件來形式組織
將單個文件中相關的路由分組管理,比如應用處理訂單和用戶,就會單獨建立兩個文件: OrderRoutes.kt和CustomerRoutes.kt文件分別管理相關路由請求。
- OrderRoutes.kt
fun Route.orderByIdRoute() {
get("/order/{id}") {
}
}
fun Route.createOrderRoute() {
post("/order") {
}
}
- CustomerRoutes.kt
fun Route.customerById() {
get("/customer/{id}") {
}
}
fun Route.createCustomer() {
post("/customer") {
}
}
4.7.2 以路由定義形式組織
fun Application.accountModule() {
routing {
loginRoute()
bindPhoneRoute()
registerRoute()
}
}
//登錄
private fun Route.loginRoute() {
post("/api/login") {
//...
}
}
//注冊
private fun Route.registerRoute() {
post("/api/register") {
//...
}
}
//綁定手機號
private fun Route.bindPhoneRoute() {
post("/api/bindPhone") {
//...
}
}
5. 使用IntelliJ IDEA快速構建Ktor Server應用
IntelliJ IDEA提供一個Ktor應用插件可以快速構建Ktor Server應用,其中可以借助Ktor插件可視化地安裝各種Feature功能構件。下面會一步一步引導快速構建一個Ktor Server應用。
5.1 安裝Ktor插件
在IDEA中的plugins模塊中,搜索ktor安裝Ktor插件。
安裝完Ktor插件后,restart IDEA。
5.2 創(chuàng)建Ktor應用工程并安裝Features
打開IDEA,點擊new Project, 選擇左邊欄中的"Ktor"應用,然后輸入Project name,選擇項目路徑、選擇構建系統(tǒng)(Groovy Gradle、Kotlin Gradle或Maven)以及選擇對應的服務器容器的引擎(Netty、Tomcat、Jetty、CIO).
點擊next后,就到需要選擇對應安裝的Feature(功能構件),Ktor插件提供了不同類型的Features, 主要有Security、Routing、HTTP、Monitoring、Templating、Serialization、Sockets、Administration幾大類的Feature, 可以按照自己應用的需求,按需安裝即可。
Security類型相關的Features:
Routing類型相關的Features: 添加Routing構件用于路由請求的處理
HTTP類型相關的Features: 添加CORS解決跨域訪問問題
監(jiān)控類型相關的Features: 添加監(jiān)控日志構件CallLogging構件
樣式模板類型相關的Features: 添加HTML DSL和CSS DSL構件
序列化類型相關的Features: 添加Gson構件
Sockets類型相關的Features
Administration類型相關的Features
最終,下面是我們安裝的所有Features,點擊Finish即可創(chuàng)建Ktor Server工程
5.3 Ktor應用工程項目結構
可以看到所有安裝的Features都在plugins包中生成,并在Application類main啟動執(zhí)行的入口函數進行初始化和配置,并且應用程序默認端口為:8080。
- Routing Feature默認生成的代碼:
- Template Feature默認生成代碼:
- 序列化Gson Feature默認生成代碼:
5.4 運行Ktor應用
Ktor應用運行起來后,可以通過localhost訪問上述默認生成的頁面:
- Routing Feature默認生成的頁面結果: http://localhost:8080/
- Template Feature默認生成的html-dsl頁面結果: http://localhost:8080/html-dsl
- Template Feature默認生成的html-dsl頁面結果: http://localhost:8080/html-css-dsl
- Gson Feature默認生成的頁面結果: http://localhost:8080/json/gson
5.5 Debug Ktor應用
6.總結
到這里有關Kotlin應用于服務端的微服務開發(fā)就簡單介紹完畢了。實際上只是一個簡單入門,有關很多ktor的高級應用可以去參考Ktor的官網。