Simple and interesting Kotlin asynchronous microservice framework (1): initial Ktor

1. Write in front

After the series of articles related to asynchronous programming in Flutter have been explained, there have been no articles published for about two weeks, because I have started planning another topic related to Kotlin:  Kotlin microservice framework Ktor . This topic mainly introduces a microservice asynchronous web framework for Kotlin application on the server side.

Why start the Kotlin server-side Ktor topic again?

Because I also said in the 2020 annual summary article, the follow-up will mainly focus on the three major topics of Kotlin, Flutter, and interviews to start a series of article summaries, so the Ktor topic on the Kotlin server is also in the planning route. It will not only involve the Kotlin server topic, but also the Kotlin jetpack Compose and the Flutter series will be launched.

Will the original Flutter topic continue? Is it necessary for Flutter learners to read this Kotlin server-side Ktor special article?

The Flutter topic will of course continue . This time, the Ktor topic on the Kotlin server side hopes to finally build a complete front-to-back full-stack application through Ktor as the API service back-end framework + Flutter as the page front -end. We all know that as a client or front-end developer, the threshold for getting started with the back-end is relatively high. For example, the Java language Spring, SpringBoot framework, Go, python-related back-end frameworks, etc. Now Ktor is a very simple and lightweight asynchronous Kotlin back-end framework, which is more lightweight than SpringBoot, and can quickly build a set of API back-end services with only a small amount of code.

Personally, I strongly recommend Flutter learners to learn, especially Android developers with Kotlin foundation . For our big front-end developers, many of the ability thinking still only stays in the big front-end, but if you can have a back-end development skill, then your thinking and problem-solving angle will be different. Of course, we don't need to be as proficient in the field of back-end development as back-end developers, but if we can master the basic development and use of back-end, it will be of great help. At least as a Flutter developer before, after learning, you can build a complete set of applications from the front-end UI page to the back-end API design and data table design. Why not recommend client developers to learn Spring or SpringBoot directly, because there are many frameworks and the cost is relatively high, so this time the Ktor microservice framework is simple and easy to use, and the learning cost is low, so it is worth trying of. The following picture is the planning of the follow-up Ktor thematic route, and let's go directly to the topic~image.png

2. What is Ktor

2.1 Basic introduction to Ktor

Introduce in one sentence from Ktor official ( ktor.io/ ): Ktor is an asynchronous framework for creating microservices, web applications, etc. It is simple, interesting, and free and open source . It is officially open sourced by jetbrains, and currently has 8.2K+ stars ( github.com/ktorio/ktor ). This framework may be unfamiliar to everyone in China, but it is still very popular abroad. Ktor can be said to be asynchronous in Kotlin. The bottom layer is based on the Kotlin Coroutine coroutine framework, which supports the dual-end asynchronous feature of Client and Server, and has good support for WebSocket and Socket on both sides of Client and Server. In addition, it has the following characteristics as a whole:image.png

  • lightweight

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 generated code by default:

image.png

  • Template Feature generates code by default:

image.png

  • Serialized Gson Feature generates code by default:

image.png

5.4 Running the Ktor application

image.png image.pngAfter the Ktor application is running, the above default generated page can be accessed through localhost:

image.png

image.png

image.png

image.png

5.5 Debug Ktor Application

image.png image.png

6. A brief summary of Mr. Panda

At this point, the first article about the introduction to the Ktor series is over. Later, we will continue to delve into Ktor-related content, including how to operate the database in Ktor. We will use the ORM framework Exposed in Kotlin and how to process requests and then package and output the restful api for the client to use. The follow-up arrangement is to share an article every week, with Kotlin and Flutter related articles alternately.

Thank you for your attention, Mr. Xiong Miao is willing to grow up with you on the road of technology!

Guess you like

Origin juejin.im/post/6955132053532704804