Appearance
第5章 Handler trait:从 async fn 到 tower::Service
两个世界之间的鸿沟
你写下这样一段 Axum 代码:
rust
async fn create_user(
State(pool): State<PgPool>,
Path(org_id): Path<u32>,
Json(body): Json<CreateUserRequest>,
) -> impl IntoResponse {
// ...
}三行参数声明,每个参数都有自己的类型和提取语义。State<PgPool> 从应用状态中获取数据库连接池,Path<u32> 从 URL 路径中解析组织 ID,Json<CreateUserRequest> 从请求体中反序列化 JSON。然后你把 create_user 传给 .route("/orgs/{org_id}/users", post(create_user)),一切都能工作。
但 tower 的世界不是这样的。tower::Service 的签名是:
rust
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
fn call(&mut self, req: Request) -> Self::Future;
}一个 call 方法,接收一个 Request,返回一个 Future。没有类型化的参数列表,没有自动提取,没有从 State / Path / Json 到函数参数的映射。Service 的世界是"一个请求进,一个响应出",类型信息在边界处被抹平了。
你写的 async fn 有零到十六个类型化的提取器参数,而 Service::call 只有一个 Request 参数。这两个世界之间存在一道鸿沟:谁来把 Request 拆解成各个部分、逐一调用提取器、把提取结果喂给函数、把函数返回值转换成 Response?谁来保证提取失败时返回正确的拒绝响应而不是 panic?谁来处理"最后一个提取器需要消费请求体,之前的提取器只能借用请求头"这个约束?
这道鸿沟的桥梁就是 Handler<T, S> trait。它是 Axum 整个架构的枢纽——路由系统(第 2 章、第 3 章)存储的是 Handler,中间件系统消费的是 Service,而 Handler 到 Service 的转换由 HandlerService 完成。理解了 Handler trait,你就理解了 Axum "为什么能把 async fn 当作 Service 用"这个根本问题。
Handler<T, S> trait 的定义
打开 axum/src/handler/mod.rs 第 148 行,你会看到 Handler trait 的完整定义:
rust
// handler/mod.rs:148
pub trait Handler<T, S>: Clone + Send + Sync + Sized + 'static {
type Future: Future<Output = Response> + Send + 'static;
fn call(self, req: Request, state: S) -> Self::Future;
fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>
where
L: Layer<HandlerService<Self, T, S>> + Clone,
L::Service: Service<Request>,
{ /* 默认实现 */ }
fn with_state(self, state: S) -> HandlerService<Self, T, S> {
HandlerService::new(self, state)
}
}逐行拆解。
关联类型 Future
type Future: Future<Output = Response> + Send + 'static;
Handler 的 Future 输出的是 Response,不是 Result<Response, Error>。这和 tower::Service 的 Result<Response, Error> 不同。原因很简单:Handler 的设计哲学是"所有错误都在 handler 内部被转换成响应"。提取器拒绝是响应,业务逻辑错误是响应,panic 是 hyper 层面的问题——但只要控制流还在 Handler 体系内,一切错误最终都变成 Response。这个设计选择有一个重要的推论:当 Handler 被转换成 Service 后,Error 类型永远是 Infallible——不可能出错。Infallible 向上传播到 Router,再到 hyper,形成一条"类型系统保证不会失败"的链条。
参数 T:一致性规则的变通
T 是 Handler trait 最不直观的部分。文档(handler/mod.rs:129-144)的解释是:
The type parameter
Tis a workaround for trait coherence rules, allowing us to write blanket implementations ofHandlerover many types of handler functions with different numbers of arguments, without the compiler forbidding us from doing so because one typeFcan in theory implement bothFn(A) -> XandFn(A, B) -> Y.
Rust 的一致性规则(orphan rule)规定:如果要为类型 F 实现 trait Handler<T, S>,要么 Handler 是当前 crate 定义的,要么 F 是当前 crate 定义的。Handler 是 axum 定义的,所以第一个条件满足。但问题出在重叠(overlap):同一个 F 类型理论上可能同时满足 Fn() -> Fut 和 Fn(A) -> Fut(虽然实际上不可能,但编译器不知道),这会导致两个 blanket impl 重叠。
T 参数就是用来区分不同的 blanket impl 的。对于零参数的 handler,T = ((),);对于单参数 handler,T = (M, T1,);对于双参数 handler,T = (M, T1, T2,);以此类推直到十六个参数。这些 T 类型各不相同,所以不会重叠。T 本身是零大小类型(ZST),不占用任何运行时内存——它纯粹是编译期的标记。
为什么是 ((),) 而不是 ()?因为 () 本身可能被其他 impl 使用,而 ((),) 这个单元素元组在类型层面足够独特,不会与其他 blanket impl 冲突。
参数 S:状态类型
S 是应用状态类型,和 Router<S> 中的 S 一致。当 handler 使用 State<PgPool> 提取器时,S 就是 PgPool。当 handler 不使用状态时,S 可以是任意类型(但实际上会是 ())。
call 方法的签名是 fn call(self, req: Request, state: S) -> Self::Future。注意 self 是按值传递的——handler 被调用一次后就消费掉了。但 Handler 要求 Clone,所以 HandlerService::call 在调用前会先 clone 一份 handler,保留原始 handler 供后续请求使用。
四个 trait bound
Clone + Send + Sync + Sized + 'static,每一个都有存在的理由:
- Clone:Handler 被存储在 MethodRouter 中,每次请求到来时需要 clone 一份来调用。因为
call(self)按值消费 handler,不 clone 的话第一个请求之后 handler 就没了。 - Send + Sync:Axum 运行在 tokio 的多线程运行时上,handler 可能在不同线程间转移。
Send保证 handler 可以安全地跨线程移动,Sync保证&handler可以安全地跨线程共享。 - Sized:Rust 的泛型默认要求
Sized,但 Handler 显式标注是为了排除?Sized类型——handler 必须有确定的大小,否则无法在栈上分配和 clone。 - 'static:handler 不允许持有非
'static的引用。这意味着你不能把一个局部变量的引用塞进 handler 的闭包里——handler 必须拥有自己的所有数据,或者引用'static数据。这是 tokio 运行时的要求:spawn 的 future 必须是'static的。
Clone 而不是 Arc:按值 clone 的合理性
一个自然的追问是:既然每次请求都 clone,为什么不直接要求 Arc<H>,让所有用户只付出一次原子加一的代价?
答案藏在"handler"这个词的实际内容里。一个典型 handler 是无捕获的 async fn——它本身是零大小类型(ZST),.clone() 编译后是零指令;即便是有捕获的闭包,大多数情况下捕获的也是 Arc<PgPool> 这种指针,clone 就是一次原子加一。和 Arc<H> 方案相比,现有方案每次请求少一次 Arc::clone、少一次解引用。对 ZST 和轻量闭包来说,Axum 的 Clone 比假想的 Arc<H> 更便宜。
只有当 H 是内嵌大数据的闭包时(比如捕获了 Vec<Layer> 或一个 10 MB 的配置),按值 clone 才会比 Arc::clone 贵。这种情况下,用户可以自己把 handler 用 Arc 包一层——Arc<F> 在 F: Fn(...) -> Fut 时可以被调用,.clone() 走 Arc::clone。Axum 不替所有用户默认加这层 Arc,是因为这种"大闭包 handler"极罕见,为它付出所有普通 handler 多一次解引用的代价并不划算。这是 Rust 生态对待类型成本的典型态度:默认提供最简单、零开销的形态,昂贵的语义交给调用者显式选择。
还有一个更深的理由:Handler::call(self, ...) 按值接收 self,允许实现者在 call 内部把 handler 的字段 move 到 async block 里、spawn 到 tokio 任务里,或者在不同的提取阶段之间转移所有权。如果 handler 是 &Arc<H> 或 &H,call 内部只能借用,一些需要 'static 所有权的操作就做不了。按值传递是最灵活的入口形态——虽然目前的 blanket impl 都没有动用这个自由度,但这个设计选择不给未来的扩展设限。
零参数实现:最简单的 blanket impl
理解 Handler 的最佳入口是零参数实现——它没有提取器的复杂性,只有"调用函数、转换结果"的核心逻辑。在 handler/mod.rs:208-219 行:
rust
// handler/mod.rs:208-219
impl<F, Fut, Res, S> Handler<((),), S> for F
where
F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Res> + Send,
Res: IntoResponse,
{
type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
fn call(self, _req: Request, _state: S) -> Self::Future {
Box::pin(async move { self().await.into_response() })
}
}这段代码的意思是:任何满足 FnOnce() -> Fut 的函数 F,只要 Fut 的输出实现了 IntoResponse,就自动实现 Handler<((),), S>。
注意几个细节:
_req和_state被忽略。零参数 handler 不需要请求也不需要状态——比如async fn health() { "ok" }。请求会被丢弃(包括请求体),不会造成资源泄漏——hyper 会在连接层面处理未读取的请求体。self()直接调用。因为F: FnOnce(),调用时不需要传任何参数。调用结果是Fut,.await得到Res,然后.into_response()转换成Response。Future 类型是
Pin<Box<dyn Future>>。这是异步 Rust 的标准模式——因为self().await.into_response()的具体 Future 类型依赖于F和Fut,无法在 trait 定义时写出一个具体的返回类型。Box<dyn Future>用一次堆分配换来了类型擦除的灵活性。标记类型是
((),)。一个包含空元组的单元素元组。这个类型在整个 Rust 生态中几乎不可能被其他 blanket impl 使用,保证了一致性规则的安全性。
零参数实现是理解后续所有实现的基础。多参数实现做的事情本质上和零参数一样——区别在于调用 self(...) 之前,需要先把 Request 拆解成各个提取器的值。
all_the_tuples! 与参数提取的核心逻辑
从零参数到多参数的跳跃,是 Handler trait 的核心。这一段代码用宏生成,但展开后的逻辑非常清晰——值得我们逐行理解。
宏展开的过程
handler/mod.rs:221-262 定义了 impl_handler! 宏,然后在第 262 行通过 all_the_tuples!(impl_handler) 展开它。all_the_tuples! 宏定义在 axum/src/macros.rs:49-67:
rust
// axum/src/macros.rs:49-67
macro_rules! all_the_tuples {
($name:ident) => {
$name!([], T1);
$name!([T1], T2);
$name!([T1, T2], T3);
$name!([T1, T2, T3], T4);
$name!([T1, T2, T3, T4], T5);
// ... 省略中间展开 ...
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
};
}以三参数 handler 为例,all_the_tuples! 会调用 impl_handler!([T1, T2], T3),展开成:
rust
#[allow(non_snake_case, unused_mut)]
impl<F, Fut, S, Res, M, T1, T2, T3> Handler<(M, T1, T2, T3,), S> for F
where
F: FnOnce(T1, T2, T3,) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Res> + Send,
S: Send + Sync + 'static,
Res: IntoResponse,
T1: FromRequestParts<S> + Send,
T2: FromRequestParts<S> + Send,
T3: FromRequest<S, M> + Send,
{
type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
fn call(self, req: Request, state: S) -> Self::Future {
let (mut parts, body) = req.into_parts();
Box::pin(async move {
let T1 = match T1::from_request_parts(&mut parts, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
let T2 = match T2::from_request_parts(&mut parts, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
let req = Request::from_parts(parts, body);
let T3 = match T3::from_request(req, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
self(T1, T2, T3,).await.into_response()
})
}
}这段代码虽然由宏生成,但逻辑完全可读。让我们拆解它。
提取顺序:先 Parts 后 Body
提取过程分两个阶段:
第一阶段:从 &mut Parts 提取所有 FromRequestParts 参数。
rust
let (mut parts, body) = req.into_parts();Request::into_parts() 把请求拆成 Parts(头部、方法、URI、extensions 等)和 Body(请求体)。Parts 可以被多次借用——from_request_parts 的签名是 fn from_request_parts(parts: &mut Parts, state: &S),它只借用 Parts,不消费。所以 T1、T2 的提取可以依次进行,每个提取器从 &mut parts 中读取自己需要的字段(比如 Path 读 URI,State 读 extensions),然后交还修改权。
第二阶段:重建 Request,从完整 Request 提取最后一个 FromRequest 参数。
rust
let req = Request::from_parts(parts, body);
let T3 = match T3::from_request(req, &state).await { ... };为什么最后一个参数是 FromRequest 而不是 FromRequestParts?因为请求体只能被消费一次。FromRequest::from_request 接收 Request(包含 body)的所有权,而不是 &mut Parts。Json<T> 是最典型的 FromRequest 提取器——它必须读取完整的请求体来反序列化。一旦 body 被消费,就无法再次提取任何需要 body 的提取器。这就是 Axum 的硬性规则:除最后一个参数外,所有参数必须实现 FromRequestParts;只有最后一个参数可以实现 FromRequest。
如果最后一个参数碰巧也只实现了 FromRequestParts(比如 Path<u32> 是最后一个参数),那也没问题——FromRequestParts<S> 的实现者会自动获得 FromRequest<S, ViaParts> 的 blanket impl(定义在 axum-core/src/extract/mod.rs:91-105),编译器会推导 M = ViaParts,在调用 from_request 时内部把 Request 拆成 parts 再调用 from_request_parts,body 被忽略。
提取失败时的行为
每个提取器调用都包裹在 match 里:
rust
let T1 = match T1::from_request_parts(&mut parts, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};一旦某个提取器失败,整个 handler 的 async move 块立即 return,返回拒绝响应。这意味着:
- 短路语义:如果
T1提取失败,T2和T3根本不会被提取。这避免了"提取了 T2 但 T1 失败了"这种浪费。 - 拒绝类型必须实现
IntoResponse:FromRequestParts::Rejection和FromRequest::Rejection都 bound 了IntoResponse。所以rejection.into_response()一定合法。 - 错误不会传播到 Service 层:拒绝响应是合法的 HTTP 响应(通常是 4xx),不是
Err。这保证了 Handler 的 Future 输出是Response而不是Result<Response, E>。
M 参数:ViaParts 与 ViaRequest
你可能注意到宏定义中有一个 M 泛型参数:
rust
impl<F, Fut, S, Res, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S> for FM 是最后一个提取器的 FromRequest 第二个类型参数,默认值是 private::ViaRequest(axum-core/src/extract/mod.rs:36)。当最后一个参数本身实现了 FromRequestParts 时,M 会被推导为 ViaParts,触发 blanket impl FromRequest<S, ViaParts> for T where T: FromRequestParts<S>。当最后一个参数实现了"原生" FromRequest(比如 Json<T>),M 就是 ViaRequest。
M 出现在标记类型 (M, $($ty,)* $last,) 中,这意味着不同的 M 值会产生不同的 T 类型。这是另一个一致性变通——同一个函数类型 F 理论上可能同时满足 Fn(T1, T2, Json<X>) 和 Fn(T1, T2, Path<Y>),但由于 T 不同(ViaRequest vs ViaParts),两个 impl 不会重叠。
IntoResponseHandler:字符串字面量也是 handler
你可能见过这种写法:
rust
Router::new().route("/", get("Hello, World!"))一个字符串字面量直接作为 handler?"Hello, World!" 既不是 async fn 也不接收 Request 参数。它之所以能工作,是因为 handler/mod.rs:264-280 的另一个 blanket impl:
rust
// handler/mod.rs:264-280
mod private {
pub enum IntoResponseHandler {}
}
impl<T, S> Handler<private::IntoResponseHandler, S> for T
where
T: IntoResponse + Clone + Send + Sync + 'static,
{
type Future = std::future::Ready<Response>;
fn call(self, _req: Request, _state: S) -> Self::Future {
std::future::ready(self.into_response())
}
}任何实现了 IntoResponse 的类型都可以直接作为 handler。标记类型是 private::IntoResponseHandler——一个零大小的 enum,定义在私有模块里,外部无法命名或实现,防止了用户自己写 Handler<IntoResponseHandler, S> 的 impl。
注意 Future 的类型:std::future::Ready<Response>。这是同步 future——std::future::ready() 创建一个立即完成的 future,不需要 Box 分配,不需要异步运行时的调度。和零参数 handler 的 Pin<Box<dyn Future>> 相比,Ready<Response> 是零堆分配的。对于固定响应(如静态字符串、状态码),这个优化是有意义的。
这也意味着 (StatusCode, Json(json!({ "id": 1 }))) 这样的元组也能直接当 handler 用——因为 (StatusCode, Json<Value>) 实现了 IntoResponse。这在 mock 接口和健康检查等场景下非常方便。
HandlerService:从 Handler 到 Service 的适配器
Handler 定义了"如何从 Request + State 提取参数并调用函数"的逻辑,但 Router 和 hyper 需要的是 tower::Service。HandlerService 就是这个适配器。
结构体定义
handler/service.rs:22-26:
rust
// handler/service.rs:22-26
pub struct HandlerService<H, T, S> {
handler: H,
state: S,
_marker: PhantomData<fn() -> T>,
}三个字段。handler 是实现了 Handler<T, S> 的类型。state 是应用状态。_marker 是幽灵数据,让 T 参数出现在类型签名中但不占用运行时空间。注意 PhantomData<fn() -> T> 而不是 PhantomData<T>——前者不暗示 HandlerService 拥有 T 类型的值,避免了不必要的 drop 检查和协变性约束。
为什么是 PhantomData<fn() -> T>
这个细节容易被忽略,却是理解 Rust 型变(variance)设计的好材料。PhantomData<T> 的型变性跟随 T:如果 T 协变,PhantomData<T> 就协变;如果 T 不变,PhantomData<T> 就不变。但 T 在这里是 Handler 的标记类型(((),)、(ViaRequest, T1, T2) 等),没有任何运行时含义,让它参与型变推导只会在嵌套场景下引起意料之外的子类型转换。
fn() -> T 是函数指针类型,本身是 Copy + Send + Sync,在 T 上协变但不暗示"结构体拥有 T"。这个差别还影响 drop check:PhantomData<T> 会让编译器认为 HandlerService 在 drop 时"可能访问 T 的内容",当 T 包含借用时会触发额外的生命周期约束;PhantomData<fn() -> T> 明确告诉编译器"这里只是标记,我不会 drop T",dropck 直接跳过。
如果 Axum 需要让 HandlerService 跨线程,fn() -> T 保留了 Send + Sync;如果想显式去掉两者,应该用 PhantomData<*const T>。Axum 选前者,因为 HandlerService 在 Tower 中间件栈中会跨线程移动。这是 Rust 生态里的惯用模式:所有"类型只出现在签名中但不存储"的场景,第一反应应该是 PhantomData<fn() -> T>——保留需要的 marker 语义,排除不想要的 dropck 与型变副作用。
Service 实现
handler/service.rs:146-176 是核心:
rust
// handler/service.rs:146-176
impl<H, T, S, B> Service<Request<B>> for HandlerService<H, T, S>
where
H: Handler<T, S> + Clone + Send + 'static,
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,
S: Clone + Send + Sync,
{
type Response = Response;
type Error = Infallible;
type Future = IntoServiceFuture<H::Future>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let req = req.map(Body::new);
let handler = self.handler.clone();
let future = Handler::call(handler, req, self.state.clone());
let future = future.map(Ok as _);
IntoServiceFuture::new(future)
}
}逐行分析 call:
req.map(Body::new):把Request<B>的 body 从泛型B转成 Axum 统一的Body类型。这一步是必要的,因为 hyper 传过来的 body 类型可能和 Axum 内部使用的不同。self.handler.clone():因为Handler::call(self, ...)按值消费 handler,所以每次调用前必须 clone。Handler的Clonebound 在这里派上用场。Handler::call(handler, req, self.state.clone()):调用 handler trait 的call方法,传入请求和状态。返回的H::Future输出类型是Response。future.map(Ok as _):把Future<Output = Response>映射成Future<Output = Result<Response, Infallible>>,匹配Service::Future的类型签名。Ok as _是一个函数指针强制转换——Ok本身的类型是fn(Response) -> Result<Response, Infallible>。IntoServiceFuture::new(future):包装成 opaque future 类型,隐藏内部实现细节。
Infallible 的保证
type Error = Infallible 是这个实现最重要的设计决策。Infallible 是 std::convert::Infallible——一个不可能构造值的 enum。Result<T, Infallible> 意味着"这个 Result 不可能是 Err"。编译器知道这一点,会对 match result { Ok(v) => ..., Err(e) => match e {} } 做穷尽性优化——Err 分支永远不会执行。
这个保证从何而来?从 Handler::Future: Future<Output = Response> 而不是 Future<Output = Result<Response, E>>。Handler 的输出就是 Response,不携带错误信息。所有错误(提取器拒绝、业务逻辑错误)都已经在上游被转换成了 Response。HandlerService::call 只是把 Response 包装成 Ok(Response),错误类型自然就是 Infallible。
Infallible 的保证向上传播:MethodRouter 的 Service impl 也返回 Error = Infallible,Router 亦然。最终 hyper 收到的 Service 的 Error 也是 Infallible。hyper 代码中不需要处理 handler 层面的错误——错误要么在 handler 里变成了响应,要么是不可能发生的 Infallible。这让 hyper 的 serve 实现可以免于 match err {} 的防御性代码,简化了整个调用链。
poll_ready:永远就绪
poll_ready 返回 Poll::Ready(Ok(()))——handler 永远就绪。注释(service.rs:159-162)解释了原因:
IntoServicecan only be constructed from async functions which are always ready, or fromLayeredwhich buffers in<Layered as Handler>::calland is therefore also always ready.
普通的 async fn handler 没有背压概念——它不维护内部缓冲区,不会因为"太忙"而拒绝请求。Layered handler 虽然包装了 Tower middleware,但 Layered::call 在调用时就立即启动 middleware 的 oneshot 调用,不会延迟到 poll_ready 阶段。
Layered handler:给 handler 穿上 Tower middleware
Handler::layer 方法让你给单个 handler 添加 Tower 中间件:
rust
async fn handler() { /* ... */ }
let layered = handler.layer(ConcurrencyLimitLayer::new(64));结构体
handler/mod.rs:285-289:
rust
pub struct Layered<L, H, T, S> {
layer: L,
handler: H,
_marker: PhantomData<fn() -> (T, S)>,
}L 是 Tower Layer 类型(如 ConcurrencyLimitLayer),H 是原始 handler,T 和 S 是 Handler 的标记类型和状态类型。
Handler impl for Layered
handler/mod.rs:317-350 展示了 Layered 如何实现 Handler:
rust
// handler/mod.rs:317-350(简化)
impl<H, S, T, L> Handler<T, S> for Layered<L, H, T, S>
where
L: Layer<HandlerService<H, T, S>> + Clone + Send + Sync + 'static,
H: Handler<T, S>,
L::Service: Service<Request, Error = Infallible> + Clone + Send + 'static,
<L::Service as Service<Request>>::Response: IntoResponse,
<L::Service as Service<Request>>::Future: Send,
{
type Future = future::LayeredFuture<L::Service>;
fn call(self, req: Request, state: S) -> Self::Future {
let svc = self.handler.with_state(state); // Handler -> HandlerService
let svc = self.layer.layer(svc); // HandlerService -> L::Service
let future = svc.oneshot(req).map(|result| match result {
Ok(res) => res.into_response(),
Err(err) => match err {}, // Infallible
});
future::LayeredFuture::new(future)
}
}执行流程是:
self.handler.with_state(state):把 Handler 转换成HandlerService(Handler -> Service)。self.layer.layer(svc):把HandlerService包裹在 Layer 中,得到L::Service(Service -> 中间件 Service)。svc.oneshot(req):调用 Tower 的Oneshot工具,对请求执行一次 Service 调用。oneshot内部先调用poll_ready(保证就绪),再调用call。.map(|result| match result { ... }):把Result<L::Response, Infallible>映射成Response。Err(err) => match err {}是 Infallible 的标准处理方式——因为Infallible没有变体,match err {}是穷尽的,编译器知道这个分支不可达。
注意 bound L::Service: Service<Request, Error = Infallible>——中间件 Service 的错误类型也必须是 Infallible。这意味着如果你用了可能产生错误的中间件(如 Timeout 中间件在超时时返回错误),你必须确保错误类型被转换成了 Infallible——通常是把超时错误映射成 HTTP 响应(比如 503 Service Unavailable),然后包装成 Error = Infallible 的 Service。
这个设计有一个微妙之处:Layered 本身也实现了 Handler。这意味着 layer 可以链式调用:
rust
handler
.layer(ConcurrencyLimitLayer::new(64))
.layer(TimeoutLayer::new(Duration::from_secs(10)))第一次 layer 返回 Layered<ConcurrencyLimitLayer, H, T, S>,它实现了 Handler<T, S>,所以可以再调用 layer,返回 Layered<TimeoutLayer, Layered<ConcurrencyLimitLayer, H, T, S>, T, S>。类型嵌套越来越深,但运行时就是一系列 Service 的嵌套调用。
HandlerWithoutStateExt:无状态 handler 的便捷方法
如果你的 handler 不需要状态(不用 State<S> 提取器),你可以用 HandlerWithoutStateExt 代替 Handler::with_state,省去传入 () 的麻烦。
handler/mod.rs:357-398:
rust
// handler/mod.rs:357-358
pub trait HandlerWithoutStateExt<T>: Handler<T, ()> {
fn into_service(self) -> HandlerService<Self, T, ()>;
fn into_make_service(self) -> IntoMakeService<HandlerService<Self, T, ()>>;
fn into_make_service_with_connect_info<C>(self)
-> IntoMakeServiceWithConnectInfo<HandlerService<Self, T, ()>, C>;
}实现(handler/mod.rs:380-398)极其简单:
rust
impl<H, T> HandlerWithoutStateExt<T> for H
where
H: Handler<T, ()>,
{
fn into_service(self) -> HandlerService<Self, T, ()> {
self.with_state(())
}
// ...
}into_service() 就是 with_state(()) 的别名。那为什么不直接用 with_state(())?因为 into_service 的名字更清晰地表达了意图——"把这个 handler 转换成 Service",而 with_state(()) 的语义是"提供空状态",对于不关心状态的调用者来说,前者更容易理解。
into_make_service() 和 into_make_service_with_connect_info() 用于直接将 handler 暴露为 TCP 服务,跳过 Router。使用场景是极简单的服务——只有一个 handler,没有路由:
rust
async fn hello() -> &'static str {
"Hello, World!"
}
// 不需要 Router,直接服务
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, hello.into_make_service()).await;all_the_tuples! 宏的工程细节
宏的展开模式
回看 all_the_tuples! 的定义(macros.rs:49-67),它把 impl_handler! 宏展开 16 次,每次增加一个参数:
rust
impl_handler!([], T1); // 1 参数
impl_handler!([T1], T2); // 2 参数
impl_handler!([T1, T2], T3); // 3 参数
// ...
impl_handler!([T1, ..., T15], T16); // 16 参数方括号里的 $($ty),* 是"前面的参数"(都实现 FromRequestParts),$last 是"最后一个参数"(实现 FromRequest)。这种展开模式在 Rust 生态中并不罕见——Serde 的 impl_tuple! 宏用同样的方式为 1-16 元素的元组实现 Serialize 和 Deserialize,在《Serde 元编程》第 7 章中我们会详细分析那个宏的实现。
Axum 和 Serde 的宏虽然在目的上完全不同——前者是为函数参数列表生成 handler impl,后者是为元组生成序列化 impl——但技术方案是一样的:用一个外层宏枚举元组长度,用一个内层宏为每种长度生成 impl。这不是巧合,而是 Rust 类型系统约束下的自然选择:Rust 不支持可变参数泛型(variadic generics),所以元组/参数列表的不同长度必须通过宏展开为不同的 impl 块。
为什么是 16 而不是更多
16 是 Axum 选择的参数上限。超过 16 个参数的 handler 必须重构——把多个提取器打包成一个结构体,为该结构体实现 FromRequestParts。这不是随意的限制。16 是 Rust 生态中的惯例数字——Serde 的元组 impl 也是 16,标准库的 Fn trait 也是到 FnOnce(A, B, ..., P) 共 16 个参数(在 2024 edition 之前)。选择 16 是在"覆盖绝大多数用例"和"控制编译时间"之间的平衡——每多一种参数长度,就多一个 blanket impl,编译器需要检查的类型组合就更多。
对错误信息的影响
Handler 的 T 参数还服务于一个不太明显的目标:改善错误信息。当 handler 的参数类型不匹配时,编译器需要告诉你"第几个参数有问题"。如果 T 只是 (),编译器无法区分零参数 handler 和多参数 handler——所有 blanket impl 都是为同一个 T 类型写的(如果允许的话),错误信息会非常混乱。
有了 T,不同的参数列表对应不同的 Handler<(M, T1, T2, ...) 类型,编译器在报错时至少能告诉你"期望的 Handler 类型是什么"。Axum 还提供了 #[debug_handler] 属性宏,它解析 T 的结构来生成更有可读性的错误信息——这在第 4 章提到过,下一章我们会深入提取器时再回来讨论它的实际效果。
#[debug_handler]:让编译器说人话
Handler trait 最痛苦的不是它本身的复杂性,而是它失败时的编译错误。假设你这样写:
rust
async fn create_user(
pool: PgPool, // 漏了 State(...)
Json(req): Json<CreateUserRequest>,
) -> impl IntoResponse { /* ... */ }
Router::new().route("/users", post(create_user));第一个参数忘了用 State(pool): State<PgPool> 包装。编译器会给出类似:
text
error[E0277]: the trait bound `fn(PgPool, Json<_>) -> impl Future {create_user}:
Handler<_, _>` is not satisfied然后列出 Handler 的 16 个 blanket impl 的候选,一条都不匹配,不告诉你到底是哪个参数不对。原因是 blanket impl 的 where 子句检查发生在 trait 解析的最后阶段,这个阶段 Rust 编译器已经失去了参数位置信息——它能说"没有哪个 impl 匹配",但说不出"第 1 个参数的类型没有实现 FromRequestParts"。
axum-macros 的 #[debug_handler] 宏就是为这个问题准备的。它展开后会在 handler 函数附近生成一个辅助函数,逐参数单独检查 FromRequestParts 或 FromRequest bound:
rust
#[axum::debug_handler]
async fn create_user(
pool: PgPool,
Json(req): Json<CreateUserRequest>,
) -> impl IntoResponse { /* ... */ }诊断会变成(简化后):
text
error: `PgPool: FromRequestParts<_>` is not satisfied
--> create_user 的第 1 个参数
help: maybe you meant `State<PgPool>`?逐参数定位、甚至给出修复建议。代价是编译时间略增(多生成一个检查函数),所以只在开发期使用,生产构建用 cfg_attr(debug_assertions, axum::debug_handler) 条件编译即可。
这个宏做的事情本质上是"把 Handler trait 的 bound 从最终 trait 解析阶段前置到函数签名检查阶段"——通过为每个参数生成一个"占位断言"函数,让每个参数的 bound 失败都能生成一条独立、精确的错误消息。这是一种工程化的技巧,不是语言特性,而是对 Rust 错误信息系统的巧妙利用。第 19 章会详细剖析 axum-macros 里这类"错误信息改善"宏的实现模式,并和 serde-macros 的 derive 错误诊断做对比。
全景:从 async fn 到 hyper 的完整调用链
现在让我们把所有片段拼在一起,追踪一个请求从到达 hyper 到 handler 返回响应的完整路径。
这条链的关键性质:
- Handler 内部的所有分支都输出
Response,不输出Result<Response, E>。无论是正常返回还是提取器拒绝,最终都是Response。 - HandlerService 的
Error = Infallible,这个性质在整个调用链上保持。 - 从
Handler::call到Service::call的唯一转换是map(Ok)——把Response包进Result<Response, Infallible>。这是零成本的——编译器会优化掉这个包装。 - clone handler 是唯一的运行时开销。
HandlerService::call每次 clone handler,对于闭包和函数指针来说,clone 就是复制一个函数指针或复制捕获的变量,开销极小。
性能剖析:从 hyper 到 handler 的每一步开销
前面一直在说 Handler 是"零成本抽象"。零成本不是没有成本,而是"功能等价的手写代码也必须付出的成本"。具体量化一下一个典型请求从 hyper 到 handler 的完整开销。
以 async fn create_user(Path(id): Path<u32>, State(pool): State<Arc<PgPool>>, Json(req): Json<CreateRequest>) -> Json<Response> 为例:
| 步骤 | 性质 | 大致量级 |
|---|---|---|
1. hyper 调用 Router::call(BoxCloneService) | 一次虚函数跳转 | ~1 ns |
| 2. Router 路径匹配(matchit trie) | 哈希 + 字节比较 | 100 ns ~ 1 µs |
3. MethodRouter::call | 直接泛型分发 | 编译期消除 |
| 4. 方法匹配(enum switch) | 一次分支判断 | < 1 ns |
5. HandlerService::call clone handler + state | 两次 Clone(ZST 或 Arc::clone) | ~10 ns |
6. Handler::call 拆请求为 Parts + Body | move,零指令 | 0 |
7. Path::from_request_parts | URI 解析 + u32 parse | ~100 ns |
8. State::from_request_parts | extensions HashMap 查找 + Arc::clone | ~50 ns |
9. 重建 Request + Json::from_request | 读 body + serde_json::from_slice | 取决于 body |
10. self(...) 业务逻辑 | 数据库 IO、计算 | µs ~ ms |
11. .into_response() | Json<Resp> 序列化 | 取决于 resp |
12. map(Ok) 包装 | 编译期消除 | 0 |
框架固有开销(1-8 加 12)累计约 1-5 µs;业务开销(9-11)从几十 µs 到数毫秒。框架开销占比通常在个位数百分点。这也是 TechEmpower 基准测试里 axum 相对裸 hyper 的性能损失能控制在 5% 以内的原因——只要业务有任何实际计算或 IO,框架抽象的成本就几乎不可见。
真正值得关注的潜在热点只有两处。
Box<dyn Future> 的堆分配。Handler::Future = Pin<Box<dyn Future>>,每次 call 都有一次堆分配。这和 Rust 生态普遍的"async-trait 需要 Box"同源——当你需要对多个返回不同 Future 类型的函数做类型擦除,Box<dyn Future> 是当前稳定 Rust 里唯一的选择(在 trait async fn 稳定并支持 impl Trait 返回之前)。对绝大多数场景可以忽略,对极低延迟(p99 < 10 µs)的特殊场景会显出来。绕过它的唯一办法是跳过 Handler,直接写 impl Service<Request> 并手动实现 poll——但那样就失去了 Handler 自动提取参数的便利。这是 Axum 的设计立场:如果你苛刻到这一点 Box 分配都不能忍,那你也愿意自己写 Service。
大闭包捕获下的 clone。Handler 是带大量捕获的闭包时,clone 不再零成本:
rust
let huge_config = vec![0u8; 10 * 1024 * 1024]; // 10 MB
let handler = move |req| async move { /* 用 huge_config */ };
// 每次请求 clone 整个 10 MB vec——灾难正确写法是让闭包只捕获指针:
rust
let huge_config = Arc::new(vec![0u8; 10 * 1024 * 1024]);
let handler = move |req| {
let cfg = Arc::clone(&huge_config);
async move { /* 用 cfg */ }
};
// 每次请求 clone 一个 Arc——原子加一#[debug_handler] 不会对闭包捕获大小发出警告——它的检查发生在类型层,看不到运行时内存布局。这依赖工程自觉:handler 的闭包捕获要么是 Copy 的小值,要么是 Arc / Rc 的指针。凡是可能长尾增长的数据结构(配置表、缓存、路由信息),都应该在 Arc 之内,而不是在闭包之内。这条规则对《Tokio 源码深度解析》第 4 章讨论的 "tokio::spawn 的 future 必须 'static" 场景也适用——两个问题共享同一条工程直觉:框架强制的所有权模型会把 handler 和 future 推向"指针化捕获"的最佳实践。
为什么 Handler 返回 Response 而不是 Result
这是一个值得单独讨论的设计决策。很多 Web 框架(包括 actix-web)允许 handler 返回 Result<Response, Error>,错误通过 ? 操作符传播到框架层统一处理。Axum 选择了另一条路:handler 的 Future 输出是 Response,所有错误必须在 handler 内部被转换成响应。
这个选择有几个理由:
第一,简化 Service 合约。 tower::Service 的 Error 类型是关联类型,不同的 Service 可以有不同的 Error。如果 handler 返回 Result<Response, E>,那么 E 必须出现在 HandlerService 的类型签名中,进而出现在 MethodRouter 和 Router 的类型签名中。当多个 handler 有不同的错误类型时,Router 需要用 BoxError 或 trait object 来统一它们,引入动态分发和堆分配。Axum 的选择——Error = Infallible——消除了这个问题:所有 handler 的 Service 都有相同的错误类型,Router 可以用泛型参数 E = Infallible 统一处理。
第二,契合 HTTP 语义。 HTTP 协议里没有"错误"——只有响应。4xx 是客户端错误响应,5xx 是服务端错误响应,但它们都是合法的 HTTP 响应。把"错误"和"成功响应"统一成 Response,更准确地反映了 HTTP 的本质。Handler 不应该"抛出异常",它应该"返回一个合适的 HTTP 响应"。
第三,和 Tower 生态兼容。 Tower 的很多中间件(如 Retry、LoadShed)依赖 Service::Error 来决定是否重试或降级。如果 Error = Infallible,这些中间件知道"Service 不可能失败",可以跳过错误处理路径。如果 Error 是某种应用错误类型,中间件需要理解这个类型才能做出正确的决策——这破坏了中间件和业务逻辑之间的解耦。
但这个选择也有代价:handler 内部的错误处理不够优雅。你不能直接在 handler 里用 ? 传播错误到框架层——你必须把错误转换成 Response,通常通过 impl IntoResponse for Result<T, E> 的 blanket impl。Axum 提供了 Result<impl IntoResponse, impl IntoResponse> 的支持,让你可以用 ? 在 handler 内部传播错误,但最终 Result 还是在 handler 内部被消费掉了。
与 Serde impl_tuple! 的对比:同一模式,不同语义
前面提到 Axum 的 all_the_tuples! 和 Serde 的 impl_tuple! 使用了相同的技术模式。值得对比一下它们的异同。
相同点:
- 都用宏为 1-16 个元素的元组/参数列表生成 blanket impl。
- 都是为了绕过 Rust 缺少可变参数泛型的限制。
- 标记类型都在编译期区分不同长度的参数列表,运行时零开销。
不同点:
- 语义层面:Serde 的
impl_tuple!为元组类型本身实现Serialize/Deserialize,元组就是数据。Axum 的impl_handler!为函数类型实现Handler,函数的参数列表是签名的一部分。 - 标记类型的角色:Serde 不需要额外的标记类型——元组
(A, B, C)的类型本身就编码了长度。Axum 需要T参数作为标记,因为Fn(A, B, C) -> Fut和Fn(D, E) -> Fut可能由同一个F实现(理论上),需要T来区分。 - 最后一个参数的特殊处理:Serde 对元组的所有元素一视同仁。Axum 必须区分
FromRequestParts和FromRequest——最后一个参数可以消费请求体,前面的参数不能。 - 提取顺序的约束:Serde 的元组序列化/反序列化是位置无关的(所有元素独立处理)。Axum 的提取器有严格的顺序依赖——
FromRequestParts修改&mut Parts,后面的提取器看到的是被前一个修改后的Parts。
这个对比揭示了一个更深层的设计原则:当宏生成的代码需要处理元素之间的交互(如共享可变状态 &mut Parts),复杂性会显著高于元素之间独立的场景。Axum 的 impl_handler! 宏虽然展开后的代码看起来简单,但"先提取 Parts 再提取 Body"的顺序约束、提取失败时的短路返回、M 参数的一致性变通——这些都是"元素之间有交互"带来的额外复杂性。
Handler trait 的边界:什么它做不了
理解 Handler 做了什么很重要,理解它不做什么同样重要。
Handler 不做路由匹配。 Handler::call 接收的 Request 已经是路由匹配后的请求。路径参数(Path<T>)的值在请求的 URI 中,但"哪个 handler 处理这个路径"是 Router 的职责。Handler 只负责"从这个请求中提取我需要的参数"。
Handler 不做方法匹配。 一个 handler 只对应一个 HTTP 方法。方法匹配是 MethodRouter 的职责——它决定 GET 请求走哪个 handler、POST 请求走哪个 handler。
Handler 不做中间件编排。 Handler::layer 是单 handler 的中间件,不是全局中间件。Router 级别的中间件通过 Router::layer 添加,作用域更广。Handler 的 layer 适合那种"只对这一个端点生效"的中间件,比如"只对上传接口限流"。
Handler 不处理共享状态的初始化。 状态的创建和注入发生在 with_state 调用时——这是 Router 层面的操作。Handler 只接收一个 state: S 参数,它不关心这个状态从哪来。
这些边界不是 Handler 的缺陷,而是职责划分的结果。Handler 的唯一职责是:给定一个 Request 和一个 State,提取参数、调用函数、返回 Response。这个职责定义得足够窄,所以实现可以做到足够简单和正确。
什么时候不用 Handler:直接写 Service
Router::route_service 方法允许把一个 tower::Service 直接挂到路由上,跳过 Handler:
rust
use tower::service_fn;
let svc = service_fn(|req: Request| async move {
Ok::<_, Infallible>(Response::new(Body::from("hello")))
});
Router::new().route_service("/raw", svc);既然 Handler 这么好用,什么情况下需要跳过它、直接写 Service?三种典型场景:
一、需要手动管理 poll_ready 背压。 Handler 的 Service 适配器永远 Poll::Ready(Ok(())),不做背压。如果你想在处理请求前根据系统负载主动拒绝——比如数据库连接池满了就返回 503——你需要一个有状态的 Service,poll_ready 检查连接池、call 消费连接。Handler 层做不到这件事:提取器在 call 内部执行,poll_ready 阶段拿不到请求信息。
二、需要避免 Box<dyn Future> 的堆分配。 前面性能剖析提到了这一点。手写的 Service 可以返回具体的 Future 类型(impl Future),避免每次 call 都堆分配。这在 SLA 严苛的 API 网关、代理服务、或者 p99 < 10 µs 的场景下有意义——但对 p99 在毫秒级的业务 API 是过度优化。
三、要和非 axum 的 Service 库互操作。 tower 生态有大量现成 Service——tower-http::ServeDir 是静态文件服务,tonic 的 gRPC Service,tower::steer::Steer 的负载均衡——它们都是 Service<Request>,直接 route_service 挂上去即可,不需要经过 Handler 包装。特别是 tonic:一个 gRPC 方法本质是一个 Service,塞进 axum Router 后可以和 REST handler 共存在同一个端口上。
Handler 是"类型化的参数提取 + 易用性",Service 是"完全控制的生命周期 + 与 Tower 生态无缝互操作"。两者不互斥——同一个 Router 里可以混用。当你不需要 Handler 提供的提取便利时,直接写 Service 反而更清晰。这种"开放的抽象"是 Axum 架构的重要特性:它不强迫你只能用它的抽象,而是让你在任何一层都能向下拆解到更底层的接口。这个哲学会在第 13 章讲中间件、第 15 章讲 Serve 时反复出现。
下一章我们将深入提取器的实现——FromRequestParts 和 FromRequest 如何从 Request 中提取出 Path<u32>、State<Pool>、Json<Payload> 这些类型化的值,以及提取失败时如何生成拒绝响应。