Skip to content
Server Plugin

路由

路由是用于处理服务器应用程序中传入请求的核心 Ktor 插件。当客户端向特定 URL(例如 /hello)发起请求时,路由机制允许我们定义如何为该请求提供服务。

安装路由

可以通过以下方式安装路由插件:

Kotlin
import io.ktor.server.routing.*

install(RoutingRoot) {
    // ...
}

鉴于路由插件在任何应用程序中都非常常用,Ktor 提供了一个便捷的 routing 函数,使安装路由更加简单。在下面的代码片段中,install(RoutingRoot) 被替换为 routing 函数:

kotlin
import io.ktor.server.routing.*

routing {
    // ...
}

定义路由处理程序

安装路由插件后,你可以在 routing 内部调用 route 函数来定义路由:

kotlin
import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.response.*

routing {
    route("/hello", HttpMethod.Get) {
        handle {
            call.respondText("Hello")
        }
    }
}

Ktor 还提供了一系列函数,使定义路由处理程序变得更加容易和简洁。例如,你可以使用 get 函数替换前面的代码,现在它只需要接收 URL 和处理请求的代码:

kotlin
import io.ktor.server.routing.*
import io.ktor.server.response.*

routing {
    get("/hello") {
        call.respondText("Hello")
    }
}

类似地,Ktor 为所有其他动词提供了函数,即 putposthead 等。

总之,你需要指定以下设置来定义路由:

  • HTTP 动词

    选择 HTTP 动词,例如 GETPOSTPUT 等。最方便的方法是使用专用的动词函数,如 getpostput 等。

  • 路径模式

    指定用于匹配 URL 路径的路径模式,例如 /hello/customer/{id}。你可以将路径模式直接传递给 get/post 等函数,也可以使用 route 函数来对路由处理程序进行分组并定义嵌套路由

  • 处理程序

    指定如何处理请求响应。在处理程序内部,你可以访问 ApplicationCall、处理客户端请求并发送响应。

指定路径模式

传递给路由函数(routegetpost 等)的路径模式用于匹配 URL 的 path(路径)部分。路径可以包含一系列由斜杠 / 字符分隔的路径段。

请注意,Ktor 会区分带尾随斜杠和不带尾随斜杠的路径。你可以通过安装 IgnoreTrailingSlash 插件来更改此行为。

以下是几个路径示例:

  • /hello
    包含单个路径段的路径。
  • /order/shipment
    包含多个路径段的路径。你可以按原样将此类路径传递给 route/get/等函数,或者通过嵌套多个 route 函数来组织子路由。
  • /user/{login}
    带有 login 路径参数的路径,其值可以在路由处理程序内部访问。
  • /user/*
    带有通配符的路径,可匹配任何路径段。
  • /user/{...}
    带有尾随通配符 (tailcard)的路径,可匹配 URL 路径的所有其余部分。
  • /user/{param...}
    包含带尾随通配符的路径参数的路径。
  • Regex("/.+/hello")
    包含正则表达式的路径,用于匹配路径段,直到并包括最后一次出现的 /hello

通配符

通配符 (*) 匹配任何路径段且不能为空。例如,/user/* 匹配 /user/john,但不匹配 /user

尾随通配符

尾随通配符 (tailcard) ({...}) 匹配 URL 路径的所有其余部分,可以包含多个路径段,也可以为空。例如,/user/{...} 既可以匹配 /user/john/settings,也可以匹配 /user

路径参数

路径参数 ({param}) 匹配一个路径段,并将其捕获为名为 param 的参数。此路径段是强制性的,但你可以通过添加问号将其变为可选参数:{param?}。例如:

  • /user/{login} 匹配 /user/john,但不匹配 /user
  • /user/{login?} 既匹配 /user/john,也匹配 /user

    请注意,可选路径参数 {param?} 只能在路径末尾使用。

要在路由处理程序内访问参数值,请使用 call.parameters 属性。例如,在下面的代码片段中,对于 /user/admin 路径,call.parameters["login"] 将返回 admin

kotlin
get("/user/{login}") {
    if (call.parameters["login"] == "admin") {
        // ...
    }
}

如果请求包含查询字符串,call.parameters 也会包含该查询字符串的参数。要了解如何在处理程序内部访问查询字符串及其参数,请参阅查询参数

带尾随通配符的路径参数

带尾随通配符的路径参数 ({param...}) 匹配 URL 路径的所有其余部分,并使用 param 作为键,将每个路径段的多个值放入参数中。例如,/user/{param...} 匹配 /user/john/settings。 要在路由处理程序内部访问路径段的值,请使用 call.parameters.getAll("param")。对于上述示例,getAll 函数将返回一个包含 johnsettings 值的数组。

正则表达式

正则表达式可以与所有定义路由处理程序的函数配合使用:routegetpost 等。

要了解有关正则表达式的更多信息,请参阅 Kotlin 文档

让我们编写一个匹配以 /hello 结尾的任何路径的路由。

kotlin
import io.ktor.server.routing.*
import io.ktor.server.response.*

routing {
    get(Regex(".+/hello")) {
        call.respondText("Hello")
    }
}

通过此路由定义,任何指向以 /hello 结尾的路径的传入请求(例如 /foo/hello/bar/baz/hello 等)都将被匹配。

在处理程序中访问路径部分

在正则表达式中,命名组是一种捕获字符串中匹配模式的特定部分并为其分配名称的方法。 语法 (?<name>pattern) 用于定义命名组,其中 name 是组名,而 pattern 是匹配该组的正则表达式模式。

通过在路由函数中定义命名组,你可以捕获路径的一部分,然后在处理程序函数中,使用 call.parameters 对象访问捕获的参数。

例如,你可以定义一个路由,用于匹配包含整数标识符且后跟 /hello 的路径请求。

kotlin
import io.ktor.server.routing.*
import io.ktor.server.response.*

routing {
    get(Regex("""(?<id>\d+)/hello""")) {
        val id = call.parameters["id"]!!
        call.respondText(id)
    }
}

在下面的代码中,(?<id>\d+) 命名组用于从请求路径中捕获整数标识符 idcall.parameters 属性用于在处理程序函数中访问捕获的 id 参数。

在正则表达式路由处理程序内部无法访问未命名的组,但你可以使用它们来匹配路径。例如,路径 hello/world 将被匹配,而 hello/World 则不会:

kotlin
import io.ktor.server.routing.*
import io.ktor.server.response.*

routing {
    get(Regex("hello/([a-z]+)")) {
        call.respondText("Hello")
    }
}

此外,整个路径段需要被正则表达式消耗。例如,路径模式 get(Regex("[a-z]+")) 将不匹配路径 "hello11",但会匹配路径 hello/1hello 部分,并将 /1 留给下一个路由。

定义多个路由处理程序

按动词函数分组路由

如果你想定义多个路由处理程序(这当然是任何应用程序的情况),只需将它们添加到 routing 函数中即可:

kotlin
routing {
    get("/customer/{id}") {

    }
    post("/customer") {

    }
    get("/order") {

    }
    get("/order/{id}") {
    
    }
}

在这种情况下,每个路由都有自己的函数,并响应特定的端点和 HTTP 动词。

按路径分组路由

另一种方法是按路径对这些路由进行分组,即定义路径,然后使用 route 函数将该路径的动词作为嵌套函数放置:

kotlin
routing {
    route("/customer") {
        get {

        }
        post {

        }
    }
    route("/order") {
        get {

        }
        get("/{id}") {

        }
    }
}

嵌套路由

无论你如何进行分组,Ktor 还允许你将子路由作为 route 函数的参数。这对于定义逻辑上属于其他资源子资源的资源非常有用。 以下示例向我们展示了如何响应针对 /order/shipmentGETPOST 请求:

kotlin
routing {
    route("/order") {
        route("/shipment") {
            get {
                
            }
            post {
                
            }
        }
    }
}

因此,每次调用 route 都会生成一个单独的路径段。

传递给路由函数(routegetpost 等)的路径模式用于匹配 URL 的 path(路径)部分。路径可以包含由斜杠 / 字符分隔的一系列路径段。

路由扩展函数

一种常见的模式是在 Route 类型上使用扩展函数来定义实际路由,这使我们能够轻松访问谓词,并消除在单个路由函数中包含所有路由的混乱。无论你决定如何对路由进行分组,都可以独立应用此模式。因此,第一个示例可以以更整洁的方式表示:

kotlin
routing {
    listOrdersRoute()
    getOrderRoute()
    totalizeOrderRoute()
}

fun Route.listOrdersRoute() {
    get("/order") {

    }
}

fun Route.getOrderRoute() {
    get("/order/{id}") {
        
    }
}

fun Route.totalizeOrderRoute() {
    get("/order/{id}/total") {
        
    }
}

有关演示此方法的完整示例,请参阅 legacy-interactive-website

为了使我们的应用程序在可维护性方面能够扩展,建议遵循某些结构化模式

跟踪路由

配置日志记录后,Ktor 会启用路由跟踪,帮助你确定某些路由未被执行的原因。 例如,如果你运行应用程序并向指定端点发出请求,应用程序的输出可能如下所示:

Console
TRACE Application - Trace for [missing-page]
/, segment:0 -> SUCCESS @ /
  /, segment:0 -> SUCCESS @ /
    / [(method:GET)], segment:0 -> FAILURE "Not all segments matched" @ / [(method:GET)]
Matched routes:
  No results
Route resolve result:
  FAILURE "No matched subtrees found" @ /

要在 Native 服务器上启用路由跟踪,请在运行应用程序时向 KTOR_LOG_LEVEL 环境变量传递 TRACE 值。