IntelliJ IDEA
IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin
Ktor 101:高效的 JVM HTTP 工具包
简介
作为一名经验丰富的 Java 开发者,您可能已经熟悉了像 Spring Boot 这样强大的全功能 Web 框架。 今天,我们将探索 Ktor,它是一款用于在 JVM 上使用 Kotlin 构建服务器应用程序的工具包。 Ktor 最初的设计目标就是充分利用 Kotlin 的功能(包括协程)以构建高效且灵活的 Web 应用程序。
作为一个 Web 应用程序工具包,Ktor 提供了构建应用程序所需的基本组件,如路由、身份验证,以及处理各种协议(包括 HTTP 和 WebSockets)的实用工具。 对于其他用例(如处理数据库),开发者可以自由选择满足自己需求的任何库。
在本文中,我们将简要介绍 Ktor 的功能,以帮助您快速上手这款工具。
“Hello, World!”应用
使用 Ktor 开始一个新项目很简单。 您可以使用 start.ktor.io 上提供的项目生成器或 IntelliJ IDEA 中的 New Project(新建项目)向导。
使用 Ktor 编写的 Hello World 应用程序的等效代码如下:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
routing {
get("/hello") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}
首先,我们实例化一个使用 Netty 作为底层引擎并在端口 8080 上运行的服务器。
接下来,我们定义一个特定的路由来处理传入请求。 在此实例中,我们指示服务器在向 /hello 路径发出请求时返回纯文本消息 Hello, World! 作为响应。
最后,我们启动服务器并指示其等待,从而防止应用程序立即终止。
对于 Ktor 而言,实现上述操作非常简单。 要实现其他功能,我们需要在 routing 函数内定义额外的 HTTP 谓词及其相应的 URL。 例如,如果我们要响应 POST,只需添加另一个函数:
routing {
get("/hello") {
call.respondText("Hello, World!")
}
post("/hello") {
// …
}
}
或者,我们可以通过声明一个 route 将端点重新分组,并为该路由指定 HTTP 谓词,代码如下所示:
routing {
route("/hello") {
get {
call.respondText("Hello, World!")
}
post {
// …
}
}
}
将端点分组到路由中对于在每个路由的作用域内启用 Ktor 插件(如内容协商)非常有用。
为了演示 Ktor 如何利用 Kotlin 的功能,我们将各个端点提取到 Route 类型的扩展函数中:
routing {
route("/hello") {
getEndpoint()
postEndpoint()
}
}
private fun Route.getEndpoint() {
get { … }
}
private fun Route.postEndpoint() {
post { … }
}
新函数 getEndpoint 和 postEndpoint 被定义为 Ktor 的 Route 类的扩展,这意味着它们只能在 route { … } DSL 块内使用。 Ktor 高度依赖类型安全的构建器来实现其 DSL,从而使代码样式更具声明性。
内容协商
内容协商的目的是在客户端与服务器之间通信时处理不同数据格式之间的转换。 在 Ktor 中,内容协商还支持根据 HTTP 请求和响应中的 Content-Type 头对数据格式(如 JSON、XML 或其他类型)进行自动序列化和反序列化。
以下示例演示了如何将内容协商与 JSON 序列化一起使用来处理消息数据类:
@Serializable
data class Message(val id: String, val message: String)
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(ContentNegotiation) {
json()
}
routing {
route("/hello") {
get {
call.respond(Message("123ABC", "Hello, World!"))
}
post {
val message = call.receive()
// do something with the message
}
}
}
}.start(wait = true)
首先,即使项目的类路径中存在实现相应功能的依赖项,在 Ktor 中,我们也必须显式启用此功能。 可以使用功能名称位于配置块后面的 install 函数实现这一点。
install(ContentNegotiation) {
json()
}
这种方式避免了类路径中意外存在依赖项工件而可能带来的隐式行为。
只要配置了内容协商,端点就可以随时与预期格式交互。 在我们的示例中,Message 数据类将在 call.respond(...) 调用的 GET 端点中序列化为 JSON,并在 call.receive() 调用的 POST 端点中反序列化。
插件
我们在之前的内容协商部分中已经提到过插件。 Ktor 提供了一些开箱即用的插件,以及一个简单的 API 来实现您自己的插件。 不过,Ktor 并不阻止您在没有插件的前提下使用任何您喜欢的库。 插件只是一种能够实现通用配置和一致代码样式的便利工具。
install(ContentNegotiation) {
json()
}
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}
install(Compression) {
gzip {
minimumSize(1024)
}
}
install(CORS) {
allowMethod(HttpMethod.Put)
allowHeader("MyCustomHeader")
}
可以为应用程序实例或所选路由启用插件。 可以在项目生成器或插件注册表的 GitHub 仓库中找到插件的完整列表,插件开发者可以通过注册他们的插件来做出贡献。
通过拦截器实现扩展
Ktor 提供了一种强大的拦截机制,允许您为应用程序添加自定义行为。 这提供了类似面向切面编程的灵活性,但以更符合 Kotlin 习惯的方式实现。
要创建拦截器,我们只需使用 createApplicationPlugin 函数创建一个应用程序插件的实例。 在插件中,我们可以使用一组提供对调用不同阶段的访问的处理程序来实现自定义逻辑,从而处理各种请求和响应。
val UserAgentValidation = createApplicationPlugin("UserAgent") {
onCall { call ->
val userAgent = call.request.headers["User-Agent"]
?: throw UnsupportedUserAgentException()
if (userAgent.isNotBrowser())
call.respond(HttpStatusCode.Forbidden)
}
}
install(UserAgentValidation)
在我们的示例中,onCall 函数实现了检查用户代理标头,并拒绝任何不是 Web 浏览器的客户端的逻辑。 请记住,我们仍需使用 install 函数来激活插件。
Kotlin 协程
Ktor 项目最初是作为 Kotlin 协程的测试平台。 这就是为什么它提供对异步编程的原生支持。 这样可以编写更直观、看起来是按顺序的,但实际上是非阻塞的代码。
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.module() {
routing {
get("/api/users") {
selectAllUsers().let {
call.respond(it)
}
}
}
}
suspend fun selectAllUsers(): List =
newSuspendedTransaction(Dispatchers.IO) {
UserTable.selectAll()
.orderBy(UserTable.id, SortOrder.ASC)
.map { row: ResultRow ->
User(
userId = row[UserTable.id],
name = row[UserTable.name],
// map more columns
)
}
}
object UserTable : Table("users") {
val id = long("id").autoIncrement()
val name = varchar("name", 50).uniqueIndex()
val email = text("email").uniqueIndex()
val link = text("link").nullable()
val userType = enumeration("user_type")
override val primaryKey: Table.PrimaryKey = PrimaryKey(id)
}
在此示例中,selectAllUsers 标记为 suspend 关键字,这意味着它可以在不阻塞线程的情况下暂停其执行。 在此示例中,我们使用 Exposed 库来访问数据库,随后将查询的结果映射到 User 对象。
您将注意到编辑器的装订区域会显示特殊图标,指示在相应行上存在挂起的函数调用:

在第 17、19 和 21 行,装订区域中的图标表示异步函数的调用。
使用 Ktor 时,您将注意到尽管应用程序代码看起来是按顺序的,但装订区域中有许多用这些图标高亮显示的函数。 这是一个提示,表明这些位置的执行实际上是异步的。
WebSocket
得益于协程,Ktor 使您可以利用 WebSocket 轻松为应用程序添加实时双向通信。 以下示例展示了一个使用 WebSocket 的超简单回显服务器:
fun main() {
embeddedServer(Netty, port = 8080) {
install(WebSockets)
routing {
webSocket("/chat") {
send("You are connected!")
for (frame in incoming) {
frame as? Frame.Text ?: continue
val receivedText = frame.readText()
send("You said: $receivedText")
}
}
}
}.start(wait = true)
}
服务器在 /chat 处设置了一个 WebSocket 端点,该端点会回显以“You said:”为前缀的消息。
可测试性
Ktor 在设计时考虑了可测试性。 它提供了一个测试引擎,允许您在不启动真实服务器的情况下测试应用程序:
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.*
class ApplicationTest {
@Test
fun testRoot() = testApplication {
application {
module()
}
client.get("/").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("Hello, World!", bodyAsText())
}
}
}
提供的测试代码演示了 Ktor 如何允许您利用其 TestEngine 为您的应用程序编写测试。 测试模拟向 Ktor 服务器发出 HTTP 请求,而无需启动实际服务器,在开发过程中,这种做法更快且更容易管理。 这样一来,为 Ktor 应用程序编写全面测试变得轻而易举,包括完整的集成测试。
身份验证
Ktor 提供了一个支持各种身份验证方法的灵活身份验证系统。 下面是一个基本身份验证的示例:
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(Authentication) {
basic("auth-basic") {
realm = "Access to the '/' path"
validate { credentials ->
if (credentials.name == "username"
&& credentials.password == "password") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
routing {
authenticate("auth-basic") {
get("/") {
call.respondText("Hello,
${call.principal()?.name}!")
}
}
}
}.start(wait = true)
该示例演示了如何在 Ktor 中实现基本身份验证。 当客户端(如浏览器)在没有提供 Authorization 头的情况下请求根路径(“/”)时,服务器会以 401(未授权)状态代码响应。 它还包括一个 WWW-Authenticate 响应头,表明该路由受基本身份验证保护。
收到此响应后,客户端通常会显示一个登录对话框,供用户输入其凭据。 随后,客户端发出新请求,这次包括一个包含 Base64 编码的用户名和密码对的 Authorization 头。
在服务器收到经过正确身份验证的请求后,它会验证凭据并允许访问根端点(“/”)。 在此示例中,端点以个性化消息响应:“Hello, username”。
此外,Ktor 还支持其他身份验证方法,包括基于表单、JWT 和 OAuth。
提供静态内容
提供静态内容是路由功能不可或缺的一部分。 因此,不需要额外的插件,并且该功能默认启用。 要配置资源映射,我们可以使用 Ktor 的 API 函数,例如下面示例中的 staticResources:

根路径(“/”)映射到项目中的 `static` 文件夹。
状态页面
通过使用状态页面插件,我们可以配置应用程序如何响应异常和状态代码。
install(StatusPages) {
exception { call, cause ->
call.respond(HttpStatusCode.Forbidden)
}
exception { call, cause ->
call.respondText(text = "500: ${cause.localizedMessage}",
status = HttpStatusCode.InternalServerError)
}
status(HttpStatusCode.NotFound) { call, _ ->
call.respondRedirect("/404.html", permanent = true) // 301
}
}
此设置为未授权访问和执行期间可能发生的任何其他未处理异常提供自定义响应。 对于 404 状态代码,请求会重定向到静态资源(假定静态资源已相应配置)。
结论
Ktor 提供了一种 Kotlin 优先的全新方式来构建服务器端应用程序。 基于协程的架构、模块化设计和类型安全的 API 为现代应用程序开发提供了一个强大的工具包。 Ktor 的灵活性和性能使其成为微服务、API 后端和其他现代应用程序架构的绝佳选择。
对于想要在服务器端应用程序中充分利用 Kotlin 优势的 Java 开发者而言,Ktor 提供了一个值得探索且极具吸引力的工具包。 对于已经熟悉 Kotlin 的用户来说,Ktor 的学习曲线相对平缓,而且在代码简洁性和性能方面的好处可能十分显著。
本文简要介绍了 Ktor 的功能。 有关更多示例,建议查看 GitHub 上的示例仓库,我们在其中展示了开箱即用的各种功能。
通过利用 Ktor 生成器创建项目,您可以立即开始使用 Ktor。
本博文英文原作者:
