Skip to content

使用 Ktor 和 Kotlin 处理 HTTP 请求并生成响应

代码示例: tutorial-server-routing-and-requests

使用的插件:

Routing
Routing 是一个核心插件,用于处理服务器应用程序中的传入请求。

在本教程中,你将通过构建任务管理器应用程序,学习如何使用 Ktor 在 Kotlin 中进行路由、处理请求和参数的基础知识。

完成本教程后,你将了解如何执行以下操作:

  1. 处理 GET 和 POST 请求。
  2. 从请求中提取信息。
  3. 转换数据时处理错误。
  4. 使用单元测试来验证路由。

先决条件

这是 Ktor 服务器入门指南的第二个教程。你可以独立完成本教程,但我们强烈建议你先完成前一个教程,学习如何

创建、打开和运行新的 Ktor 项目
学习如何使用 Ktor 打开、运行和测试服务器应用程序。

了解 HTTP 请求类型、标头和状态码的基本知识也很有用。

我们推荐安装 IntelliJ IDEA,但你也可以使用其他你选择的 IDE。

任务管理器应用程序

在本教程中,你将逐步构建一个具有以下功能的任务管理器应用程序:

  1. 以 HTML 表格形式查看所有可用的任务。
  2. 再次以 HTML 形式,按优先级和名称查看任务。
  3. 通过提交 HTML 表单添加其他任务。

你将尽可能地实现一些基本功能,然后通过七次迭代改进和扩展此功能。这项基本功能将由一个包含某些模型类型、值列表和一个路由的项目组成。

显示静态 HTML 内容

在第一次迭代中,你将为应用程序添加一个新路由,它将返回静态 HTML 内容。

使用 Ktor 项目生成器,创建一个名为 ktor-task-app 的新项目。你可以接受所有默认选项,但可能希望更改 artifact 名称。

TIP

关于创建新项目的更多信息,请参见
创建、打开和运行新的 Ktor 项目
学习如何使用 Ktor 打开、运行和测试服务器应用程序。
。如果你最近完成了该教程,可以随意重用在那里创建的项目。
  1. 打开 Routing.kt 文件,该文件位于 src/main/kotlin/com/example/plugins 文件夹中。
  2. 将现有的 Application.configureRouting() 函数替换为以下实现:

    kotlin

    这样,你已为 URL /tasks 和 GET 请求类型创建了一个新路由。GET 请求是 HTTP 中最基本的请求类型。当用户在浏览器的地址栏中键入或点击常规 HTML 链接时会触发它。

    目前你只返回静态内容。要通知客户端你将发送 HTML,你需要将 HTTP Content Type 标头设置为 "text/html"

  3. 添加以下导入以访问 ContentType 对象:

    kotlin
  4. 在 IntelliJ IDEA 中,点击 Application.ktmain() 函数旁边的运行边槽图标 (intelliJ IDEA run application icon),以启动应用程序。

  5. 在浏览器中导航至 http://0.0.0.0:8080/tasks。你应该会看到待办列表显示:

    A browser window displaying a to-do list with two items

实现任务模型

现在你已经创建了项目并设置了基本路由,接下来你将通过以下操作扩展你的应用程序:

  1. 创建模型类型来表示任务。
  2. 声明包含示例值的任务列表。
  3. 修改路由和请求处理程序以返回此列表。
  4. 使用浏览器测试新特性是否正常工作。
  1. src/main/kotlin/com/example 内部,创建一个名为 model 的新子包。

  2. model 目录中,创建一个新文件 Task.kt

  3. 打开 Task.kt 文件,添加以下 enum 来表示优先级,并添加一个 class 来表示任务:

    kotlin
  4. 你将把任务信息发送到客户端的 HTML 表格中,因此也请添加以下扩展函数:

    kotlin

    Task.taskAsRow() 函数使 Task 对象能够渲染为表格行,而 List<Task>.tasksAsTable() 允许将任务列表渲染为表格。

  1. 在你的 model 目录中,创建一个新文件 TaskRepository.kt

  2. 打开 TaskRepository.kt 并添加以下代码来定义一个任务列表:

    kotlin
  1. 打开 Routing.kt 文件,并将现有 Application.configureRouting() 函数替换为以下实现:

    kotlin

    现在你不再向客户端返回静态内容,而是提供任务列表。由于列表无法直接通过网络发送,因此必须将其转换为客户端能理解的格式。在此例中,任务被转换为 HTML 表格。

  2. 添加所需的导入:

    kotlin
  1. 在 IntelliJ IDEA 中,点击重新运行按钮 (intelliJ IDEA rerun button icon) 以重新启动应用程序。

  2. 在浏览器中导航至 http://0.0.0.0:8080/tasks。它应该显示一个包含任务的 HTML 表格:

    A browser window displaying a table with four rows

    如果是这样,恭喜你!应用程序的基本功能已正常工作。

重构模型

在继续扩展应用程序的功能之前,你需要通过将值列表封装在版本库中来重构设计。这将使你能够集中管理数据,从而专注于 Ktor 特有的代码。

  1. 返回 TaskRepository.kt 文件,并将现有任务列表替换为以下代码:

    kotlin

    这实现了一个非常简单的基于列表的任务数据存储。为了示例目的,任务的添加顺序将被保留,但通过抛出异常来禁止重复项。

    在后续教程中,你将学习如何通过 Exposed 库实现连接到关系型数据库的版本库。

    目前,你将在路由中使用此版本库。

  2. 打开 Routing.kt 文件,并将现有 Application.configureRouting() 函数替换为以下实现:

    Kotlin

    当请求到来时,版本库用于获取当前的任务列表。然后,构建包含这些任务的 HTTP 响应。

  1. 在 IntelliJ IDEA 中,点击重新运行按钮 (intelliJ IDEA rerun button icon) 以重新启动应用程序。

  2. 在浏览器中导航至 http://0.0.0.0:8080/tasks。输出应保持不变,显示 HTML 表格:

    A browser window displaying a table with four rows

处理参数

在此次迭代中,你将允许用户按优先级查看任务。为此,你的应用程序必须允许对以下 URL 发送 GET 请求:

  1. /tasks/byPriority/Low
  2. /tasks/byPriority/Medium
  3. /tasks/byPriority/High
  4. /tasks/byPriority/Vital

你将添加的路由是 /tasks/byPriority/{priority?} ,其中 {priority?} 表示你需要在运行时提取的路径参数,问号用于表示参数是可选的。查询参数可以是你喜欢的任何名称,但 priority 似乎是显而易见的选择。

处理请求的过程可总结如下:

  1. 从请求中提取一个名为 priority 的路径参数。
  2. 如果此参数缺失,则返回 400 状态(Bad Request)。
  3. 将参数的文本值转换为 Priority 枚举值。
  4. 如果失败,则返回状态码为 400 的响应。
  5. 使用版本库查找所有具有指定优先级的任务。
  6. 如果没有匹配的任务,则返回 404 状态(Not Found)。
  7. 返回匹配的任务,格式化为 HTML 表格。

你将首先实现此功能,然后找到检测其是否正常工作的最佳方式。

    打开 Routing.kt 文件,并将以下路由添加到你的代码中,如下所示:

    kotlin

    如上所述,你已为 URL /tasks/byPriority/{priority?} 编写了一个处理程序。符号 priority 代表用户添加的路径参数。不幸的是,在服务器上无法保证这对应于 Kotlin 枚举中的四个值之一,因此必须手动检测。

    如果路径参数缺失,服务器将向客户端返回 400 状态码。否则,它会提取参数的值并尝试将其转换为枚举的成员。如果此操作失败,则会抛出异常,服务器会捕获该异常并返回 400 状态码。

    假设转换成功,版本库将用于查找匹配的 Tasks。如果没有指定优先级的任务,服务器会返回 404 状态码,否则会以 HTML 表格的形式发送匹配项。

    你可以通过在浏览器中请求不同的 URL 来测试此功能。

  1. 在 IntelliJ IDEA 中,点击重新运行按钮 (intelliJ IDEA rerun button icon) 以重新启动应用程序。

  2. 要检索所有中等优先级任务,请导航至 http://0.0.0.0:8080/tasks/byPriority/Medium

    A browser window displaying a table with Medium priority tasks
  3. 不幸的是,在出现错误的情况下,你通过浏览器进行的测试是有限的。除非你使用开发者扩展,否则浏览器不会显示不成功响应的详细信息。一个更简单的替代方案是使用专业工具,例如 Postman

  4. 在 Postman 中,发送针对相同 URL http://0.0.0.0:8080/tasks/byPriority/Medium 的 GET 请求。

    A GET request in Postman showing the response details

    这显示了服务器的原始输出,以及请求和响应的所有详细信息。

  5. 要检测对紧急任务的请求是否返回 404 状态码,请向 http://0.0.0.0:8080/tasks/byPriority/Vital 发送新的 GET 请求。然后你将在 Response 面板的右上角看到显示的状态码。

    A GET request in Postman showing the status code
  6. 要验证当指定无效优先级时是否返回 400,请创建另一个包含无效属性的 GET 请求:

    A GET request in Postman with a Bad Request status code

添加单元测试

到目前为止,你已经添加了两个路由——一个用于检索所有任务,另一个用于按优先级检索任务。像 Postman 这样的工具使你能够完全测试这些路由,但它们需要手动探查并在 Ktor 外部运行。

这在原型设计和小型应用程序中是可以接受的。然而,这种方法不适用于大型应用程序,其中可能需要频繁运行数千个测试。一个更好的解决方案是完全自动化你的测试。

Ktor 提供其自己的

测试框架
学习如何使用特殊的测试引擎测试你的服务器应用程序。
来支持路由的自动化验证。接下来,你将为你应用程序的现有功能编写一些测试。

  1. src 中创建一个名为 test 的新目录,并创建一个名为 kotlin 的子目录。

  2. src/test/kotlin 内部,创建一个新文件 ApplicationTest.kt

  3. 打开 ApplicationTest.kt 文件并添加以下代码:

    kotlin

    在每个测试中都创建了一个新的 Ktor 实例。这在测试环境中运行,而不是像 Netty 这样的 Web 服务器中运行。项目生成器为你编写的模块会被加载,这反过来会调用路由函数。然后,你可以使用内置的 client 对象向应用程序发送请求,并验证返回的响应。

    测试可以在 IDE 内部运行,也可以作为 CI/CD 流水线的一部分运行。

  4. 要在 IntelliJ IDE 中运行测试,请点击每个测试函数旁边的边槽图标 (intelliJ IDEA gutter icon)。

    TIP

    有关如何在 IntelliJ IDE 中运行单元测试的更多详细信息,请参见IntelliJ IDEA 文档

处理 POST 请求

你可以按照上述过程创建任意数量的 GET 请求附加路由。这些将允许用户使用我们喜欢的任何搜索条件来获取任务。但用户也希望能够创建新任务。

在这种情况下,合适的 HTTP 请求类型是 POST。POST 请求通常在用户完成并提交 HTML 表单时触发。

与 GET 请求不同,POST 请求具有一个 body,其中包含表单中所有输入的名称和值。此信息经过编码,以分离不同输入的数据并转义非法字符。你无需担心此过程的详细信息,因为浏览器和 Ktor 将为我们管理它。

接下来,你将通过以下步骤扩展现有应用程序以允许创建新任务:

  1. 创建一个包含 HTML 表单的静态内容文件夹。
  2. 让 Ktor 知道此文件夹,以便可以提供其内容。
  3. 添加新的请求处理程序来处理表单提交。
  4. 测试已完成的功能。
  1. src/main/resources 内部,创建一个名为 task-ui 的新目录。 这将是你静态内容的文件夹。

  2. task-ui 文件夹中,创建一个新文件 task-form.html

  3. 打开新创建的 task-form.html 文件并向其中添加以下内容:

    html
  1. 导航至 src/main/kotlin/com/example/plugins 中的 Routing.kt 文件。

  2. 将以下对 staticResources() 的调用添加到 Application.configureRouting() 函数中:

    kotlin

    这将需要以下导入:

    kotlin
  3. 重新启动应用程序。

  4. 在浏览器中导航至 http://0.0.0.0:8080/task-ui/task-form.html。HTML 表单应该会显示:

    A browser window displaying an HTML form

    Routing.kt 中,将以下附加路由添加到 configureRouting() 函数中:

    kotlin

    如你所见,新路由映射到 POST 请求而不是 GET 请求。Ktor 通过调用 receiveParameters() 处理请求体。这会返回请求体中存在的参数集合。

    共有三个参数,因此你可以将关联的值存储在 Triple 中。如果参数不存在,则存储一个空字符串。

    如果任何值为空,服务器将返回状态码为 400 的响应。然后,它将尝试将第三个参数转换为 Priority,如果成功,则将信息作为新 Task 添加到版本库中。这两个操作都可能导致异常,在这种情况下,再次返回状态码 400

    否则,如果一切成功,服务器将向客户端返回 204 状态码( No Content)。这表示他们的请求已成功,但没有新的信息可以发送给他们。

  1. 重新启动应用程序。

  2. 在浏览器中导航至 http://0.0.0.0:8080/task-ui/task-form.html

  3. 使用示例数据填写表单,然后点击 Submit

    A browser window displaying an HTML form with sample data

    当你提交表单时,不应被重定向到新页面。

  4. 导航至 URL http://0.0.0.0:8080/tasks。你应该 会看到新任务已添加。

    A browser window displaying an HTML table with tasks
  5. 为了验证该功能,请将以下测试添加到 ApplicationTest.kt

    kotlin

    在此测试中,两个请求发送到服务器:一个 POST 请求创建新任务,一个 GET 请求确认新任务已添加。进行第一个请求时,使用 setBody() 方法将内容插入请求体中。测试框架提供了一个作用于集合的 formUrlEncode() 扩展方法,它抽象了像浏览器那样格式化数据的过程。

重构路由

如果你检查目前的路由,你会发现所有路由都以 /tasks 开头。你可以通过将它们放入自己的子路由来消除这种重复:

kotlin

如果你的应用程序达到拥有多个子路由的阶段,那么将每个子路由放入自己的辅助函数中是合适的。但是,目前这不是必需的。

你的路由组织得越好,就越容易扩展它们。例如,你可以添加一个按名称查找任务的路由:

kotlin

后续步骤

你现在已经实现了基本的路由和请求处理功能。此外,你还了解了验证、错误处理和单元测试。所有这些主题都将在后续教程中扩展。

继续阅读

下一个教程
学习如何使用 Kotlin 和 Ktor 构建后端服务,其中包含一个生成 JSON 文件的 RESTful API 示例。
,学习如何为你的任务管理器创建一个生成 JSON 文件的 RESTful API。