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-netty
对ktor-server-netty
ktor-jackson
和ktor-jackson
依赖。顾名思义,这意味着Netty将用于此帖子。 可以使用不同的基础Web服务器,具体取决于您选择导入的服务器。 当前,其余选项是Jetty和Tomcat 。 - 引入了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
函数。 embeddedServer
的module
参数需要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
。 此外, route
, get
和post
函数都是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文档,看来这是一个不错的解决方案。 我相信我看到了另一种方法,但是我需要花更多的时间。
请求处理程序的内容
将请求路由到请求处理程序时执行的代码显然很重要。 该功能毕竟需要做一些事情……
每个处理程序函数都在协程的上下文中执行。 因为我展示的每个功能都是完全同步的,所以我并没有真正利用这个事实。 有关更多信息, 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
和respondText
改变基础response
当前的call
。 您可以手动执行此操作,例如,使用:
call.response.status(HttpStatusCode.OK)
call.response.pipeline.execute(call, it)
你可以这样做,但没有任何必要,因为这实际上只是执行respond
。 respondText
有一些额外的逻辑,但委派给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
委托以install
其install
在其实现中。 所以你可以这样写:
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