Ktor:用 Kotlin 构建服务端应用

Ktor 是 JetBrains 面向 Kotlin 的 Web 与服务端框架。它适合写 REST API、WebSocket、轻量后台服务、网关、内部工具,也可以配合模板或静态资源生成网页。Kotlin 官方服务端概览把 Ktor 描述为使用协程、具备惯用 Kotlin API 的框架;这意味着它不是把 Java Servlet 风格直接换成 Kotlin 语法,而是把路由、请求处理、插件安装、测试等环节都设计成 Kotlin DSL。

如果你来自 Java 后端,最直接的类比是:

  • Spring MVC 更偏“注解 + 容器扫描 + 自动装配”。
  • Ktor 更偏“代码式配置 + 路由 DSL + 显式安装插件”。
  • Servlet API 的请求响应对象通常是框架回调参数,Ktor 中常见入口是 ApplicationCall,通过 call.requestcall.respond 等 API 读写请求和响应。
  • Java Web 项目经常先理解容器、Filter、Interceptor、Controller;Ktor 项目通常先理解 Application、路由、插件、引擎和配置文件。

什么时候选择 Ktor

Ktor 适合这些场景:

  • 团队希望服务端代码保持 Kotlin-first,而不是在 Java 框架上写 Kotlin。
  • 服务较轻量,路由和中间件行为希望清楚写在代码里。
  • 需要协程风格的异步 I/O,而不是回调链或复杂响应式类型。
  • 想在 Kotlin 多平台生态中复用模型、序列化和客户端代码。
  • 做内部 API、网关、WebSocket 服务、任务平台、CLI 附带的小型 HTTP 服务。

不一定优先选择 Ktor 的情况:

  • 团队已有成熟 Spring Boot 基础设施、Starter、监控、权限、网关、数据访问规范。
  • 需要大量企业级自动配置,且组织内已经围绕 Spring 形成标准。
  • 新人主要是 Java/Spring 背景,短期目标是降低学习成本。

这不是能力高低的问题,而是工程约束不同。Ktor 的优势在于简单、显式、Kotlin 原生;Spring 的优势在于生态、约定和企业集成。

创建项目

官方推荐的入口包括:

  • Ktor 在线项目生成器:start.ktor.io
  • IntelliJ IDEA Ultimate 的 Ktor 插件。
  • Ktor CLI。

项目生成器通常会让你选择:

  • 构建系统:Gradle Kotlin、Gradle Groovy、Maven 或 Amper。
  • Engine:运行服务器的引擎。
  • Configuration:用 YAML、HOCON 或代码配置服务端参数。官方教程中特别说明,Maven 项目当前不支持 YAML 配置。
  • Plugins:认证、序列化、内容编码、压缩、Cookie 等能力都以插件形式加入。

Java 对比:

Spring Initializr: 选择 Starter -> 生成 Spring Boot 项目 -> 注解驱动
Ktor Generator: 选择 Engine/配置/插件 -> 生成 Ktor 项目 -> DSL 驱动

如果你的团队已经熟悉 Gradle Kotlin DSL,优先选 Gradle Kotlin。这样构建脚本、应用代码和示例文档都使用 Kotlin 语法,心智成本最低。

典型项目结构

官方 Ktor 生成项目的核心代码通常在 src/main/kotlin 下,常见文件包括:

  • Application.kt:应用入口或模块配置。
  • Routing.kt:路由配置。
  • src/main/resources:配置文件、静态资源、模板等。
  • settings.gradle.kts:Gradle 项目名。

一种典型写法是把路由拆成扩展函数:

package com.example

import io.ktor.server.application.Application
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello, Kotlin")
        }

        get("/health") {
            call.respondText("OK")
        }
    }
}

这里的 fun Application.configureRouting() 是扩展函数。它不是继承 Application,而是给 Application 类型增加一个可调用函数。

Java 对比:

@RestController
class HealthController {
    @GetMapping("/health")
    String health() {
        return "OK";
    }
}

Spring MVC 用注解把方法注册成路由。Ktor 用 DSL 在 routing {} 块中显式声明路由。前者依赖组件扫描和注解元数据,后者依赖函数调用和代码组织。

运行项目

官方教程中的基本流程是:

./gradlew build
./gradlew run

应用启动后,可以访问终端输出中的地址。新项目默认常见地址是:

http://0.0.0.0:8080

如果你在本机浏览器访问,通常也可以用:

http://localhost:8080

如果在 macOS 或 Linux 上下载的新项目无法执行 gradlew,先给脚本执行权限:

chmod +x ./gradlew

配置方式

Ktor 支持把服务端配置放在外部文件,也支持写在代码里。

外部配置适合:

  • 端口、host、部署环境等运行参数。
  • 开发、测试、生产环境差异。
  • 容器或云平台通过环境变量覆盖配置。

代码配置适合:

  • 小型服务。
  • 示例和测试。
  • 配置和业务模块强相关,拆到配置文件反而降低可读性。

代码式启动示例:

package com.example

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

fun main() {
    embeddedServer(
        factory = Netty,
        host = "0.0.0.0",
        port = 8080,
        module = Application::module,
    ).start(wait = true)
}

fun Application.module() {
    configureRouting()
}

Java 对比:在 Spring Boot 中,你通常很少手写底层服务器启动逻辑,而是调用 SpringApplication.run(...),端口放在 application.propertiesapplication.yml。Ktor 也支持外部配置,但它允许你更直接地看到服务器是如何被创建和启动的。

路由与请求处理

Ktor 路由是嵌套 DSL。你可以按资源组织:

fun Application.configureUserRoutes() {
    routing {
        route("/users") {
            get {
                call.respondText("List users")
            }

            get("/{id}") {
                val id = call.parameters["id"] ?: return@get call.respondText("Missing id")
                call.respondText("User: $id")
            }
        }
    }
}

注意 return@get。它表示从当前 get {} lambda 返回,而不是从外层函数返回。这是 Kotlin 标签返回的典型用法。

Java 对比:

@GetMapping("/users/{id}")
String findUser(@PathVariable String id) {
    return "User: " + id;
}

Spring MVC 把路径参数绑定到方法参数。Ktor 里你通常从 call.parameters 或请求对象中读取。Ktor 写起来更显式,Spring 写起来更声明式。

响应 JSON

在 Ktor 中,JSON 通常通过内容协商插件处理。使用 kotlinx.serialization 时,模型可以写成:

import kotlinx.serialization.Serializable

@Serializable
data class UserResponse(
    val id: Long,
    val name: String,
)

然后在应用模块中安装 JSON 支持:

import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json()
    }
}

路由中直接响应对象:

import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

fun Application.configureUserApi() {
    routing {
        get("/api/users/1") {
            call.respond(UserResponse(id = 1, name = "Ada"))
        }
    }
}

Java 对比:

  • Spring Boot 常用 Jackson,返回对象后由 HttpMessageConverter 序列化。
  • Ktor 也可以使用 Jackson,但 Kotlin-first 项目常搭配 kotlinx.serialization
  • Kotlin 数据类很适合 DTO,但对外 API 的数据类要注意兼容性,公开库尤其要谨慎,见本仓库的库作者 API 指南。

插件模型

Ktor 的很多功能都通过插件安装:

  • ContentNegotiation:JSON、XML 等内容协商。
  • StatusPages:异常处理与错误响应。
  • Authentication:认证。
  • Compression:响应压缩。
  • Cookie、Session、CORS、静态资源等。

示例:给非法状态异常返回文本响应。

import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.response.respondText

fun Application.configureErrors() {
    install(StatusPages) {
        exception<IllegalStateException> { call, cause ->
            call.respondText(
                text = "Application state error: ${cause.message}",
                status = HttpStatusCode.Conflict,
            )
        }
    }
}

Java 对比:

  • Spring MVC 常用 @ControllerAdvice@ExceptionHandler
  • Ktor 用 install(StatusPages) 明确把错误处理装进应用。
  • 两者都能集中处理异常;Ktor 的配置点更集中,Spring 的注解模型更分散但更符合大型应用分层习惯。

测试

Ktor 的测试工具可以在不启动真实 Netty 服务器的情况下运行应用模块,并使用内置客户端发请求。

import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication
import kotlin.test.Test
import kotlin.test.assertEquals

class HealthRouteTest {
    @Test
    fun healthEndpointReturnsOk() = testApplication {
        application {
            configureRouting()
        }

        val response = client.get("/health")

        assertEquals(HttpStatusCode.OK, response.status)
    }
}

这类测试类似 Spring 的 MockMvcWebTestClient,但 Ktor 的入口通常是配置 application { ... },让测试环境加载和生产相同的模块函数。

工程分层建议

小项目可以把路由、服务和数据访问写在少量文件中。项目变大后建议这样拆:

src/main/kotlin/com/example
  Application.kt
  routes/
    UserRoutes.kt
    HealthRoutes.kt
  service/
    UserService.kt
  repository/
    UserRepository.kt
  model/
    UserDto.kt
    User.kt
  config/
    Serialization.kt
    ErrorHandling.kt

Ktor 不强制 MVC 分层,优点是灵活,缺点是团队需要自己约定结构。为了长期维护,建议把这几类代码分开:

  • 路由只做 HTTP 参数解析、响应码和 DTO 转换。
  • Service 承载业务规则。
  • Repository 负责持久化。
  • Application 扩展函数负责安装插件和组装模块。

常见误区

误区一:Ktor 没有注解,所以不适合大项目

注解不是大项目的必要条件。Ktor 可以通过模块函数、插件和 Gradle 多模块组织大型项目。但如果团队已经有大量 Spring 基础设施,迁移到 Ktor 的收益要和成本一起评估。

误区二:协程等于自动更快

协程让高并发 I/O 代码更容易表达,但性能仍取决于数据库连接池、外部服务延迟、序列化、阻塞调用和线程调度。不要在 Ktor 路由里直接调用阻塞数据库或阻塞 SDK,而不理解它们运行在哪个 dispatcher 上。

误区三:所有配置都应该写在代码里

代码式配置可读性高,但端口、密钥、数据库地址、外部服务 URL 等环境相关参数应该外部化。否则部署环境变多后会出现大量重复构建或条件判断。

误区四:路由 DSL 可以替代业务分层

routing {} 只是 HTTP 入口,不应该成为业务逻辑堆积处。超过几十行的 route handler 通常应该拆出 service。

与 Spring Boot 的选择对比

维度 Ktor Spring Boot
主要风格 Kotlin DSL、显式插件 注解、自动配置、Starter
生态成熟度 Kotlin 服务端核心生态 JVM 企业生态非常成熟
学习重点 Application、routing、plugins、coroutines DI、AOP、auto configuration、Spring MVC/WebFlux
异步模型 协程优先 Servlet、Reactive、协程支持并存
配置方式 代码、YAML、HOCON properties、YAML、Java/Kotlin config
适合项目 Kotlin-first 服务、轻量 API、WebSocket 企业应用、复杂集成、标准化平台

最小实践清单

开始一个 Ktor 服务时,建议先定下这些约定:

  • Gradle Kotlin DSL。
  • Application.module() 只做组装,不写业务。
  • 每类能力拆成 configureXxx() 扩展函数。
  • JSON DTO 使用 @Serializable,并区分外部 DTO 与内部领域模型。
  • 错误响应统一通过 StatusPages
  • 每个重要路由至少有一个 testApplication 测试。
  • 阻塞调用集中封装,不直接散落在 route handler 中。

官方参考

  • Kotlin Backend development with Kotlin:https://kotlinlang.org/docs/server-overview.html
  • Ktor Create, open, and run a new Ktor project:https://ktor.io/docs/server-create-a-new-project.html