带验证的查询参数提取器 (`VQuery<T>`
带验证的查询参数提取器 (VQuery<T>)
在处理 GET 请求时,我们经常需要从 URL 的查询字符串 (Query String) 中提取参数,用于分页、排序、筛选等功能。例如 /users?page_num=1&page_size=20。
与处理请求体一样,对这些查询参数进行 验证 是至关重要的,以防止无效的输入(如负数页码)导致程序错误或安全漏洞。
为了将 查询参数解析 和 数据验证 这两个步骤合二为一,我们创建了自定义的 Axum 提取器——VQuery<T> (Validated Query)。
核心理念
如果你的 Handler 代码能够执行,那么 VQuery 中的数据就一定是已经通过了所有验证规则的 有效查询参数。你无需再进行任何手动的 validate() 调用或错误处理。
如何使用 VQuery<T>
使用 VQuery 的流程与 VJson 和 VForm 一脉相承,非常简单。
第 1 步:定义你的查询参数结构体
首先,创建一个结构体来表示你期望从 URL 查询字符串中接收的参数。关键在于要为它派生 Deserialize (由 serde 提供) 和 Validate (由 validator 提供) 两个 Trait。
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)] // 关键!
pub struct PaginationParams {
// ... 字段定义
}第 2 步:为字段添加验证注解
使用 validator 库提供的宏属性,为结构体的字段添加具体的验证规则。分页参数是 VQuery 最典型的应用场景。
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct PaginationParams {
// 规定页码必须是大于 0 的正数
#[validate(range(min = 1, message = "页码必须大于 0"))]
pub page_num: u64,
// 规定每页数量在 1 到 100 之间
#[validate(range(min = 1, max = 100, message = "每页数量必须在 1 到 100 之间"))]
pub page_size: u64,
// 假设还有一个可选的搜索关键字
pub keyword: Option<String>,
}第 3 步:在 Handler 中使用 VQuery<T>
现在,你可以在 Handler 的函数签名中直接使用 VQuery<YourParams> 来代替 Axum 原生的 Query<T>。
✅ 推荐做法 (使用 VQuery)
use crate::common::validatedquery::VQuery; // 引入 VQuery
// Handler 代码极其简洁和专注
pub async fn list_articles(
// 只需这一行!
// 如果查询参数缺失、类型错误或验证失败,请求会直接被拒绝并返回 400 错误,
// 根本不会执行这个函数的代码。
VQuery(params): VQuery<PaginationParams>,
) -> Response {
// 在这里,你可以完全信任 params.page_num 和 params.page_size 是合法的
// 直接进入核心业务逻辑
let result = article_service::get_paginated_list(params).await;
ApiResponse::from_result(result)
}❌ 传统做法 (使用 Query,对比)
如果没有 VQuery,你的代码会是这样,充满了模板式的验证和错误处理逻辑:
use axum::extract::Query;
pub async fn list_articles_legacy(
Query(params): Query<PaginationParams>,
) -> Response {
// 1. 手动调用 validate()
if let Err(e) = params.validate() {
// 2. 手动处理验证错误,并返回 400 响应
return ApiResponse::bad_request(e.to_string());
}
// 3. 只有通过了验证,才能继续执行业务逻辑
let result = article_service::get_paginated_list(params).await;
ApiResponse::from_result(result)
}对比之下,VQuery 显然让我们的代码更加健壮和优雅。
核心优势
- 代码简洁 (Clean Code): 将验证逻辑从 Handler 中剥离,使其只关注核心业务,极大地提升了代码的可读性。
- 职责分离 (Separation of Concerns): 数据验证的职责被明确地交给了参数结构体自身(通过注解),而不是业务处理函数。
- 安全可靠 (Secure by Default): 从根本上避免了开发者忘记调用验证函数的风险。只要使用了
VQuery,验证就是强制的。 - 统一错误处理 (Unified Error Handling): 无论是查询参数解析失败还是数据验证失败,
VQuery都会自动返回一个格式统一的400 Bad Request错误响应(通过我们的ApiResponse),前端可以获得一致的错误体验。
实现原理:FromRequestParts 与 Deref
VQuery 的“魔法”源于它为自己实现了 Axum 的 FromRequestParts Trait。这与处理请求体的 VJson/VForm (它们实现 FromRequest) 有所不同,因为查询参数只存在于 URI 中,而不需要访问请求体。
当 Axum 看到 Handler 参数中有 VQuery<T> 时,它会调用我们的自定义实现,这个实现会按顺序执行以下操作:
- 内部调用
Query<T>: 它首先使用 Axum 内置的Query<T>::from_request_parts来尝试从 URI 中解析查询参数。如果解析失败(例如,字段缺失或类型不匹配),Query<T>会返回一个QueryRejection错误。 - 执行
.validate(): 如果参数成功解析,它会接着调用value.validate()?。如果数据不符合你在结构体上定义的验证规则,.validate()会返回一个ValidationErrors错误。 - 返回成功或失败:
- 如果以上两步都成功,它会将解析并验证过的数据包装在
VQuery中返回给 Handler。 - 如果任何一步失败,它会捕获错误,并将它们统一包装成我们自定义的
ServerError类型,最终由ApiResponse转换为400 Bad Request响应。
- 如果以上两步都成功,它会将解析并验证过的数据包装在
便利性特性:Deref
我们还为 VQuery 实现了 Deref Trait (axum_core::__impl_deref!(VQuery);)。这是一个非常有用的“语法糖”,它允许你直接在 VQuery 实例上访问内部数据的字段,就好像它不是一个包装类型一样。
- 没有
Deref:VQuery(params)->params.0.page_num - 有了
Deref:VQuery(params)->params.page_num(实际上你甚至可以直接用params作为参数名,然后写params.page_num)
这让代码写起来更加自然。