使用 Ktor 和 Kotlin 处理 HTTP 请求并生成响应
在本教程中,你将通过构建任务管理器应用程序,学习如何使用 Ktor 在 Kotlin 中进行路由、处理请求和参数的基础知识。
完成本教程后,你将了解如何执行以下操作:
- 处理 GET 和 POST 请求。
- 从请求中提取信息。
- 转换数据时处理错误。
- 使用单元测试来验证路由。
先决条件
这是 Ktor 服务器入门指南的第二个教程。你可以独立完成本教程,但我们强烈建议你先完成前一个教程,学习如何
了解 HTTP 请求类型、标头和状态码的基本知识也很有用。
我们推荐安装 IntelliJ IDEA,但你也可以使用其他你选择的 IDE。
任务管理器应用程序
在本教程中,你将逐步构建一个具有以下功能的任务管理器应用程序:
- 以 HTML 表格形式查看所有可用的任务。
- 再次以 HTML 形式,按优先级和名称查看任务。
- 通过提交 HTML 表单添加其他任务。
你将尽可能地实现一些基本功能,然后通过七次迭代改进和扩展此功能。这项基本功能将由一个包含某些模型类型、值列表和一个路由的项目组成。
显示静态 HTML 内容
在第一次迭代中,你将为应用程序添加一个新路由,它将返回静态 HTML 内容。
使用 Ktor 项目生成器,创建一个名为 ktor-task-app 的新项目。你可以接受所有默认选项,但可能希望更改 artifact 名称。
TIP
关于创建新项目的更多信息,请参见- 打开 Routing.kt 文件,该文件位于 src/main/kotlin/com/example/plugins 文件夹中。
将现有的
Application.configureRouting()
函数替换为以下实现:kotlin这样,你已为 URL
/tasks
和 GET 请求类型创建了一个新路由。GET 请求是 HTTP 中最基本的请求类型。当用户在浏览器的地址栏中键入或点击常规 HTML 链接时会触发它。目前你只返回静态内容。要通知客户端你将发送 HTML,你需要将 HTTP Content Type 标头设置为
"text/html"
。添加以下导入以访问
ContentType
对象:kotlin在 IntelliJ IDEA 中,点击 Application.kt 中
main()
函数旁边的运行边槽图标 (),以启动应用程序。
在浏览器中导航至 http://0.0.0.0:8080/tasks。你应该会看到待办列表显示:
实现任务模型
现在你已经创建了项目并设置了基本路由,接下来你将通过以下操作扩展你的应用程序:
在 src/main/kotlin/com/example 内部,创建一个名为 model 的新子包。
在 model 目录中,创建一个新文件 Task.kt 。
打开 Task.kt 文件,添加以下
enum
来表示优先级,并添加一个class
来表示任务:kotlin你将把任务信息发送到客户端的 HTML 表格中,因此也请添加以下扩展函数:
kotlinTask.taskAsRow()
函数使Task
对象能够渲染为表格行,而List<Task>.tasksAsTable()
允许将任务列表渲染为表格。
在你的 model 目录中,创建一个新文件 TaskRepository.kt 。
打开 TaskRepository.kt 并添加以下代码来定义一个任务列表:
kotlin
打开 Routing.kt 文件,并将现有
Application.configureRouting()
函数替换为以下实现:kotlin现在你不再向客户端返回静态内容,而是提供任务列表。由于列表无法直接通过网络发送,因此必须将其转换为客户端能理解的格式。在此例中,任务被转换为 HTML 表格。
添加所需的导入:
kotlin
在 IntelliJ IDEA 中,点击重新运行按钮 (
) 以重新启动应用程序。
在浏览器中导航至 http://0.0.0.0:8080/tasks。它应该显示一个包含任务的 HTML 表格:
如果是这样,恭喜你!应用程序的基本功能已正常工作。
重构模型
在继续扩展应用程序的功能之前,你需要通过将值列表封装在版本库中来重构设计。这将使你能够集中管理数据,从而专注于 Ktor 特有的代码。
返回 TaskRepository.kt 文件,并将现有任务列表替换为以下代码:
kotlin这实现了一个非常简单的基于列表的任务数据存储。为了示例目的,任务的添加顺序将被保留,但通过抛出异常来禁止重复项。
在后续教程中,你将学习如何通过 Exposed 库实现连接到关系型数据库的版本库。
目前,你将在路由中使用此版本库。
打开 Routing.kt 文件,并将现有
Application.configureRouting()
函数替换为以下实现:Kotlin当请求到来时,版本库用于获取当前的任务列表。然后,构建包含这些任务的 HTTP 响应。
在 IntelliJ IDEA 中,点击重新运行按钮 (
) 以重新启动应用程序。
在浏览器中导航至 http://0.0.0.0:8080/tasks。输出应保持不变,显示 HTML 表格:
处理参数
在此次迭代中,你将允许用户按优先级查看任务。为此,你的应用程序必须允许对以下 URL 发送 GET 请求:
你将添加的路由是 /tasks/byPriority/{priority?}
,其中 {priority?}
表示你需要在运行时提取的路径参数,问号用于表示参数是可选的。查询参数可以是你喜欢的任何名称,但 priority
似乎是显而易见的选择。
处理请求的过程可总结如下:
- 从请求中提取一个名为
priority
的路径参数。 - 如果此参数缺失,则返回
400
状态(Bad Request)。 - 将参数的文本值转换为
Priority
枚举值。 - 如果失败,则返回状态码为
400
的响应。 - 使用版本库查找所有具有指定优先级的任务。
- 如果没有匹配的任务,则返回
404
状态(Not Found)。 - 返回匹配的任务,格式化为 HTML 表格。
你将首先实现此功能,然后找到检测其是否正常工作的最佳方式。
打开 Routing.kt 文件,并将以下路由添加到你的代码中,如下所示:
如上所述,你已为 URL /tasks/byPriority/{priority?}
编写了一个处理程序。符号 priority
代表用户添加的路径参数。不幸的是,在服务器上无法保证这对应于 Kotlin 枚举中的四个值之一,因此必须手动检测。
如果路径参数缺失,服务器将向客户端返回 400
状态码。否则,它会提取参数的值并尝试将其转换为枚举的成员。如果此操作失败,则会抛出异常,服务器会捕获该异常并返回 400
状态码。
假设转换成功,版本库将用于查找匹配的 Tasks
。如果没有指定优先级的任务,服务器会返回 404
状态码,否则会以 HTML 表格的形式发送匹配项。
在 IntelliJ IDEA 中,点击重新运行按钮 (
) 以重新启动应用程序。
要检索所有中等优先级任务,请导航至 http://0.0.0.0:8080/tasks/byPriority/Medium:
不幸的是,在出现错误的情况下,你通过浏览器进行的测试是有限的。除非你使用开发者扩展,否则浏览器不会显示不成功响应的详细信息。一个更简单的替代方案是使用专业工具,例如 Postman。
在 Postman 中,发送针对相同 URL
http://0.0.0.0:8080/tasks/byPriority/Medium
的 GET 请求。这显示了服务器的原始输出,以及请求和响应的所有详细信息。
要检测对紧急任务的请求是否返回
404
状态码,请向http://0.0.0.0:8080/tasks/byPriority/Vital
发送新的 GET 请求。然后你将在 Response 面板的右上角看到显示的状态码。要验证当指定无效优先级时是否返回
400
,请创建另一个包含无效属性的 GET 请求:
你可以通过在浏览器中请求不同的 URL 来测试此功能。
添加单元测试
到目前为止,你已经添加了两个路由——一个用于检索所有任务,另一个用于按优先级检索任务。像 Postman 这样的工具使你能够完全测试这些路由,但它们需要手动探查并在 Ktor 外部运行。
这在原型设计和小型应用程序中是可以接受的。然而,这种方法不适用于大型应用程序,其中可能需要频繁运行数千个测试。一个更好的解决方案是完全自动化你的测试。
Ktor 提供其自己的
在 src 中创建一个名为 test 的新目录,并创建一个名为 kotlin 的子目录。
在 src/test/kotlin 内部,创建一个新文件 ApplicationTest.kt 。
打开 ApplicationTest.kt 文件并添加以下代码:
kotlin在每个测试中都创建了一个新的 Ktor 实例。这在测试环境中运行,而不是像 Netty 这样的 Web 服务器中运行。项目生成器为你编写的模块会被加载,这反过来会调用路由函数。然后,你可以使用内置的
client
对象向应用程序发送请求,并验证返回的响应。测试可以在 IDE 内部运行,也可以作为 CI/CD 流水线的一部分运行。
要在 IntelliJ IDE 中运行测试,请点击每个测试函数旁边的边槽图标 (
)。
TIP
有关如何在 IntelliJ IDE 中运行单元测试的更多详细信息,请参见IntelliJ IDEA 文档。
处理 POST 请求
你可以按照上述过程创建任意数量的 GET 请求附加路由。这些将允许用户使用我们喜欢的任何搜索条件来获取任务。但用户也希望能够创建新任务。
在这种情况下,合适的 HTTP 请求类型是 POST。POST 请求通常在用户完成并提交 HTML 表单时触发。
与 GET 请求不同,POST 请求具有一个 body
,其中包含表单中所有输入的名称和值。此信息经过编码,以分离不同输入的数据并转义非法字符。你无需担心此过程的详细信息,因为浏览器和 Ktor 将为我们管理它。
接下来,你将通过以下步骤扩展现有应用程序以允许创建新任务:
在 src/main/resources 内部,创建一个名为 task-ui 的新目录。 这将是你静态内容的文件夹。
在 task-ui 文件夹中,创建一个新文件 task-form.html 。
打开新创建的 task-form.html 文件并向其中添加以下内容:
html
导航至 src/main/kotlin/com/example/plugins 中的 Routing.kt 文件。
将以下对
staticResources()
的调用添加到Application.configureRouting()
函数中:kotlin这将需要以下导入:
kotlin重新启动应用程序。
在浏览器中导航至 http://0.0.0.0:8080/task-ui/task-form.html。HTML 表单应该会显示:
在 Routing.kt 中,将以下附加路由添加到 configureRouting()
函数中:
如你所见,新路由映射到 POST 请求而不是 GET 请求。Ktor 通过调用 receiveParameters()
处理请求体。这会返回请求体中存在的参数集合。
共有三个参数,因此你可以将关联的值存储在 Triple 中。如果参数不存在,则存储一个空字符串。
如果任何值为空,服务器将返回状态码为 400
的响应。然后,它将尝试将第三个参数转换为 Priority
,如果成功,则将信息作为新 Task
添加到版本库中。这两个操作都可能导致异常,在这种情况下,再次返回状态码 400
。
否则,如果一切成功,服务器将向客户端返回 204
状态码( No Content)。这表示他们的请求已成功,但没有新的信息可以发送给他们。
重新启动应用程序。
使用示例数据填写表单,然后点击 Submit 。
当你提交表单时,不应被重定向到新页面。
导航至 URL http://0.0.0.0:8080/tasks。你应该 会看到新任务已添加。
为了验证该功能,请将以下测试添加到 ApplicationTest.kt:
kotlin在此测试中,两个请求发送到服务器:一个 POST 请求创建新任务,一个 GET 请求确认新任务已添加。进行第一个请求时,使用
setBody()
方法将内容插入请求体中。测试框架提供了一个作用于集合的formUrlEncode()
扩展方法,它抽象了像浏览器那样格式化数据的过程。
重构路由
如果你检查目前的路由,你会发现所有路由都以 /tasks
开头。你可以通过将它们放入自己的子路由来消除这种重复:
如果你的应用程序达到拥有多个子路由的阶段,那么将每个子路由放入自己的辅助函数中是合适的。但是,目前这不是必需的。
你的路由组织得越好,就越容易扩展它们。例如,你可以添加一个按名称查找任务的路由:
后续步骤
你现在已经实现了基本的路由和请求处理功能。此外,你还了解了验证、错误处理和单元测试。所有这些主题都将在后续教程中扩展。
继续阅读