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 栈这三个专栏之后:
- 《Rust 编译器与运行时揭秘》专栏:讲编译器如何把你写的
async fn展开成状态机。 - 《Tokio 源码深度解析》专栏:讲运行时如何把状态机调度到线程与 I/O 之上。
- 《Hyper 与 Tower:工业级 HTTP 栈》专栏:讲 HTTP 协议栈如何把字节流翻译成请求与响应,讲 Service / Layer 抽象如何统一中间件。
- 《Axum 设计与实现》专栏(就是这一个):讲一个 Web 框架如何建立在 Hyper + Tower 之上,把"写一个 HTTP handler"这件事做到极致简洁——同时保留在任意层级向下拆解的能力。
flowchart TB
REQ[HTTP 请求] --> RT
subgraph RT [Router<S> · 类型状态 · 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/>借 &mut Parts · 可多个"] --> FR[FromRequest<br/>消费 Request · 只能一个]
end
EXT --> H["Handler<T, S><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 的提取器只能放在参数列表最后一个。理解了这一条,那些"编译器报了一大段看不懂的错"的时刻就消失了。
读完你能做到什么
- 看懂
Router的类型状态。Router<S>为什么要带状态泛型、为什么用Arc包裹、PathRouter 的双层索引(matchit 基数树 + 双向 HashMap)、一次路径匹配的完整流程(第2章 Router、第3章 MethodRouter、第4章 嵌套与合并)。 - 解释"为什么我的函数不是 handler"。
Handler<T, S>的定义、all_the_tuples!怎么把多参数展开、从 Handler 到 Service 的适配器(第5章 Handler)。 - 写自己的提取器。 两个 trait 的分工与那条 body 物理约束、Rejection 与 IntoResponse 的硬契约、
Result<T, E>用 Infallible 把拒绝变成值、Option<T>的语义、元组提取器(第6章 FromRequest、第7章 内置提取器、第8章 高级提取器)。 - 控制响应。 IntoResponse 的实现面、响应类型与响应各部分的组装(第9章 IntoResponse、第10章 响应类型、第11章 响应部件)。
- 搞清 axum 的错误哲学。 为什么坚持
Infallible、哪些 Service 真的会失败、HandleError的源码、BoxError与 downcast 的陷阱,以及该监控哪些指标(第12章 错误处理)。 - 写中间件并知道什么时候不该用 from_fn。
Next这个抽象、签名要求、from_fn_with_state,以及它与tower::Layer的分界(第13章 函数式中间件、第14章 map 系列)。 - 组织好 state 与 body。
FromRef的子状态抽取、多字段 state 与#[derive(FromRef)]、嵌套 FromRef,以及 Body 的类型擦除与try_downcast优化、流式构造(第18章 State、第17章 Body 与流式)。 - 上生产。 serve 与监听器/执行器、宏、axum-extra、测试与生产实践(第15章 serve、第16章 监听器、第19章 宏、第20章 extra、第21章 测试、第22章 生产)。
适合谁读
- Rust Web 开发者:每天写 Axum handler,想理解
#[debug_handler]报的那些错到底是什么意思、State和Extension有什么区别、为什么Body只能被消费一次。 - 框架/库作者:准备写自己的提取器、中间件、或 axum 生态库,想从源码层面理解 axum-core 的 trait 设计约束。
- 性能工程师:在生产环境遇到路由匹配热点、中间件栈过深、连接泄漏、优雅关闭不干净等问题,需要从框架源码找根因。
- 读过《Hyper 与 Tower》的读者:已经理解了
Service/Layer/Body心智模型,想看这些抽象在真实 Web 框架中如何落地。 - Rust 语言爱好者:对类型状态、零成本抽象、宏元编程在工业项目中的运用感兴趣。
前置知识:本专栏假设读者熟悉 Rust 的 trait、泛型、生命周期、Pin、Future 与 async/await。建议先阅读《Hyper 与 Tower:工业级 HTTP 栈》专栏——本专栏不会重复解释 Service / Layer / Body 这些概念,但会在每一处与 Hyper+Tower 心智模型相关的地方给出章节索引。
目录
开篇
第一部分:路由系统
第二部分:Handler 与提取器
- 第5章 Handler trait:从 async fn 到 tower::Service
- 第6章 FromRequest 与 FromRequestParts:提取器的所有权分裂
- 第7章 内置提取器:Path、Query、State、Json
- 第8章 高级提取器:WebSocket、Multipart、ConnectInfo
第三部分:响应与错误
- 第9章 IntoResponse:构建 HTTP 响应的统一接口
- 第10章 响应类型实战:JSON、HTML、Redirect、SSE
- 第11章 IntoResponseParts 与元组响应:类型级响应组合
- 第12章 错误处理模型:Infallible 与 HandleError
第四部分:中间件
第五部分:运行与状态
- 第15章 Serve:监听、接受连接与优雅关闭
- 第16章 Listener 与 Executor:可插拔的传输与调度
- 第17章 Body 处理与流式响应
- 第18章 State 管理与 FromRef 子状态提取
第六部分:元编程与扩展
- 第19章 axum-macros:debug_handler、FromRequest derive、TypedPath
- 第20章 axum-extra:Typed 路由、Cookie、Protobuf、Either
第七部分:工程实践
源码版本
本专栏所有源码引用以 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 主要有:
serve::Executor与Serve::with_executor(#3704)、ListenerExt::limit_connections(#3489)axum::serve默认启用 hyper 的header_read_timeout(#3478)、future 的Output去掉io::Result(#3601)、serve增加 body 类型泛型参数(#3205)#[from_request(via(...))]改用被代理提取器自身的 rejection 类型(#3261)IntoResponse/IntoResponseParts上的#[diagnostic::on_unimplemented]提示(#3723)IntoResponseFailed/ForceStatusCode失败标记(#3603,CHANGELOG 的Unreleased段未列这一条,它在 0.8.9 的发版提交里被 revert 掉(99068f5),主线上仍在;源码注释称其为 "a behavior introduced with axum 0.9")MethodRouter::method_filter与route_with_tsr的方法过滤(#3586)、嵌套 Router 的 fallback 合并修正(#3158)、matchit 升级带来的「参数 + 静态前后缀」路径(#3702)Query/Form的反序列化从serde_urlencoded换成serde_html_form(#3594)- axum-extra 移除已弃用的
Host/Scheme/OptionalPath提取器(#3599)
跨仓对照用的版本:正文有若干处拿别的 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
正文的源码引用都标注了文件路径,绝大多数附有行号,读者可在对应版本的代码中逐行验证。
与其他专栏的关联
- 前置:《Hyper 与 Tower:工业级 HTTP 栈》专栏第 2-4 章(Service / Layer / poll_ready)、第 9-10 章(http crate / http-body)、第 12 章(Dispatcher)、第 19 章(hyper-util)——本专栏直接使用这些概念,不再重复解释。
- 前置:《Tokio 源码深度解析》专栏第 4-7 章(Runtime / Task)、第 8-10 章(I/O Driver)——
serve()内部的连接接受和任务调度完全运行在 Tokio 之上。 - 前置:《Rust 编译器与运行时揭秘》专栏第 9 章(async 状态机)——第 5 章 Handler trait 的 blanket impl 涉及复杂的 Future 类型推导,理解 async 状态机展开有助于理解编译器为什么能接受这些 impl。
- 平行:《Serde 元编程》——Handler 的
all_the_tuples!宏与 Serde 的tuple_impls!宏共享同一套"用宏为元组实现 trait"的模式;axum-macros 的 derive 宏与 serde_derive 的 derive 宏在设计哲学上一致。 - 交叉:《LangGraph 设计与实现》专栏第 3 章——LangGraph 的节点与 Axum 的 Handler 共享"函数即节点"的设计哲学,两者都通过 trait 把用户函数接入框架。
版权声明
本专栏内容为 杨艺韬 版权所有,保留一切权利。未经书面许可,不得全文或大段转载、改编、翻译,或用于任何商业用途(含以本专栏内容训练模型、生成衍生课程或商品)。
欢迎分享本专栏的链接。引用少量内容用于评论、教学或研究时,请署名 杨艺韬 并附上原文链接。