3. 路由与请求处理:路由定义、GET/POST/PUT/DELETE方法、路径参数、查询参数、请求体解析
路由,说白了就是你的 API 大门。用户发来一个请求,Ktor 得知道该让谁去处理。我刚开始用 Ktor 时,觉得路由不就是写几个路径嘛,后来才发现,路由设计的好坏,直接决定了你的后端代码是「清爽小别墅」还是「垃圾堆」。今天我们就来把这套东西捋清楚。
本章核心脉络:从路由定义开始,逐步掌握 HTTP 方法的用法,再到路径参数、查询参数和请求体解析。最终你能写出一个完整的 RESTful API 端点。
3.1 路由定义:你的 API 地图
在 Ktor 中,路由是通过 DSL 风格定义的。我个人非常喜欢这种写法——它就像在画一棵树,清晰又直观。你不需要到处找路由配置,所有端点都写在同一个地方。
fun Application.module() {
install(Routing) {
get("/") {
call.respondText("Hello, Ktor!")
}
get("/api/users") {
// 返回用户列表
}
}
}
嗯,这里要注意:路由的嵌套是有顺序的。Ktor 会从上到下匹配,一旦匹配成功就不会继续往下找了。我在项目中遇到过一个问题——把 /api/users/{id} 写在了 /api/users/new 前面,结果 "new" 被当成了 id 参数。这种坑踩过一次就记住了。
小技巧:把精确路径放在前面,带参数的路径放在后面。这样能避免参数误匹配。
3.2 HTTP 方法:GET / POST / PUT / DELETE
说白了,HTTP 方法就是告诉服务器你想干什么。Ktor 对每个方法都有对应的扩展函数,用起来非常顺手。
| 方法 | 用途 | Ktor 写法 |
|---|---|---|
| GET | 获取资源 | get("/path") { ... } |
| POST | 创建资源 | post("/path") { ... } |
| PUT | 更新资源 | put("/path") { ... } |
| DELETE | 删除资源 | delete("/path") { ... } |
// 一个完整的 CRUD 示例
routing {
get("/api/items") {
// 查询所有
}
post("/api/items") {
// 创建新项
}
put("/api/items/{id}") {
// 更新指定项
}
delete("/api/items/{id}") {
// 删除指定项
}
}
你想想看,这种写法是不是比 Spring 的注解式路由更直观?至少我个人觉得,路由和业务逻辑的边界更清晰了。
3.3 路径参数:从 URL 中提取动态值
路径参数是 RESTful API 的核心。比如 /api/users/42 中的 42,就是路径参数。Ktor 用花括号来定义它。
get("/api/users/{id}") {
val userId = call.parameters["id"]
// userId 是 String? 类型,需要做空安全处理
if (userId != null) {
call.respondText("User ID: $userId")
} else {
call.respondText("Missing user ID", status = HttpStatusCode.BadRequest)
}
}
我曾经在项目里犯过一个低级错误——直接用了 call.parameters["id"]!!。结果有个客户端传了个空值,服务端直接崩了。从那以后,我再也不敢对参数做非空断言了。安全第一,用 ?: 或者 ?.let 来处理才是正道。
注意:路径参数的值永远是 String 类型。如果你需要 Int 或 Long,记得手动转换,并处理转换失败的情况。
3.4 查询参数:URL 问号后面的秘密
查询参数通常用于过滤、分页或排序。比如 /api/users?page=1&size=20。Ktor 获取查询参数的方式和路径参数类似,但用的是 request.queryParameters。
get("/api/users") {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 20
// 用 page 和 size 做分页查询
call.respondText("Page: $page, Size: $size")
}
这里有个细节:查询参数是可选的。你不能假设客户端一定会传。我习惯给每个查询参数都设一个合理的默认值,这样即使客户端忘了传,服务端也不会报错。
建议:对于分页参数,默认 page=1、size=20 是比较通用的做法。如果客户端传了负数或超大值,记得做校验和截断。
3.5 请求体解析:接收客户端发来的数据
POST 和 PUT 请求通常需要携带请求体。Ktor 支持多种格式,最常用的是 JSON。你需要先安装 ContentNegotiation 插件,并配置 JSON 序列化器。
// 安装 ContentNegotiation
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
// 定义数据类
data class User(val name: String, val email: String, val age: Int)
// 接收请求体
post("/api/users") {
val user = call.receive<User>()
// user 已经是解析好的 Kotlin 对象了
println("Received user: ${user.name}")
call.respond(HttpStatusCode.Created, user)
}
你想想看,这比手动解析 JSON 省了多少事?Ktor 的 call.receive<T>() 会自动帮你完成反序列化。不过要注意,如果客户端传的 JSON 格式不对,会抛出异常。我一般会在路由外层加一个 try-catch,或者用状态码处理。
避坑指南:call.receive<T>() 只能调用一次。如果你在同一个路由里调了两次,第二次会报错。我曾经因为这个 bug 排查了半天——说白了,Ktor 的请求体是流式的,读完就没了。
3.6 综合示例:一个完整的用户 API
把上面所有知识点串起来,就是一个完整的 RESTful 端点。我个人觉得,这种写法比 Spring Boot 的注解式路由更「程序员友好」——你一眼就能看到所有路由的层级关系。
routing {
route("/api/users") {
get {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 20
// 分页查询用户列表
call.respond(listOfUsers)
}
get("{id}") {
val userId = call.parameters["id"]?.toIntOrNull()
if (userId == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
return@get
}
// 查询单个用户
call.respond(foundUser)
}
post {
val user = call.receive<User>()
// 创建用户
call.respond(HttpStatusCode.Created, newUser)
}
put("{id}") {
val userId = call.parameters["id"]?.toIntOrNull() ?: return@put
val user = call.receive<User>()
// 更新用户
call.respond(updatedUser)
}
delete("{id}") {
val userId = call.parameters["id"]?.toIntOrNull() ?: return@delete
// 删除用户
call.respond(HttpStatusCode.NoContent)
}
}
}
这个例子涵盖了本章所有知识点。你仔细看,路径参数、查询参数、请求体解析、各种 HTTP 方法,全都在里面了。我在实际项目中就是这么写的,跑了一年多没出过问题。
核心要点回顾:
- 路由用 DSL 定义,支持嵌套和链式调用
- GET/POST/PUT/DELETE 各有对应的扩展函数
- 路径参数用花括号
{id},通过call.parameters获取 - 查询参数用
call.request.queryParameters,记得设默认值 - 请求体用
call.receive<T>(),需要先安装 ContentNegotiation - 所有参数都要做空安全和类型转换校验
好了,路由和请求处理这块就讲完了。说白了,Ktor 的路由系统就是一套优雅的 DSL,你只要记住「路径参数用花括号、查询参数用问号、请求体用 receive」这个口诀,基本就能上手了。下一节我们会聊更进阶的内容,先把这些基础练熟再说。