简单、有趣的Kotlin异步微服务框架(一): 初始Ktor

4,044 阅读17分钟

1. 写在前面

Flutter异步编程相关的系列文章已经讲解完毕后,大概已经两周没有发布文章,因为我又开始在筹划了另一个Kotlin相关的专题: Kotlin微服务框架Ktor。 这次专题主要是介绍Kotlin在服务端应用的一个微服务异步web框架。

为什么又开始Kotlin服务端Ktor专题呢?

因为之前在2020年度总结文章也说过,后续会主要focus在Kotlin、Flutter、面试三大主题展开一系列文章总结,所以开始Kotlin服务端Ktor专题也是在规划路线之中。不仅会涉及到Kotlin服务端专题后续还会在Kotlin jetpack Compose方面、Flutter系列都会展开。

原来Flutter专题会继续吗?此次Kotlin服务端Ktor专题文章,Flutter学习者有必要看吗?

Flutter专题当然会继续,此次开始Kotlin服务端Ktor专题是希望最后会通过Ktor作为API服务后端框架+Flutter作为页面前台搭建一套完整的从前到后的全栈应用。我们都知道作为一名客户端或前端开发者,上手后端门槛是比较高的。比如Java语言的Spring、SpringBoot框架,Go、python相关的后端框架等等。而现在的Ktor就是一套非常非常简单、轻量级的异步Kotlin后端框架, 它比SpringBoot更加轻量级,仅仅只需少量代码就能快速搭建一套API后端服务。

个人还是强烈推荐Flutter学习者学习下 , 特别是有了Kotlin基础的Android开发者 。对于我们大前端开发者很多的能力思维还仅仅停留在大前端,但是如果你能有一项后端开发的技能,那么你思考问题和解决问题角度就会不一样的。当然我们不需要像后端开发者那样很精通后端开发领域,但是如果能掌握后端基本的开发和使用,还是有很大的帮助的。至少之前作为一名Flutter开发者,学习完后可以自己从前端UI页面搭建到后端API设计、数据表设计撸一整套的应用。为什么不推荐客户端开发者去直接学Spring或SpringBoot,说真的因为框架东西比较多,成本比较高,所以这次Ktor这个微服务框架简单且易用,学习成本较低,所以还是值得试试的。 以下这张图就是后续Ktor专题路线的规划,下面就直接进入正题~ image.png

2. 什么是Ktor

2.1 Ktor基本介绍

用Ktor官方(ktor.io/)一句话来介绍: Ktor是一个用于创建微服务、web应用程序等异步框架,它很简单、有趣并且免费开源。它是由jetbrains官方开源,目前已经有8.2K+ star (github.com/ktorio/ktor),该框架在国内大家可能比较陌生但是在国外还是很受欢迎的,Ktor可以说是为Kotlin中异步而生的框架,它最底层基于Kotlin Coroutine协程框架,支持了Client、Server双端异步特性并且在Client、Server双端上对WebSocket、Socket有了很好的支持。此外它整体具有以下几种特性: image.png

  • 轻量级

Ktor框架可以说是非常轻量级,仅仅有一些Ktor基础引擎内容,并没有冗杂一些其他的功能,甚至日志功能都没有,但是你可以任意选择定制你仅仅需要的功能,以构件形式可插拔地集成到Ktor框架中。

  • 可扩展性强

可扩展性可以说是Ktor框架又一大亮点之一,Ktor框架的本质就Pipeline管道,任何的功能构件都可以可插拔方式集成在Pipeline中。比如Ktor官方提供一系列构件用于构建所需的功能,使用起来非常简单方便。

  • 多平台

借助Kotlin Multiplatform技术构建,可以在任何地方部署Ktor应用程序. image.png

  • 异步

Ktor底层是基于Kotlin协程构建的,Ktor的异步具有很高的可伸缩性,并且利用其非阻塞式特性,从此摆脱了异步回调地狱。

2.2 Ktor的架构组成

Ktor Framework主要分为以下几层,最底层核心是Kotlin协程和基本SDK,然后往上是Ktor核心基础层,包括了引擎、管道、构件、路由、监控等;再往上就是四大主要功能模块分别是Client模块、Server模块、Socket模块、WebSocket模块。那么该专题主要是focus在Server模块,主要利用Server模块来构件web后端服务。关于WebSocket实际上Ktor分别在Client WebSocket和Server WebSocket两个层面都给了很大的支持。后续会基于WebSocket使用构建一个实时IM应用的例子。所以整体上来看Ktor框架还是比较简单和轻量级的,最为功能丰富在于它的功能构件(Feature), 几乎后续所有web后端服务功能都可以看成作为它的一个功能构件(Feature)集成到Ktor中,比如序列化(gson、jackson)、日志、auth认证、template模版(freemarker、velocity)、CORS(解决跨域问题配置)、Session等功能 image.png

3. 如何构建一个简单的Ktor Server应用

构建一个Ktor Server应用可以说是非常非常简单,仅仅只需简单十几行代码就构建一个Server服务。而构建Ktor Server应用主要分为两种 : 一种是通过embeddedServer方式构建,另一种则是通过EngineMain方式构建。

3.1 通过embeddedServer方式构建

通过embeddedServer函数构建Ktor Server应用是一种最为简单的方式也是官方默认推荐使用的一种方式。embeddedServer函数是通过在代码中配置服务器参数并快速运行应用程序的简单方法,不需要额外配置文件。比如在下面的代码段中,它接收服务器容器引擎类型和端口参作为参数,传入Netty服务器容器引擎和端口8080,启动应用后就会在8080端口监听。

  • 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 配置文件中配置应用启动参数,比如服务监听端口等

  • 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应用也是类似,主要是多了一些如何模块化组织业务模块和更清晰化去架构业务。 主要分为以下7个步骤: image.png

4.1 选择构建Server的方式

构建Ktor Server应用主要分为两种: 一种是通过embeddedServer方式构建,另一种则是通过EngineMain方式构建。 具体的选择使用方式参考上面第3节

4.2 选择Server Engine

要想运行Ktor服务器应用程序,就需要首先创建和配置服务器。服务器配置其中就包括服务器引擎配置,各种引擎特定的参数比如主机地址和启动端口等等。 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 //指定用于处理连接,解析消息和执行引擎的内部工作的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(本地环境)、PRE(预发环境)、ONLINE(线上环境)
    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环境
    //通过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环境就切换到本地数据库连接方式
        .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)

    //创建数据库表
    transaction {
        tables.forEach {
            SchemaUtils.create(it)
        }
    }
}

4.4 通过Features添加必要功能构件

在Ktor中一个最典型的请求(Request)-响应(Response)管道模型大致是这样的: 它从一个请求开始,该请求会被路由到特定的程序处理,并经由我们的应用程序逻辑处理,最后做出响应。然而在实际的应用开发中,并不会这么简单的,但是本质上Pipeline是不变的。那么在Ktor如何更加将这个简单管道模型给丰富起来呢? 那就是向管道模式添加各种各样的Feature(功能构件或者功能插件)。 image.png

4.4.1 向管道模型添加功能构件

在许多应用开发中经常会用到一些基础通用的功能,比如内容编码、序列化、cookie、session等,这些基础通用的功能在Ktor中统称为**Features(功能构件)。所有的Features构件都类似一个插件,插入在Request、application Logic和Response切面之间。 image.png 由上图可知,当一个请求Request进来后,首先会通过Routing路由机制路由给一个特定的Handler进行处理;然而在把Request交由Handler处理之前可能会经过若干个Feature处理;然后Handler处理完这个Request请求,就会将Response响应返回给客户端,然而在将响应发送给客户端之前,它还是可能会经过若干个Feature处理,最终Response响应返回到客户端。可以看出整条从Request到Response链路就类似一个工厂流水线,每个Feature各司其职。

4.4.2 Routing本质上也是一个Feature

Feature的灵活性和可插拔性非常强大,它可以出现在Request/Response管道模型中任何一个节点部分。Routing虽然我们称为路由,但其本质也是一个Feature image.png

4.4.3 如何安装Feature

一般都是在应用初始化的时候去安装Feature即可,安装Feature非常简单。仅仅几行 install 即可搞定,如果是非内置的 Feature 还需要自己引入相关lib依赖. 除了使用现有的Feature, 还可以自定义Feature,关于如何自定义Feature属于Ktor高阶命题,后续再展开。

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)
    
//通过源码可以发现,如果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)执行的lambda,
    //也就是创建完child后返回一个Route对象,最终再调用它的handle函数
}

4.6 应用模块化

为了使得Ktor应用更具有可维护性、灵活性以及,Ktor提供一种思路就是将应用按照业务维度进行模块化设计。注意这里模块化概念并不是在项目中的一个Module,而这里module本质上是一个 Application 的扩展函数。并且可以在 application.conf 指定某一个或若干个module进行可插拔式的部署和卸载。 image.png 然后一个Module又包括了一条或若干条Request/Response的管道模型。 image.png 应用模块代码例子如下:

//定义一个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动态配置所需加载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插件。 image.png 安装完Ktor插件后,restart IDEA。

5.2 创建Ktor应用工程并安装Features

打开IDEA,点击new Project, 选择左边栏中的"Ktor"应用,然后输入Project name,选择项目路径、选择构建系统(Groovy Gradle、Kotlin Gradle或Maven)以及选择对应的服务器容器的引擎(Netty、Tomcat、Jetty、CIO). image.png 点击next后,就到需要选择对应安装的Feature(功能构件),Ktor插件提供了不同类型的Features, 主要有Security、Routing、HTTP、Monitoring、Templating、Serialization、Sockets、Administration几大类的Feature, 可以按照自己应用的需求,按需安装即可。 Security类型相关的Features: image.png Routing类型相关的Features: 添加Routing构件用于路由请求的处理 image.png HTTP类型相关的Features: 添加CORS解决跨域访问问题 image.png 监控类型相关的Features: 添加监控日志构件CallLogging构件 image.png 样式模板类型相关的Features: 添加HTML DSL和CSS DSL构件 image.png 序列化类型相关的Features: 添加Gson构件 image.png Sockets类型相关的Features image.png Administration类型相关的Features image.png 最终,下面是我们安装的所有Features,点击Finish即可创建Ktor Server工程 image.png

5.3 Ktor应用工程项目结构

image.png 可以看到所有安装的Features都在plugins包中生成,并在Application类main启动执行的入口函数进行初始化和配置,并且应用程序默认端口为:8080。

  • Routing Feature默认生成的代码:

image.png

  • Template Feature默认生成代码:

image.png

  • 序列化Gson Feature默认生成代码:

image.png

5.4 运行Ktor应用

image.png image.png Ktor应用运行起来后,可以通过localhost访问上述默认生成的页面:

image.png

image.png

image.png

image.png

5.5 Debug Ktor应用

image.png image.png

6. 熊猫先生的小总结

到这里,有关Ktor系列专题的入门第一篇文章就结束了。后面会继续深入Ktor相关的内容,包括如何在Ktor操作数据库,我们会用到Kotlin中的ORM框架Exposed以及如何处理请求然后包装输出restful api给到客户端使用。后续安排是每周一篇文章分享,分别是Kotlin和Flutter相关文章交替进行。

感谢关注,熊喵先生愿和你在技术路上一起成长!