当前位置: 首页 > 工具软件 > Ktor > 使用案例 >

Ktor – Kotlin网络框架

董奇思
2023-12-01

Ktor是用Kotlin编写并设计的异步Web框架。 不仅可以使用Kotlin令人印象深刻的功能(例如协程),还可以作为头等公民获得支持。 通常,Spring是我的通用框架,通常是在需要将REST API放在一起时使用的框架。 但是,在最近参加伦敦Kotlin(Kotlin)聚会并发表有关Ktor的演讲后,我决定尝试一下新事物。 这就是我最终在这里撰写有关Ktor的博客文章的方式。 因此,本文对您和我来说都是一次学习经历。 这篇文章的内容将缺乏经验丰富的建议,但会记录我第一次与Ktor一起旅行时的旅程。

这是有关Ktor的更多背景信息。 它由Jetbrains支持, Jetbrains也是Kotlin本身的创造者。 与使用该语言的男人和女人相比,谁更擅长制作Kotlin网络框架。

实作

依存关系

buildscript {
  ext.kotlin_version = '1.3.41'
  ext.ktor_version = '1.2.2'

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

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

// might not be needed but my build kept defaulting to Java 12
java {
  disableAutoTargetJvm()
}

// Ktor uses coroutines
kotlin {
  experimental {
    coroutines "enable"
  }
}

compileKotlin {
  kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
  kotlinOptions.jvmTarget = "1.8"
}

dependencies {
  // Kotlin stdlib + test dependencies

  // ktor dependencies
  compile "io.ktor:ktor-server-netty:$ktor_version"
  compile "io.ktor:ktor-jackson:$ktor_version"
  // logback for logging
  compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
  // kodein for dependency injection
  compile group: 'org.kodein.di', name: 'kodein-di-generic-jvm', version: '6.3.0'
}

这里发生了一些事情。

  • Ktor要求最低版本的Kotlin 1.3 ,以便可以利用协程。
  • ktor-server-nettyktor-server-netty ktor-jacksonktor-jackson依赖。顾名思义,这意味着Netty将用于此帖子。 可以使用不同的基础Web服务器,具体取决于您选择导入的服务器。 当前,其余选项是JettyTomcat
  • 引入了Logback来处理日志记录。 这不包括在Ktor依赖项中,如果您打算进行任何类型的日志记录,则必须使用它。
  • Kodein是用Kotlin编写的依赖项注入框架。 我在本文中松散地使用了它,由于代码示例的大小,我可能会完全删除它。 这样做的主要原因是为我提供了使用Spring以外的其他东西的机会。 请记住,这也是我尝试Ktor的原因之一。

启动网络服务器

不用烦人的东西了,我现在可以帮助您实现一个简单的Web服务器。 下面的代码就是您所需要的:

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

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

am 你有它。 运行Ktor和Netty的简单Web服务器。 好的,是的,它实际上并没有做任何事情,但是我们将在以下各节中对此进行扩展。 该代码非常不言自明。 唯一值得强调的部分是Application.module函数。 embeddedServermodule参数需要Application的扩展功能。 这将成为使服务器完成工作的主要功能。

在以下各节中,我们将扩展Application.module的内容,以便您的Web服务器实际上可以完成一些工作。

路由

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

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

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)
    }
  }
}

routing是一种便利功能,可以使代码流更流畅。 routing内部的上下文(也称为this )的类型为Routing 。 此外, routegetpost函数都是Routing扩展功能。

route设置了到其所有后续端点的基本路径。 在这种情况下, /peoplegetpost本身不会指定路径,因为基本路径足以满足他们的需求。 如果需要,可以将路径添加到每个路径,例如:

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文档,看来这是一个不错的解决方案。 我相信我看到了另一种方法,但是我需要花更多的时间。

请求处理程序的内容

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

每个处理程序函数都在协程的上下文中执行。 因为我展示的每个功能都是完全同步的,所以我并没有真正利用这个事实。 有关更多信息, 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 。 如果不是,则返回错误响应。 这两种respondrespondText改变基础response当前的call 。 您可以手动执行此操作,例如,使用:

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

你可以这样做,但没有任何必要,因为这实际上只是执行respondrespondText有一些额外的逻辑,但委派给response敲定一切。 在该函数中execute的最终调用代表该函数的返回值。

安装额外功能

在Ktor中,可以在需要时插入其他功能 。 例如,可以添加Jackson JSON解析以处理和返回应用程序中的JSON。 以下是示例应用程序中安装的功能:

fun Application.module() {
  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-jackson作为依赖项。 如果愿意,也可以使用GSON

有关Ktor包括的其他功能的详细列表,请点击此处,轻松链接至他们的docs

安装功能一直与之前完成的路由联系在一起。 向下routing委托以installinstall在其实现中。 所以你可以这样写:

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

无论您的船浮在水上,但我只会坚持使用routing 。 希望这可以帮助您了解幕后情况,即使只是一点点。

科丁简介

自从我在这篇文章中使用它以来,我想对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())

相反,让我们将其视为理解的一个非常简洁的示例。

总结思想

作为一个非常偏向于Spring的人,我发现与Ktor的工作与我以前的工作截然不同。 我花了比平时更长的时间来编写一些我很满意的示例代码。 话虽如此,我认为结果看起来还不错,我将需要花更多的时间在Ktor上,以更好地准确地了解如何从中获得最大的收益。 目前,我相信还有更多需要挤出的Ktor。 有关Ktor的更多信息,我将不得不再次向您参考他们的文档 ,其中包含大量示例和教程。

翻译自: https://www.javacodegeeks.com/2019/08/ktor-kotlin-web-framework.html

 类似资料: