
Vapor 教程系列 - 05 异步编程与全面错误处理
前面几篇文章中,我们一起完成了 TILApp 的基础功能开发。本文我们将深入了解 Vapor 中的异步编程和错误处理,这对于构建一个健壮的生产级 Web API 至关重要。文章会结合代码示例,帮你理解和掌握这些概念。🚀
- 第一部分:构建 Web API
- 创建项目和设置数据库与路由
- 实现基本 CRUD 操作与控制器封装
- 进阶数据库模型与 API 实现
- Web API 测试与稳定性验证
- 异步编程与全面错误处理
- 第二部分:制作简单的前端网页应用
- 第三部分:数据校验、用户验证和授权
- 第四部分:Vapor 的进阶使用
- 第五部分:生产部署
前面几篇文章中,我们一起完成了 TILApp 的基础功能开发。本文我们将深入了解 Vapor 中的异步编程和错误处理,这对于构建一个健壮的生产级 Web API 至关重要。文章会结合代码示例,帮你理解和掌握这些概念。🚀
异步编程概述
Vapor 使用 SwiftNIO,这是一个用于构建高性能、非阻塞网络应用的工具。Swift 5.5 引入了 async/await,让我们能够更加轻松地实现异步操作 🌀,使代码更加简洁 ✨ 和易于理解。通过使用 async/await,Vapor 的异步代码变得更自然、更符合直觉。
什么是SwiftNIO?
SwiftNIO 是一个基于事件驱动的框架,专门用于处理网络应用的异步事件。它能够高效管理并发请求 📥📤,而无需阻塞线程来等待操作完成。因此,Vapor 能够更加高效地处理并发请求,非常适合用于构建大型、高性能的应用。
为什么需要异步?
在 Web 应用中,很多操作都是耗时的,比如:
- 数据库查询 🗄️
- 文件读写 📁
- 网络请求 🌐
- 加密解密 🔐
如果同步执行这些操作,会导致:
- 程序阻塞,降低响应速度
- 资源利用率低
- 用户体验差
使用异步编程可以让程序在等待这些耗时操作的同时继续处理其他请求,提高系统的吞吐量。
用 async/await 替换 EventLoopFuture
以前,我们使用 EventLoopFuture 来实现异步编程,但现在有了 async/await,我们可以使用更简单的语法来完成异步操作。这意味着不再需要频繁使用回调函数,使得代码更易于维护。
例如,之前使用 EventLoopFuture 的代码:
1 2 3app.get("hello") { req in return req.eventLoop.future("Hello, Vapor!") }
现在可以简化为:
1 2 3app.get("hello") { req async -> String in return "Hello, Vapor!" }
Async/Await 基础使用
在 Vapor 中,异步函数都标记为 async,调用时需要使用 await:
1 2 3 4@Sendable func getAllHandler(_ req: Request) async throws -> [User] { try await User.query(on: req.db).all() }
让我们修改之前 TILApp 中的一些函数,使其更加异步友好。打开 Sources/App/Controllers/AcronymsController.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20// 修改创建 acronym 的方法 @Sendable func createHandler(_ req: Request) async throws -> Acronym { let data = try req.content.decode(CreateAcronymData.self) // 异步查询确认用户存在 guard let user = try await User.find(data.userID, on: req.db) else { throw Abort(.notFound, reason: "User not found") } let acronym = Acronym( short: data.short, long: data.long, userID: user.id! ) // 异步保存 try await acronym.save(on: req.db) return acronym }
错误处理
一个健壮的 Web API 需要优雅地处理各种错误情况。Vapor 提供了全面的错误处理机制。
自定义错误类型
首先创建一个自定义错误类型来处理 TILApp 中可能出现的错误。在 Sources/App/Errors 目录下创建 TILError.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37import Vapor enum TILError { case acronymNotFound case userNotFound case categoryNotFound case invalidUserID case duplicateAcronym(String) } extension TILError: AbortError { var status: HTTPStatus { switch self { case .acronymNotFound, .userNotFound, .categoryNotFound: return .notFound case .invalidUserID: return .badRequest case .duplicateAcronym: return .conflict } } var reason: String { switch self { case .acronymNotFound: return "Acronym not found" case .userNotFound: return "User not found" case .categoryNotFound: return "Category not found" case .invalidUserID: return "Invalid user ID provided" case .duplicateAcronym(let short): return "Acronym '\(short)' already exists" } } }
应用错误处理
现在让我们修改 AcronymsController 中的方法使用新的错误类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39@Sendable func getHandler(_ req: Request) async throws -> Acronym { guard let acronym = try await Acronym.find(req.parameters.get("acronymID"), on: req.db) else { throw TILError.acronymNotFound } return acronym } @Sendable func updateHandler(_ req: Request) async throws -> Acronym { let updatedData = try req.content.decode(AcronymCreateData.self) // 检查用户是否存在 guard let user = try await User.find(updatedData.userID, on: req.db) else { throw TILError.userNotFound } // 查找要更新的缩写 guard let acronym = try await Acronym.find(req.parameters.get("acronymID"), on: req.db) else { throw TILError.acronymNotFound } // 检查是否存在重复 let existingAcronym = try await Acronym.query(on: req.db) .filter(\.$short == updatedData.short) .filter(\.$id != acronym.id!) .first() if existingAcronym != nil { throw TILError.duplicateAcronym(updatedData.short) } acronym.short = updatedData.short acronym.long = updatedData.long try await acronym.save(on: req.db) return acronym }
全局错误处理
为了统一处理错误并返回标准格式的错误响应,我们可以创建一个自定义的错误中间件。创建 Sources/App/Middleware/CustomErrorMiddleware.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39import Vapor struct CustomErrorMiddleware: AsyncMiddleware { func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { do { return try await next.respond(to: request) } catch { // 记录错误 request.logger.error("\(error)") // 构建错误响应 let status: HTTPStatus let reason: String if let abortError = error as? AbortError { status = abortError.status reason = abortError.reason } else { status = .internalServerError #if DEBUG reason = "\(error)" #else reason = "Something went wrong." #endif } // 创建JSON响应 let response = Response(status: status) try response.content.encode(CustomErrorResponse(error: true, reason: reason)) return response } } } struct CustomErrorResponse: Content { let error: Bool let reason: String }
在 configure.swift 中注册这个中间件:
1 2 3 4 5 6 7// 移除默认的错误中间件 app.middleware = .init() // 添加自定义错误中间件 app.middleware.use(CustomErrorMiddleware()) // 添加其他中间件...
测试错误处理
让我们测试一下错误处理机制。首先测试查找不存在的缩写:
测试更新为其它存在的缩写,以检测重复错误的响应:
改进日志记录
为了更好地监控和调试应用,我们需要完善日志记录。Vapor 使用 SwiftLog 进行日志管理。
配置日志级别
在 configure.swift 中设置合适的日志级别:
1 2 3 4 5 6 7// 开发环境使用 debug 级别 if app.environment == .development { app.logger.logLevel = .debug } else { // 生产环境使用 notice 级别 app.logger.logLevel = .notice }
理解日志级别
Vapor 基于 SwiftLog 提供了多个日志级别,从最详细到最严重依次是:
💡 选择合适的日志级别很重要:
- 开发环境使用 debug 级别,可以看到详细信息
- 测试环境使用 info 级别,关注重要操作
- 生产环境使用 notice 级别,保持性能的同时不错过重要信息
添加关键操作日志
修改 AcronymsController 添加重要操作的日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21func create(_ req: Request) async throws -> Acronym { let data = try req.content.decode(CreateAcronymData.self) req.logger.info("Creating new acronym: \(data.short)") guard let user = try await User.find(data.userID, on: req.db) else { req.logger.error("User not found for id: \(data.userID)") throw TILError.userNotFound } let acronym = Acronym( short: data.short, long: data.long, userID: user.id! ) try await acronym.save(on: req.db) req.logger.info("Successfully created acronym: \(acronym.id?.uuidString ?? "")") return acronym }
测试缩写创建接口,可以看到添加的 INFO 日志信息:
同理,可以把 users/categories/acronyms 所有的接口处理中添加上日志记录操作(让 AI 帮你快速添加)。
日志最佳实践
选择合适的日志级别
- trace/debug: 仅用于开发调试
- info: 记录正常的业务操作
- notice: 记录需要关注的系统状态
- warning: 记录潜在问题
- error: 记录具体错误
- critical: 记录严重故障
结构化的日志信息
1 2 3 4 5 6 7 8 9// 好的实践 req.logger.info("User action", metadata: [ "user_id": .string(user.id?.uuidString ?? ""), "action": "create_acronym", "acronym_short": .string(acronym.short) ]) // 避免这样 req.logger.info("User \(userId) created acronym \(acronym)")
包含关键上下文
1 2 3 4 5 6 7 8extension Logger { func logError(_ error: Error, context: String) { self.error("\(context): \(error)", metadata: [ "error_type": .string(String(describing: type(of: error))), "stack_trace": .string(String(describing: error)) ]) } }
通过合理使用日志级别和良好的日志实践,我们可以:
- 更容易地进行问题诊断 🔍
- 监控系统健康状态 💓
- 追踪用户行为 👥
- 分析系统性能 📊
性能优化
避免阻塞操作
在处理请求时,应该避免执行阻塞操作。以加密操作为例,我们应该将其放在后台线程执行:
1 2 3 4 5 6 7 8app.post("hash") { req async throws -> String in // 在后台线程执行密码哈希 try await req.application.threadPool.async { // 模拟耗时的哈希操作 sleep(1) return "hashed_result" } }
并发操作
当需要执行多个独立的异步操作时,可以使用 Swift 的并发特性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14app.post("process") { req async throws -> ProcessResult in async let hash1 = req.application.threadPool.async { try Bcrypt.hash("password1") } async let hash2 = req.application.threadPool.async { try Bcrypt.hash("password2") } // 等待所有哈希操作完成 let (result1, result2) = try await (hash1, hash2) return ProcessResult(hash1: result1, hash2: result2) }
以我们更新 Acronym 的接口为例,原代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31@Sendable func updateHandler(_ req: Request) async throws -> Acronym { let updatedData = try req.content.decode(AcronymCreateData.self) // 检查用户是否存在 guard let user = try await User.find(updatedData.userID, on: req.db) else { throw TILError.userNotFound } // 查找要更新的缩写 guard let acronym = try await Acronym.find(req.parameters.get("acronymID"), on: req.db) else { throw TILError.acronymNotFound } // 检查是否存在重复 let existingAcronym = try await Acronym.query(on: req.db) .filter(\.$short == updatedData.short) .filter(\.$id != acronym.id!) .first() if existingAcronym != nil { throw TILError.duplicateAcronym(updatedData.short) } acronym.short = updatedData.short acronym.long = updatedData.long try await acronym.save(on: req.db) return acronym }
如果使用并发操作优化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30func update(_ req: Request) async throws -> Acronym { let updatedData = try req.content.decode(CreateAcronymData.self) // 并发执行检查 async let acronymExistsCheck = Acronym.find(req.parameters.get("acronymID"), on: req.db) async let duplicateCheck = Acronym.query(on: req.db) .filter(\.$short == updatedData.short) .first() // 等待所有检查完成 let (acronym, existingAcronym) = try await (acronymExistsCheck, duplicateCheck) // 验证结果 guard let acronym = acronym else { throw TILError.acronymNotFound } if let existing = existingAcronym, existing.id != acronym.id { throw TILError.duplicateAcronym(updatedData.short) } // 执行更新 acronym.short = updatedData.short acronym.long = updatedData.long try await acronym.save(on: req.db) return acronym }
我们的代码就先不按照这种方式优化了,大家只需知道后续可以对多个异步操作使用并发操作即可。
总结
通过本文的学习,我们:
- 掌握了 Vapor 中的异步编程基础 ⚡️
- 实现了全面的错误处理机制 🛡️
- 配置了合适的日志记录 📝
- 学习了一些性能优化技巧 🚀
希望这些内容能帮助你构建更健壮、高效的 Web API。下一篇我们将开始制作前端网页,不要错过! 💪
附件
本文代码: s1-5.zip



