Axum 设计与实现

从源码视角剖析 Rust Web 框架 Axum。

打开任何一个 Rust 后端项目的 src/main.rs,你几乎都会看到这样的代码:

let app = Router::new()
    .route("/", get(handler))
    .with_state(state);

let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
serve(listener, app).await.unwrap();

三行代码,一个 HTTP 服务就跑起来了。路由、提取、响应、中间件——框架帮你把这些全部安排妥当。但当你想给路由加个前缀、想让某个提取器返回自定义错误、想在 serve 之外接管连接生命周期、想搞清楚 Handler<T, S> 那个幽灵参数 T 到底是什么……你就得往下挖了。

这个专栏的目的,就是把这三行代码底下的每一层都打开给你看。

你会看到 Router<S> 如何用类型状态模式在编译期强制你提供 State;你会看到 matchit 如何用基数树完成路径匹配;你会看到 Handler<T, S> 如何用一个宏把最多 16 个参数的 async fn 展开成 tower::Service;你会看到 FromRequest 与 FromRequestParts 的分裂为什么不是设计冗余而是 Body 所有权约束的必然结果;你会看到 from_fn 中间件的 Next 如何把剩余的中间件栈类型擦除成一个值——调用 next.run(req) 就继续往里走,不调用就短路;你会看到 Infallible 错误模型如何让错误永远不会逃逸到 hyper;你会看到 Serve 如何用 tokio::select! 实现优雅关闭。

这是 Rust 后端系列的第五个专栏,也是 async 这条主线上的第四环,紧接在编译器 → 运行时 → HTTP 栈这三个专栏之后:

flowchart TB
  REQ[HTTP 请求] --> RT
  subgraph RT [Router&lt;S&gt; · 类型状态 · Arc 包裹]
    direction LR
    PR[PathRouter<br/>matchit 基数树] --> MR[MethodRouter<br/>按方法分发]
  end
  RT --> MW[中间件栈<br/>tower::Layer / from_fn]
  MW --> EXT
  subgraph EXT [提取器 · 一条物理约束]
    direction LR
    FP["FromRequestParts<br/>借 &amp;mut Parts · 可多个"] --> FR[FromRequest<br/>消费 Request · 只能一个]
  end
  EXT --> H["Handler&lt;T, S&gt;<br/>all_the_tuples! 展开"]
  H --> IR[IntoResponse]
  IR --> RES[HTTP 响应]
  ST[AppState<br/>FromRef 子状态抽取] -.注入.-> EXT
  ERR[HandleError<br/>为什么坚持 Infallible] -.兜底.-> MW

这张图里最值钱的一条是提取器那一格。 body 在协议层是一次性的——这条物理约束直接决定了 axum 为什么要分 FromRequestParts 与 FromRequest 两个 trait、为什么消费 body 的提取器只能放在参数列表最后一个。理解了这一条,那些"编译器报了一大段看不懂的错"的时刻就消失了。

读完你能做到什么

  1. 看懂 Router 的类型状态。 Router<S> 为什么要带状态泛型、为什么用 Arc 包裹、PathRouter 的双层索引(matchit 基数树 + 双向 HashMap)、一次路径匹配的完整流程(第2章 Router、第3章 MethodRouter、第4章 嵌套与合并)。
  2. 解释"为什么我的函数不是 handler"。 Handler<T, S> 的定义、all_the_tuples! 怎么把多参数展开、从 Handler 到 Service 的适配器(第5章 Handler)。
  3. 写自己的提取器。 两个 trait 的分工与那条 body 物理约束、Rejection 与 IntoResponse 的硬契约、Result<T, E> 用 Infallible 把拒绝变成值、Option<T> 的语义、元组提取器(第6章 FromRequest、第7章 内置提取器、第8章 高级提取器)。
  4. 控制响应。 IntoResponse 的实现面、响应类型与响应各部分的组装(第9章 IntoResponse、第10章 响应类型、第11章 响应部件)。
  5. 搞清 axum 的错误哲学。 为什么坚持 Infallible、哪些 Service 真的会失败、HandleError 的源码、BoxError 与 downcast 的陷阱,以及该监控哪些指标(第12章 错误处理)。
  6. 写中间件并知道什么时候不该用 from_fn。 Next 这个抽象、签名要求、from_fn_with_state,以及它与 tower::Layer 的分界(第13章 函数式中间件、第14章 map 系列)。
  7. 组织好 state 与 body。 FromRef 的子状态抽取、多字段 state 与 #[derive(FromRef)]、嵌套 FromRef,以及 Body 的类型擦除与 try_downcast 优化、流式构造(第18章 State、第17章 Body 与流式)。
  8. 上生产。 serve 与监听器/执行器、宏、axum-extra、测试与生产实践(第15章 serve、第16章 监听器、第19章 宏、第20章 extra、第21章 测试、第22章 生产)。

适合谁读

前置知识:本专栏假设读者熟悉 Rust 的 trait、泛型、生命周期、Pin、Future 与 async/await。建议先阅读《Hyper 与 Tower:工业级 HTTP 栈》专栏——本专栏不会重复解释 Service / Layer / Body 这些概念,但会在每一处与 Hyper+Tower 心智模型相关的地方给出章节索引。

目录

开篇

第一部分:路由系统

第二部分:Handler 与提取器

第三部分:响应与错误

第四部分:中间件

第五部分:运行与状态

第六部分:元编程与扩展

第七部分:工程实践

源码版本

本专栏所有源码引用以 commit de9f13d 的工作树为准(2026 年 4 月 23 日锁定)。这是 axum 仓库 main 分支上的一个快照,各 crate 的 Cargo.toml 版本字符串如下表:

Crate 工作树版本号 Git Commit
axum 0.8.9 de9f13d
axum-core 0.5.6 de9f13d
axum-macros 0.5.1 de9f13d
axum-extra 0.12.6 de9f13d

需要留意的一点:main 分支上的 Cargo.toml 在下一次发版之前一直写着上一个版本号,所以上表的版本字符串与 crates.io 上同名版本相同,内容却不同——de9f13d 领先 crates.io 的 0.8.9 若干提交(git describe --tags 给出 axum-core-v0.5.6-103-gde9f13d),axum / axum-macros / axum-extra 三个 crate 的 CHANGELOG 在这个 commit 上都还挂着 Unreleased 段。尚未发布的 API 主要有:

跨仓对照用的版本:正文有若干处拿别的 crate 作对照(Serde 的 tuple_impls!、 tokio 的 TcpListener::bind、#[sqlx::test] 的建库策略、tower / tower-http 的各种 Layer), 这些路径与行号统一按下表核对,与对应专栏共用同一份 checkout:

Crate 版本 出现在
tower / tower-service 0.5.3 / 0.3.3 第 1、2、3、5、9、12、13、14、15、20、21 章
tower-http 0.6.8 第 1、9、12、13、14、17、22 章
hyper 1.9.0 第 2、5、16、17 章
http 1.4.0 第 9、11 章
http-body / http-body-util 1.0.1 / 0.1.3 第 9、17 章
serde / serde_core 1.0.228 第 5 章
tokio 1.46.1 第 3、10、15、16 章
sqlx 0.8.6 第 6、21 章

正文凡涉及这些尚未发布的 API,都会就地注明它只在主线上、未包含在 0.8.9 里;没有标注的内容即为 0.8.9 发布版同样成立。另外,路径匹配库 matchit 在这个 commit 上被精确锁定为 =0.9.2(axum/Cargo.toml:114),正文所说的「0.8+ 花括号语法」指的是 matchit 从 0.8 起启用、并一直沿用到 0.9.x 的那套语法。

读者可通过以下命令获取与本专栏完全一致的源码:

git clone https://github.com/tokio-rs/axum.git
cd axum && git checkout de9f13d

正文的源码引用都标注了文件路径,绝大多数附有行号,读者可在对应版本的代码中逐行验证。

与其他专栏的关联

版权声明

本专栏内容为 杨艺韬 版权所有,保留一切权利。未经书面许可,不得全文或大段转载、改编、翻译,或用于任何商业用途(含以本专栏内容训练模型、生成衍生课程或商品)。

欢迎分享本专栏的链接。引用少量内容用于评论、教学或研究时,请署名 杨艺韬 并附上原文链接。