Rust in Everything
基于 axum 0.8.9

怎么读这一卷

主线位置:两条主线的总览。本页把装配主线和请求主线各画一遍,标出每一步落在哪个函数上;下一部分的「一个请求穿过 axum」用一个程序把两条线同时跑一遍。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1[01 全景]
  use1 --> use2[02 Handler 与提取器]
  use2 --> use3[04 路由]
  use3 --> use4[10 常见坑]
  q -->|读懂实现| all[00 到 12 按顺序]

锁定版本

全卷只对着一组版本写:

项值
crateaxum 0.8.9,axum-core 0.5.6,axum-macros 0.5.1,tower-http 0.7.1
tagaxum-v0.8.9(axum、axum-macros),axum-core-v0.5.6,tower-http-0.7.1
commitaxum c59208c8(2026-04-06),axum-core e710a97a(2025-12-26),tower-http c941451
路由树matchit 0.8.4(axum 的 Cargo.toml 写死了 =0.8.4)
实验用的底层hyper 1.12.0、hyper-util 0.1.21、tower 0.5.3、tokio 1.54.1,和前三卷相同

源码链接都用 tag,形如 https://github.com/tokio-rs/axum/blob/axum-v0.8.9/axum/src/...#L123;axum-core 的链接用 axum-core-v0.5.6,tower-http 的用 tower-http-0.7.1。matchit、serde_path_to_error 这些不在两个仓库里的依赖只写 crate 名和版本。

axum-core 为什么按 0.5.6

axum 和 axum-core 在同一个仓库里,按理 axum-v0.8.9 这个 tag 下的 axum-core/ 就是配套的版本。但这个 tag 的最后一个提交叫「revert axum-core changelog changes」,tag 里 axum-core/Cargo.toml 写的是 version = "0.5.5"。而 axum 对它的依赖是 axum-core = { path = "../axum-core", version = "0.5.5" },是个 ^0.5.5 的要求;crates.io 上已经有 0.5.6(2025-12-26 发布),Cargo 解析出来的就是它。你的项目里真正编译的 axum-core 是 0.5.6。

两份源码差得很少。git diff axum-v0.8.9 axum-core-v0.5.6 -- axum-core 是 6 个文件、增删各 29 行,其中 Cargo.toml、CHANGELOG.md、LICENSE 占了大头;src/ 下只有三个文件不同:0.5.6 在 option.rs、request_parts.rs 里多了 4 行 #[allow(clippy::use_self)],tag 里的 from_ref.rs 文档多一个指向「Sharing state with handlers」的链接。没有一行行为上的差别。本卷讲 axum-core 时行号一律从 axum-core-v0.5.6 取。

仓库的 main 已经走过了 tag

写作时 axum 仓库的 main 比 axum-v0.8.9 多 298 个提交,其中有几个直接改了本卷要讲的东西:去掉 0.7 路径语法检查(#3889,「remove v07 path checks」)、把 tower-log 从默认 feature 里拿掉(#3928)、合并进来的服务路由不再多包一层 Route(#3909)。这些都不在本卷的版本里,读 main 会看到对不上的代码和行号。

本站自己锁的是 axum 0.8.9、axum-core 0.5.6,和本卷相同;tower-http 锁的是 0.6.11,比本卷旧一个大版本,差异放在「在本站中」一部分讲。

两条主线

axum 的工作分在两个时刻:启动时把一堆 async fn 装配成一棵 tower 服务树,运行时把每个 Request 送进这棵树。全卷沿这两条线展开,每页开头都会标出本页在哪条主线上。

装配主线(启动时,类型在变):

flowchart TB
  f["async fn handler"] --> h["impl Handler T S"]
  h --> mr["get:MethodRouter S"]
  mr --> rt["Router::route:插进 matchit"]
  rt --> ly["Router::layer:套中间件"]
  ly --> ws["with_state:Router S 变 Router"]
  ws --> sv["axum::serve"]
  1. 一个 async fn 本身不实现任何 axum 的 trait。impl_handler! 由 all_the_tuples! 展开,为「参数都是提取器、返回值实现 IntoResponse」的函数实现 Handler<T, S>。T 是参数类型组成的标记,S 是状态类型。见 Handler trait。
  2. get(handler) 调用 MethodRouter::on,把 handler 包成 BoxedIntoRoute(擦掉具体类型、但还没拿到状态)放进 get 这个方法槽,同时记下 Allow 头要写的 GET,HEAD。
  3. Router::route 校验路径,给它分配一个 RouteId,把「路径 → RouteId」插进 matchit 的路由树,「RouteId → 端点」放进一张 HashMap。路径冲突在这一步 panic。
  4. Router::layer 给已经加进来的每个端点和 fallback 套上一个 tower Layer。已经是服务的端点当场套上;还欠着状态的 handler 只把「怎么套」记下来,等拿到状态再套。
  5. Router::with_state 把状态交给每个方法槽:BoxedIntoRoute 变成 Route,也就是一个 BoxCloneSyncService;Router<S> 变成 Router<()>。只有 Router<()> 实现了 Service<Request>,才能交给 axum::serve。
  6. axum::serve 的 accept 循环、hyper-util 的 auto::Builder、TowerToHyperService 都在 hyper 卷讲过,见 hyper-util 的 auto 和 本站的服务端路径。

请求主线(每个请求,值在流动):

flowchart TB
  rc["Router::call"] --> pr["PathRouter:matchit 匹配"]
  pr -->|没匹配上| fb["fallback:默认 404"]
  pr -->|匹配上| mrc["MethodRouter:按方法选"]
  mrc -->|没有这个方法| e405["405,带 Allow"]
  mrc --> route["Route:装箱的服务"]
  route --> hc["Handler::call"]
  hc --> ex["提取器依次执行"]
  ex -->|失败| rej["rejection 转成响应"]
  ex --> fn["调用你的函数"]
  fn --> ir["into_response"]
  1. Router<()> 的 call 把 body 统一成 axum::body::Body,进 call_with_state。
  2. PathRouter::call_with_state 用 matchit 匹配路径:匹配上,就把路径参数存进请求的 extensions(UrlParams,Path 提取器之后从这里取),开着 matched-path 时再存一份 MatchedPath;没匹配上,交给 fallback,默认是 404。
  3. MethodRouter::call_with_state 按方法挑方法槽,HEAD 没有专门的端点就用 GET 的;没有对应的方法,走方法级的 fallback,默认回 405 Method Not Allowed 并带上 Allow 头。
  4. 选中的 Route 克隆一份,用 tower 的 oneshot 调用。里面最内层是 HandlerService,它的 call 克隆 handler 和状态,调用 Handler::call。
  5. Handler::call 把请求拆成 Parts 和 body,前 n-1 个参数依次跑 FromRequestParts(只能看 Parts),最后一个参数跑 FromRequest(可以拿走 body)。任何一个失败,就把它的 rejection into_response() 直接返回,后面的提取器和你的函数都不会执行。见 FromRequestParts 与 FromRequest。
  6. 你的函数返回后,返回值 into_response() 成 Response,沿原路交回 hyper。见 IntoResponse。

两条线的交点是 Route:装配主线的终点是把每个端点都变成 Route,请求主线在路由之后调用的也正是它。.layer 套上的中间件包在 Route 外面,所以它们在路由之后、提取器之前执行。

阅读路线

  • 只想会用:读 01 全景、02 Handler 与提取器、04 路由,以及常见坑。读完能看懂 Handler<_, _> 不满足这类报错从哪来,知道状态、提取器、中间件各在哪一步起作用。
  • 想读懂实现:按顺序读。02 是重点,Handler 的宏实现、两种提取器 trait、State 的类型参数都在那里;04、05 讲路由树和中间件怎么包在 Route 外面;09 把各章的取舍汇总。

摘自 axum、axum-core、tower-http 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。每个示例上方写出它需要的依赖和 feature,只开用到的那几个。实验的平台是 M2 Max、rustc 1.99.0。

前置知识

这一卷默认你会写 async Rust,也写过一两个 axum 的 hello world。axum 的大部分机制直接建在 tower 和 hyper 上,用到的东西在前几卷讲过,这里只链过去:

概念讲它的页本卷用到它的地方
tower 的 Service 与 poll_readyService traitRouter、MethodRouter、Route、HandlerService 都是 Service,poll_ready 一律直接返回 Ready
LayerLayer 与 StackRouter::layer、from_fn
层的顺序ServiceBuilder、层的顺序反了连续调用 .layer 时,后加的在外层
BoxCloneSyncService类型擦除Route 就是它,Router 因此能装任意 handler、还能克隆
oneshotServiceExt 与 OneshotRoute 每次调用都是一次 oneshot;测试里直接 router.oneshot(req)
MakeServiceSteer 与 MakeServiceaxum::serve 每条连接造一个服务
hyper 服务端主线怎么读 hyper 卷、Service 与 BodyTowerToHyperService 把 Router 交给 hyper,每个请求克隆一次服务

axum 仓库的 examples/ 有 50 多个完整的示例程序,crate 文档本身也很详细(它的大部分内容来自 src/docs/ 下的 markdown,见源码地图)。读源码之前先照着写一个带状态和提取器的小程序,会轻松很多。

目录

部分内容
00 导读本页;源码地图
01 全景axum 解决什么问题;一个请求穿过 axum
02 Handler 与提取器本卷重点:Handler trait、两种提取器 trait、State 与 FromRef、Path 与 Query、body 提取器
03 响应IntoResponse 与 IntoResponseParts;SSE 与 Redirect
04 路由Router 的内部、matchit、MethodRouter、nest 与 merge、Route 与装箱
05 中间件layer 与 route_layer;from_fn
06 tower-httpServeDir、压缩,以及超时、body 限制、CORS、Trace、CatchPanic
07 WebSocket升级握手与 WebSocket
08 测试不起服务器,用 oneshot 测 Router
09 设计取舍把各章的取舍汇总到一起
10 常见坑用报错原文做标题,讲它在源码里怎么产生
11 在本站中本站(开源)代码里怎么用 axum,tower-http 0.6.11 与 0.7.1 的差异
12 动手不用 axum,从零写一个最小的 Handler trait 和 Router

小结

  • 全卷锁定 axum axum-v0.8.9(c59208c8)、axum-core axum-core-v0.5.6(e710a97a)、tower-http tower-http-0.7.1(c941451);main 已经走过 tag 298 个提交,行号以 tag 为准。
  • tag 里的 axum-core 版本号是 0.5.5,实际编译的是 0.5.6;两者的 src/ 只差 4 行 lint 属性和一个文档链接。
  • 装配主线:async fn → Handler → get 放进 MethodRouter 的方法槽 → route 插进 matchit → layer → with_state 把方法槽变成 Route,Router<S> 变 Router<()> → axum::serve。
  • 请求主线:Router::call → matchit 匹配、存路径参数 → 按方法选端点(没有就 405)→ Route 的 oneshot → 提取器依次执行 → 你的函数 → into_response。
  • .layer 包在 Route 外面:中间件在路由之后、提取器之前执行。
  • 只想会用读 01、02、04 和常见坑;想读懂实现按顺序读,02 是重点。

💬 关联讨论

发起讨论
加载中...