3. 路由与请求处理:路由定义、GET/POST/PUT/DELETE方法、路径参数、查询参数、请求体解析

路由,说白了就是你的 API 大门。用户发来一个请求,Ktor 得知道该让谁去处理。我刚开始用 Ktor 时,觉得路由不就是写几个路径嘛,后来才发现,路由设计的好坏,直接决定了你的后端代码是「清爽小别墅」还是「垃圾堆」。今天我们就来把这套东西捋清楚。

本章核心脉络:从路由定义开始,逐步掌握 HTTP 方法的用法,再到路径参数、查询参数和请求体解析。最终你能写出一个完整的 RESTful API 端点。

路由与请求处理知识体系 路由定义 HTTP 方法 路径参数 查询参数 请求体解析 GET / POST PUT / DELETE {id} / {slug} ?page=1&size=20 JSON / Form 最终目标:构建完整的 RESTful API 端点 Ktor 路由 DSL 风格 · 类型安全 · 链式调用

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」这个口诀,基本就能上手了。下一节我们会聊更进阶的内容,先把这些基础练熟再说。