Kotlin 与 Ktor
Ktor服务端框架
前置知识
- Kotlin 与 Arrow:建议先完成前一篇的学习
学习目标
- 掌握「概述」的核心机制、典型用法与常见陷阱
- 掌握「基础概念」的核心机制、典型用法与常见陷阱
- 掌握「快速上手」的核心机制、典型用法与常见陷阱
- 掌握「详细用法」的核心机制、典型用法与常见陷阱
- 掌握「常见场景」的核心机制、典型用法与常见陷阱
概述
Ktor 是 JetBrains 开发的 Kotlin 服务端框架,基于协程构建,轻量、灵活、非阻塞。与 Spring Boot 等全功能框架不同,Ktor 采用插件化架构,你只引入需要的功能。它的 DSL 风格 API 让路由定义和配置非常直观。
Ktor 适合构建微服务、REST API、WebSocket 服务等。如果你喜欢轻量级框架,想要完全控制每个组件,Ktor 是一个很好的选择。
基础概念
- Application:Ktor 应用的入口,配置路由、插件等
- Routing:定义 URL 路径与处理函数的映射
- Plugin:插件,扩展框架功能,如 ContentNegotiation、Authentication、CORS 等
- Pipeline:请求处理管道,插件在管道的各个阶段拦截请求
- embeddedServer:嵌入式服务器,不需要外部容器,直接在代码中启动
快速上手
添加依赖:
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-server-core:2.3.7")
implementation("io.ktor:ktor-server-netty:2.3.7") // Netty 引擎
implementation("io.ktor:ktor-server-content-negotiation:2.3.7")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
implementation("ch.qos.logback:logback-classic:1.4.14")
}
最简单的 Ktor 服务器:
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
// 定义路由
get("/") {
call.respondText("Hello, Ktor!")
}
get("/hello/{name}") {
val name = call.parameters["name"] ?: "World"
call.respondText("Hello, $name!")
}
}
}.start(wait = true)
}
运行后访问 http://localhost:8080 和 http://localhost:8080/hello/Alice。
详细用法
路由定义
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.request.*
fun Application.configureRouting() {
routing {
// GET 请求
get("/users") {
call.respondText("用户列表")
}
// 带路径参数
get("/users/{id}") {
val id = call.parameters["id"]
call.respondText("用户ID: $id")
}
// 带查询参数
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val page = call.request.queryParameters["page"]?.toInt() ?: 1
call.respondText("搜索: $query, 第${page}页")
}
// POST 请求
post("/users") {
val body = call.receiveText()
call.respondText("创建用户: $body")
}
// PUT 请求
put("/users/{id}") {
val id = call.parameters["id"]
val body = call.receiveText()
call.respondText("更新用户 $id: $body")
}
// DELETE 请求
delete("/users/{id}") {
val id = call.parameters["id"]
call.respondText("删除用户: $id")
}
// 路由分组
route("/api") {
get("/v1/status") { call.respondText("OK") }
route("/v1/users") {
get { call.respondText("用户列表") }
post { call.respondText("创建用户") }
}
}
}
}
JSON 响应
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class User(val id: Int, val name: String, val email: String)
fun Application.configureSerialization() {
// 安装 ContentNegotiation 插件
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
})
}
routing {
// 返回 JSON
get("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: 0
call.respond(User(id, "Alice", "alice@example.com"))
}
// 接收 JSON 请求体
post("/users") {
val user = call.receive<User>()
println("收到: $user")
call.respond(mapOf("status" to "created", "user" to user))
}
// 返回列表
get("/users") {
val users = listOf(
User(1, "Alice", "alice@example.com"),
User(2, "Bob", "bob@example.com")
)
call.respond(users)
}
}
}
状态码和响应头
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureResponses() {
routing {
get("/notfound") {
call.respondText("资源不存在", status = HttpStatusCode.NotFound)
}
get("/created") {
call.respondText("创建成功", status = HttpStatusCode.Created)
}
get("/headers") {
call.response.headers.append("X-Custom-Header", "Hello")
call.respondText("查看响应头")
}
// 重定向
get("/old") {
call.respondRedirect("/new")
}
get("/new") {
call.respondText("新地址")
}
}
}
静态文件
import io.ktor.server.http.content.*
import io.ktor.server.routing.*
fun Application.configureStatic() {
routing {
// 静态文件服务
static("/static") {
resources("static") // 从 classpath 的 static 目录
}
// 或者从文件系统
staticFiles("/files", java.io.File("uploads"))
}
}
常见场景
RESTful API
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
@Serializable
data class CreateItemRequest(val name: String, val price: Double)
@Serializable
data class Item(val id: Int, val name: String, val price: Double)
// 模拟数据库
val items = mutableListOf<Item>()
var nextId = 1
fun Application.configureApi() {
routing {
route("/api/items") {
// 获取所有
get {
call.respond(items)
}
// 获取单个
get("/{id}") {
val id = call.parameters["id"]?.toInt()
val item = items.find { it.id == id }
if (item != null) {
call.respond(item)
} else {
call.respondText("未找到", status = HttpStatusCode.NotFound)
}
}
// 创建
post {
val request = call.receive<CreateItemRequest>()
val item = Item(nextId++, request.name, request.price)
items.add(item)
call.respond(item)
}
// 更新
put("/{id}") {
val id = call.parameters["id"]?.toInt()
val request = call.receive<CreateItemRequest>()
val index = items.indexOfFirst { it.id == id }
if (index >= 0) {
items[index] = Item(id!!, request.name, request.price)
call.respond(items[index])
} else {
call.respondText("未找到", status = HttpStatusCode.NotFound)
}
}
// 删除
delete("/{id}") {
val id = call.parameters["id"]?.toInt()
val removed = items.removeIf { it.id == id }
if (removed) {
call.respondText("已删除")
} else {
call.respondText("未找到", status = HttpStatusCode.NotFound)
}
}
}
}
}
CORS 配置
import io.ktor.server.plugins.cors.routing.*
import io.ktor.http.*
fun Application.configureCORS() {
install(CORS) {
anyHost() // 开发环境允许所有来源
// 生产环境应指定具体来源
// allowHost("example.com")
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHeader(HttpHeaders.ContentType)
allowCredentials = true
}
}
请求日志
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.application.*
fun Application.configureLogging() {
install(CallLogging) {
level = org.slf4j.event.Level.INFO
// 只记录 API 请求
filter { call -> call.request.path().startsWith("/api") }
// 记录请求耗时
format { call ->
val status = call.response.status()
val method = call.request.httpMethod.value
val path = call.request.path()
"[$method] $path -> $status"
}
}
}
注意事项
- 所有处理函数都是挂起函数:路由处理函数在协程中执行,可以调用 suspend 函数
- 安装插件的顺序:某些插件的安装顺序会影响行为,如 CORS 应在路由之前安装
- 不要阻塞线程:在路由处理中不要调用阻塞 IO,使用协程或
Dispatchers.IO - 异常处理:未捕获的异常会返回 500 错误,建议安装 StatusPages 插件统一处理
- 引擎选择:Netty 性能最好,CIO 是纯 Kotlin 实现,Jetty 支持 Servlet
进阶用法
认证
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
fun Application.configureAuth() {
val secret = "my-secret-key"
val algorithm = Algorithm.HMAC256(secret)
install(Authentication) {
jwt("auth-jwt") {
verifier(JWT.require(algorithm).build())
validate { credential ->
if (credential.payload.getClaim("userId").asString().isNotEmpty()) {
JWTPrincipal(credential.payload)
} else null
}
}
}
routing {
// 不需要认证
post("/login") {
// 验证用户名密码后生成 Token
val token = JWT.create()
.withClaim("userId", "1")
.sign(algorithm)
call.respond(mapOf("token" to token))
}
// 需要认证
authenticate("auth-jwt") {
get("/protected") {
val principal = call.principal<JWTPrincipal>()
val userId = principal!!.payload.getClaim("userId").asString()
call.respondText("欢迎, 用户$userId")
}
}
}
}
状态页面(错误处理)
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.http.*
fun Application.configureStatusPages() {
install(StatusPages) {
// 处理特定状态码
status(HttpStatusCode.NotFound) { call, status ->
call.respondText("页面不存在", status = status)
}
// 处理异常
exception<IllegalArgumentException> { call, cause ->
call.respondText("参数错误: ${cause.message}", status = HttpStatusCode.BadRequest)
}
exception<NotFoundException> { call, _ ->
call.respondText("资源不存在", status = HttpStatusCode.NotFound)
}
// 兜底异常处理
exception<Exception> { call, cause ->
call.respondText("服务器错误: ${cause.message}", status = HttpStatusCode.InternalServerError)
}
}
}
应用配置文件
// application.conf (HOCON 格式)
// 放在 resources 目录下
/*
ktor {
deployment {
port = 8080
host = 0.0.0.0
}
application {
modules = [ com.example.ApplicationKt.module ]
}
}
*/
// 在代码中读取配置
fun Application.module() {
val port = environment.config.property("ktor.deployment.port").getString().toInt()
val host = environment.config.property("ktor.deployment.host").getString()
println("服务启动在 $host:$port")
}
创建服务器
基本写法:embeddedServer 启动
embeddedServer(Netty, port = <端口>) { }.start(wait = true)
// 启动 Ktor Netty 服务器
embeddedServer(Netty, port = 8080) {
routing { get("/") { call.respondText("hello") } }
}.start(wait = true)
基本写法:指定 host
embeddedServer(Netty, port = <端口>, host = "<主机>") { }
// 指定监听主机
embeddedServer(Netty, port = 8080, host = "0.0.0.0") { }
路由配置
基本写法:定义 GET 路由
get("<路径>") { }
// 注册 GET 请求处理
get("/users") { call.respond(users) }
基本写法:POST 路由
post("<路径>") { }
// 注册 POST 请求处理
post("/users") { call.respond(create()) }
基本写法:路径参数
get("<路径>/{<参数>}") { }
// 获取路径参数
get("/users/{id}") {
val id = call.parameters["id"]
}
基本写法:route 分组
route("<前缀>") { }
// 路由分组
routing {
route("/api") {
get("/v1") { }
post("/v2") { }
}
}
请求处理
基本写法:接收 JSON
call.receive<<类型>>()
// 反序列化请求体
val user = call.receive<User>()
基本写法:响应 JSON
call.respond(<对象>)
// 序列化对象为 JSON 响应
call.respond(User("Alice"))
基本写法:响应纯文本
call.respondText("<文本>")
// 返回纯文本响应
call.respondText("hello", ContentType.Text.Plain)
基本写法:查询参数
call.request.queryParameters["<名称>"]
// 获取查询字符串参数
val q = call.request.queryParameters["q"]
ContentNegotiation 插件
基本写法:安装 JSON 插件
install(ContentNegotiation) { json() }
// 启用 JSON 序列化
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
StatusPages 异常处理
基本写法:异常映射
install(StatusPages) { exception<异常> { } }
// 异常转换为 HTTP 状态码
install(StatusPages) {
exception<NotFoundException> { call, _ ->
call.respond(HttpStatusCode.NotFound)
}
}
Authentication 认证
基本写法:Basic 认证
install(Authentication) { basic { } }
// 启用 Basic 认证
install(Authentication) {
basic("auth") {
realm = "api"
validate { cred -> if (check(cred)) UserIdPrincipal(cred.name) else null }
}
}
基本写法:路由应用认证
authenticate("<名称>") { }
// 路由级应用认证
authenticate("auth") {
get("/me") { call.respond(user) }
}
基本写法:JWT 认证
install(Authentication) { jwt { } }
// JWT 认证配置
install(Authentication) {
jwt("jwt") {
verifier(jwk)
realm = "api"
validate { cred -> UserIdPrincipal(cred.payload.subject) }
}
}
Sessions 会话
基本写法:启用会话
install(Sessions) { cookie<<类型>>("<名称>") }
// Cookie 会话
install(Sessions) {
cookie<UserSession>("SESSION")
}
基本写法:设置会话
call.sessions.set(<实例>)
// 写入会话数据
call.sessions.set(UserSession(id = "1"))
基本写法:获取会话
call.sessions.get<<类型>>()
// 读取会话数据
val s = call.sessions.get<UserSession>()
静态资源
基本写法:静态文件
staticFiles("<路径>", <文件对象>)
// 提供静态文件服务
staticFiles("/static", File("public"))
基本写法:静态默认资源
defaultResource("<文件>")
// 从资源目录提供静态文件
staticResources("/static") {
defaultResource("index.html")
}
WebSockets
基本写法:启用 WebSocket
install(WebSockets)
// 安装 WebSocket 插件
install(WebSockets)
基本写法:定义 WebSocket 路由
webSocket("<路径>") { }
// WebSocket 端点
webSocket("/chat") {
for (frame in incoming) {
val text = frame as Frame.Text
send(Frame.Text(text.readText()))
}
}
基本写法:发送消息
send(Frame.Text("<消息>"))
// 发送文本帧
send(Frame.Text("hello"))
基本写法:接收消息
incoming.receive() as Frame.Text
// 接收文本帧
val text = (incoming.receive() as Frame.Text).readText()
CORS 跨域
基本写法:启用 CORS
install(CORS) { anyHost() }
// 配置跨域
install(CORS) {
anyHost()
allowHeader(HttpHeaders.ContentType)
}
部署命令
基本写法:构建 FatJar
./gradlew buildFatJar
# 构建包含所有依赖的 FatJar
./gradlew :app:buildFatJar
基本写法:运行应用
java -jar <jar>
# 运行打包后的应用
java -jar build/libs/app-all.jar
基本写法:Docker 运行
docker build -t <名称> .
# 构建 Docker 镜像
docker build -t ktor-app .
docker run -p 8080:8080 ktor-app