莫方教程网

专业程序员编程教程与实战案例分享

Ktor - Kotlin Web 框架

进一步了解 Ktor,一个 Kotlin Web 框架。



Ktor是一个为 Kotlin 编写和设计的异步 Web 框架,它利用协程并允许您编写异步代码,而无需自己管理任何线程。

这是有关 Ktor 的更多背景信息。它得到了Jetbrains的支持,Jetbrains也是 Kotlin 本身的创造者。有谁比开发 Kotlin 的人更适合制作 Kotlin Web 框架?

执行

依赖项

buildscript {
  ext.kotlin_version = '1.7.10'
  ext.ktor_version = '2.0.3'

  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

apply plugin: 'java'
apply plugin: 'kotlin'

group 'ktor-and-kodein-di'
version '1.0.0'

repositories {
  mavenLocal()
  mavenCentral()
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
  testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
  testImplementation "junit:junit:4.12"

  implementation "io.ktor:ktor-server-netty:$ktor_version"
  // each plugin is its own dependency which matches the name in code (default-headers = DefaultHeaders)
  implementation "io.ktor:ktor-serialization-jackson:$ktor_version"
  implementation "io.ktor:ktor-server-default-headers:$ktor_version"
  implementation "io.ktor:ktor-server-call-logging:$ktor_version"
  implementation "io.ktor:ktor-server-content-negotiation:$ktor_version"

  implementation group: 'org.kodein.di', name: 'kodein-di-generic-jvm', version: '6.5.1'
  implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
  implementation group: 'com.datastax.oss', name: 'java-driver-core', version: '4.14.1'
}

这里正在发生一些事情。

  • Ktor2.0.3使用1.7我从他们的文档中确定的最低版本的 Kotlin。
  • 引入了依赖ktor-server-netty和几个ktor-server插件。正如ktor-server-netty建议的那样,本文将使用Netty 。根据您选择导入的内容,可以使用不同的底层 Web 服务器。其他可用选项包括 Netty、Jetty、Tomcat 和 CIO。更多信息可以在支持的引擎文档中找到。
  • 引入 Logback来处理日志记录。这不包含在 Ktor 依赖项中,如果您计划进行任何类型的日志记录,则需要它。
  • Kodein是一个用 Kotlin 编写的依赖注入框架。我在这篇文章中松散地使用了它,并且由于代码示例的大小,我可能会完全删除它。

启动 Web 服务器

摆脱了无聊的东西,我现在可以让你完成一个 Web 服务器的实现。下面的代码就是你所需要的:

kotlin

import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
  embeddedServer(Netty, port = 8080, module = Application::module).start()
}

fun Application.module() {
  // code that does stuff which is covered later
}

巴姆。你有它。使用 Ktor 和 Netty 运行的 Web 服务器。好的,是的,它并没有真正做任何事情,但我们将在以下部分中对此进行扩展。

该代码非常不言自明。唯一值得强调的是Application.module功能。该module参数embeddedServer需要Application.() -> Unit提供一个函数来配置服务器,并将成为服务器代码的主要入口点。

在接下来的部分中,我们将扩展内容Application.module,以便您的 Web 服务器真正做一些有价值的事情。

路由

所有传入的请求都将被拒绝,因为没有端点来处理它们。通过设置路由,您可以指定请求可以通过的有效路径以及在请求到达目的地时将处理请求的函数。

这是在一个Routing块(或多个Routing块)内完成的。在块内部,设置了到不同端点的路由:

import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route

routing {
  // All routes defined inside are prefixed with "/people"
  route("/people") {
    // Get a person
    get("/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
    // Create a person
    post {
      val person = call.receive<Person>()
      val result = personRepository.save(person.copy(id = UUID.randomUUID()))
      call.respond(result)
    }
  }
}

由于依赖于扩展函数,导入已包含在内,这使得在没有 IDE 的情况下发现函数变得困难。

routing是一个方便的函数,可以让代码流畅。里面的上下文(又名thisrouting是类型Routing。此外,函数routegetpost都是 的扩展函数Routing

route设置其所有后续端点的基本路径。在这种情况下,/people. get并且post不要自己指定路径,因为基本路径足以满足他们的需求。如果需要,可以为每个路径添加一个路径,例如:

routing {
  // Get a person
  get("/people/{id}") {
    val id = UUID.fromString(call.parameters["id"]!!)
    personRepository.find(id)?.let {
      call.respond(HttpStatusCode.OK, it)
    } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
  }
  // Create a person
  post("/people) {
    val person = call.receive<Person>()
    val result = personRepository.save(person.copy(id = UUID.randomUUID()))
    call.respond(result)
  }
}

在继续下一节之前,我想向您展示我是如何实际实现路由的:

fun Application.module() {
  val personRepository by kodein.instance<PersonRepository>()
  // Route requests to handler functions
  routing { people(personRepository) }
}

// Extracted to a separate extension function to tidy up the code
fun Routing.people(personRepository: PersonRepository) {
  route("/people") {
    // Get a person
    get("/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
    // Create a person
    post {
      val person = call.receive<Person>()
      val result = personRepository.save(person.copy(id = UUID.randomUUID()))
      call.respond(result)
    }
  }
}

我将代码提取到一个单独的函数中以减少Application.module. 在尝试编写更重要的应用程序时,这将是一个好主意。我的方式是否是Ktor方式是另一个问题。快速浏览一下 Ktor 文档,看起来这是一个不错的解决方案。我相信我看到了另一种方法来做到这一点,但我需要花更多的时间来处理它。

请求处理程序的内容

将请求路由到请求处理程序时执行的代码显然非常重要。该功能毕竟需要做一些事情......

每个处理函数都在协程的上下文中执行。我并没有真正使用这个事实,因为我展示的每个功能都是完全同步的。

在本文的其余部分,我将尽量不过多提及协程,因为它们对于这个简单的 REST API 并不是特别重要。

在本节中,get将更仔细地检查该函数:


get("/{id}") {
  val id = UUID.fromString(call.parameters["id"]!!)
  personRepository.find(id)?.let {
    call.respond(HttpStatusCode.OK, it)
  } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
}

{id}表示请求中需要路径变量,其值将存储为id. 可以包含多个路径变量,但本示例只需要一个。该值id是从 中检索的call.parameters,它采用您要访问的变量的名称。

  • call表示当前请求的上下文。
  • parameters是请求参数的列表。

id数据库使用路径变量搜索相应的记录 。在这种情况下,如果存在,则记录与相应的200 OK. 如果不是,则返回错误响应。两者都respond改变respondTextresponse当前的基础call。您可以手动执行此操作,例如,使用:


call.response.status(HttpStatusCode.OK)
call.response.pipeline.execute(call, it)

你可以这样做,但没有任何必要,因为这实际上只是respond. respondText有一些额外的逻辑,但委托response给最终确定一切。此函数中的最终调用 execute表示函数的返回值。

安装插件

在 Ktor 中,可以在需要时安装插件。例如,可以添加Jackson JSON 解析以从应用程序处理和返回 JSON。

以下是安装到示例应用程序的插件:

import io.ktor.http.HttpHeaders
import io.ktor.serialization.jackson.jackson
import io.ktor.server.application.install
import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import org.slf4j.event.Level

fun Application.module() {
  // Adds header to every response
  install(DefaultHeaders) { header(HttpHeaders.Server, "My ktor server") }
  // Controls what level the call logging is logged to
  install(CallLogging) { level = Level.INFO }
  // Setup jackson json serialisation
  install(ContentNegotiation) { jackson() }
}
  • DefaultHeaders使用服务器名称为每个响应添加一个标头。
  • CallLogging记录有关传出响应的信息并指定记录它们的级别。需要包含一个日志库才能使其工作。输出将如下所示:

INFO ktor.application.log - 200 OK: GET - /people/302a1a73-173b-491c-b306-4d95387a8e36

  • ContentNegotiation告诉服务器使用 Jackson 处理传入和传出请求。请记住,这需要包括ktor-serialization-jackson作为依赖项。如果您愿意,也可以使用GSON。

对于 Ktor 包含的其他插件的列表,您可以转到start.ktor.io,您可以在其中查看现有插件(通过假装创建新应用程序)。

安装插件一直与之前完成的路由联系在一起。routing委托到install其实现内部。所以你可以写:


install(Routing) {
  route("/people") {
    get {
      // Implementation
    }
  }
}

无论你的船漂浮什么,但我会坚持使用routing. 希望这可以帮助您了解幕后发生的事情,即使只是一点点。

Kodein简介

我想简要介绍一下Kodein,因为我在这篇文章中使用了它。Kodein 是一个用 Kotlin 编写的依赖注入框架,适用于 Kotlin。下面是我用于示例应用程序的超少量 DI:

科特林

val kodein = Kodein {
  bind<CqlSession>() with singleton { cassandraSession() }
  bind<PersonRepository>() with singleton { PersonRepository(instance()) }
}
val personRepository by kodein.instance<PersonRepository>()

Kodein块内,创建了应用程序类的实例。在这种情况下,每个类只需要一个实例。呼召singleton表明了这一点。instance是 Kodein 提供的用于传递给构造函数而不是实际对象的占位符吗?

在块之外,检索Kodein一个实例。PersonRespository

是的,我知道; 在这里使用 Kodein 没有多大意义,因为我可以用一行代码替换它……

val  personRepository  =  PersonRepository ( cassandraSession ())

相反,让我们认为这是一个非常简洁的例子来理解。

概括

在这篇文章中,我们研究了使用 Ktor 初始化 Web 服务器,将请求路由到生成响应的 lambdas/handlers,以及将插件安装到服务器。在这篇文章中,我们主要停留在表面,专注于基础知识,让您开始使用 Ktor。有关更多信息,值得前往ktor.io并查看 Ktor 的文档和示例。



控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言

    滇ICP备2024046894号-1