Appearance
第2章 Router:路径匹配与路由树
从一行 route 调用说起
每个 Axum 应用的起点几乎都长这样:
rust
let app = Router::new().route("/users/{id}", get(get_user));看起来平淡无奇——注册一个路径、绑定一个处理器。但当这个 Router 被 axum::serve 驱动后,每个进来的 HTTP 请求都要在微秒内完成路径匹配、参数提取、处理器分发。这一行代码背后,牵出了 Axum 路由系统的三个核心问题:
路径字符串如何变成可快速查询的结构?
/users/{id}这类带参数的路径,不能简单用 HashMap 做 key-value 查找,因为{id}是参数占位符,实际请求中可能是/users/42或/users/alice。Axum 选择了 matchit——一个基于基数树(radix tree)的路由匹配库,时间复杂度接近 O(k),k 为路径长度。基数树能在静态路径、参数路径、通配符路径之间做出正确的优先级判断,这是 HashMap 做不到的。Router 如何在并发连接间零成本共享? Tower 的
Servicetrait 要求实现Clone,而每个 TCP 连接都会 clone 一次 Router。如果每次 clone 都深拷贝整个路由表,包括基数树、所有 handler 闭包、所有中间件栈,开销不可接受。Axum 用Arc<RouterInner<S>>让所有连接共享同一份路由表,clone 代价仅仅是原子加一。状态如何在编译期而非运行期保证注入?
Router<AppState>代表"缺少一个AppState",只有Router<()>才能真正处理请求。这个类型状态模式,让"忘记调用with_state"变成编译错误而非运行时 panic。不需要靠文档或测试来保证状态注入,类型系统本身就是安全网。
这三个问题不是孤立的。Arc 的零拷贝 clone 决定了 Router 的运行时性能上限,matchit 的基数树决定了路径匹配的时间复杂度,类型状态模式决定了状态安全的保证级别。它们共同构成了 Axum 路由系统的骨架。
本章将沿着这三个问题,从 Router<S> 的结构定义,一路深入到 PathRouter 的 matchit::Router<RouteId>、Fallback 分发机制、with_state 的类型转换、nest 和 merge 的路由组合,以及 Router<()> 作为 Service 的完整调用链。读完之后,当你再写 .route("/users/{id}", get(get_user)) 时,脑子里应该能完整浮现从路径字符串到 handler 调用的每一步。
Router<S> 的结构:Arc 包裹的类型状态
核心定义
Router<S> 的定义极其简洁(routing/mod.rs:86-88):
rust
pub struct Router<S = ()> {
inner: Arc<RouterInner<S>>,
}仅有一个字段:Arc<RouterInner<S>>。这个 Arc 不是可有可无的包装——它是 Axum 能在 hyper 的并发连接模型下高效运行的基础。RouterInner 才是真正承载数据的结构(routing/mod.rs:98-102):
rust
struct RouterInner<S> {
path_router: PathRouter<S>,
default_fallback: bool,
catch_all_fallback: Fallback<S>,
}三个字段的职责清晰分工:
path_router:核心路由器,持有所有注册的路径和对应的处理器,封装了 matchit 基数树。它是路由系统大部分逻辑的归宿,后续的路径匹配、参数提取、handler 分发全部在它内部完成。default_fallback:布尔标记,记录当前 fallback 是否为默认的 404 处理器。这个字段看似多余——为什么不直接比较catch_all_fallback是否为Fallback::Default?原因是Fallback::Default只表示"默认的 404 Route",而default_fallback还承载了"用户是否显式设置过 fallback"的语义。当两个 Router merge 时,这个布尔值决定了是否允许合并——双方都设置了自定义 fallback 就会 panic。catch_all_fallback:兜底处理器。当路径匹配失败时调用,默认返回 404。它的名字里有 "catch-all",暗示它匹配所有未命中的路径。注意它是Fallback<S>类型,意味着它也可能依赖 State——这一点在with_state转换时会体现出来。
为什么用 Arc
Router<S> 的 Clone 实现(routing/mod.rs:90-96)给出了答案:
rust
impl<S> Clone for Router<S> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}不是深拷贝,只是增加 Arc 的引用计数。这个设计的动机来自 Tower 的 Service trait 约束——Service::call 接收 &mut self,但 hyper 在处理并发连接时需要 clone 整个 Service。如果 Router 内部是 Vec<Endpoint> 加 matchit::Router 的深拷贝,每个连接的内存开销和初始化代价都不可接受。Arc 让所有连接共享同一份路由表,clone 代价仅仅是原子加一。
但 Arc 带来一个问题:修改 Router 时不能直接修改共享数据。试想,如果两个连接同时持有 Arc<RouterInner>,其中一个修改了路由表,另一个就会看到不一致的状态。Axum 用结构化的方式解决这个问题——Router 的构建阶段和运行阶段完全分离。构建阶段通过链式 API(.route().route().layer())逐步组装,最终通过 into_make_service 或 with_state 转换为不可变的运行时结构。运行时不会修改路由表,因此 Arc 的共享语义完全安全。这个"构建期可变、运行期不可变"的设计,是 Rust 中使用 Arc 的常见范式。
into_inner 方法是构建阶段的"拆包"操作(routing/mod.rs:172-181):
rust
fn into_inner(self) -> RouterInner<S> {
match Arc::try_unwrap(self.inner) {
Ok(inner) => inner,
Err(arc) => RouterInner {
path_router: arc.path_router.clone(),
default_fallback: arc.default_fallback,
catch_all_fallback: arc.catch_all_fallback.clone(),
},
}
}如果 Arc 引用计数为 1(最常见的情况——链式 API 中间没有额外 clone),直接 unwrap 取出所有权,零开销。如果引用计数大于 1(极少见,可能是用户手动 clone 了 Router),退化为 clone。这种"先试零开销,失败再退而求其次"的模式在 Rust 生态中相当常见——Arc::try_unwrap 就是为此而生的 API。
Axum 通过 map_inner! 和 tap_inner! 两个宏统一处理这个模式(routing/mod.rs:129-152):
rust
macro_rules! map_inner {
( $self_:ident, $inner:pat_param => $expr:expr) => {
let $inner = $self_.into_inner();
Router { inner: Arc::new($expr) }
};
}
macro_rules! tap_inner {
( $self_:ident, mut $inner:ident => { $($stmt:stmt)* } ) => {
let mut $inner = $self_.into_inner();
$($stmt)*;
Router { inner: Arc::new($inner) }
};
}map_inner! 用于不需要修改内部字段的场景(如 with_state、layer),它把 RouterInner 解构到模式中再重构。tap_inner! 用于需要修改内部字段的场景(如 route、fallback),它取出 RouterInner 的可变引用后执行一系列语句。两者都先取出 RouterInner,操作后重新包进 Arc。这个模式确保了链式 API 的安全性——每次调用都产生新的 Arc,不会影响之前可能存在的 clone。
类型状态模式
Router<S = ()> 的泛型参数 S 不是普通的泛型——它是类型状态(type state)。文档注释明确说明(routing/mod.rs:80-82):
Router<S>means a router that is missing a state of typeSto be able to handle requests. Thus, onlyRouter<()>(i.e. without missing state) can be passed toserve.
"missing"这个词是理解类型状态的关键。Router<AppState> 不是"持有一个 AppState",而是"缺少一个 AppState"。当调用 with_state(state: AppState) 时,state 被注入到所有 handler 中,Router 的泛型参数变为 (),表示"不再缺少任何东西"。
这意味着:
Router::new()返回Router<()>——没有状态需求,可以直接 serve。Router::with_state(state)消耗S,返回Router<S2>——通常 S2 是(),表示状态已注入。- 只有
Router<()>实现了Service<Request<B>>(routing/mod.rs:599),编译器阻止你直接 serve 一个Router<AppState>。
这个设计把"忘记提供状态"从运行时错误提升到编译时错误。当你写了 Router::<AppState>::new().route(...) 却忘记调用 .with_state(state),编译器会直接报错:Router<AppState> 没有实现 Service。不需要靠文档或 linter 提醒,类型系统本身就是你的安全网。
类型状态模式在 Rust 生态中并不罕见。std::fs::File 的 open/builder 模式、tokio::net::TcpListener 的 bind/serve 模式,都隐含了类型状态的思路。但 Axum 把它做到了极致——Router<S> 的泛型参数不仅影响自身的 Service impl,还级联影响 MethodRouter<S>、Fallback<S>、PathRouter<S> 的整个调用链。当你在 Router<AppState> 上调用 .route("/", get(handler)) 时,handler 必须接受 State<AppState> 参数,否则类型不匹配。整个状态类型从 Router 传递到 MethodRouter 再传递到 Handler,一层层约束下来,不会有"状态类型对不上"的漏洞。
PathRouter:路由表的双层索引
PathRouter<S> 是路由系统的真正引擎(routing/path_router.rs:16-20):
rust
pub(super) struct PathRouter<S> {
routes: Vec<Endpoint<S>>,
node: Arc<Node>,
v7_checks: bool,
}三个字段各司其职:routes 是处理器列表,node 是路径匹配的基数树索引,v7_checks 是路径校验开关。下面逐一拆解。
Vec<Endpoint<S>>——处理器列表
routes 是按注册顺序排列的处理器列表。Endpoint 是一个枚举(routing/mod.rs:786-790):
rust
enum Endpoint<S> {
MethodRouter(MethodRouter<S>),
Route(Route),
}两种变体对应两种注册方式:
MethodRouter:通过.route(path, get(handler))注册的普通处理器,支持按 HTTP 方法分发(GET/POST/PUT 等)。这是最常见的变体,大多数用户只会用到它。MethodRouter 内部维护了每种 HTTP 方法对应的 handler,以及方法不允许时的 fallback。Route:通过.route_service(path, service)注册的原始 Service,不区分方法,全权接管所有 HTTP 方法。适用于需要精细控制 HTTP 行为的场景,比如代理转发、自定义协议处理。
Route 类型的内部实现也值得了解(routing/route.rs:31):
rust
pub struct Route<E = Infallible>(BoxCloneSyncService<Request, Response, E>);BoxCloneSyncService 是 tower 的类型擦除 Service 容器——它把任何实现了 Service + Clone + Send + Sync 的类型装箱为一个固定大小的对象,隐藏了原始类型信息。这意味着 Route 不关心内部 Service 的具体类型,只关心它的行为。这是 Axum 能在同一棵路由树中存储不同类型 handler 的关键——类型擦除统一了存储格式。代价是一点间接调用的开销(虚函数分发),换来的是 Vec<Endpoint> 可以存储异构类型。
RouteId 是 routes 的索引(routing/mod.rs:75-76):
rust
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct RouteId(usize);就是一个 usize 的 newtype。matchit 匹配路径后返回 RouteId,用它直接索引 Vec 取出对应的 Endpoint——O(1) 查找,没有额外的哈希计算。这个设计看似简单,但它确保了从路径匹配到处理器调用的最短路径:基数树查 RouteId,Vec 索引得 Endpoint,一步不多。RouteId 派生了 Copy、Hash、Ord 等 trait,这让它可以作为 HashMap 的 key,也可以在双向映射中高效比较。
Node——matchit 基数树加双向 HashMap
Node 是 PathRouter 的核心索引结构(routing/path_router.rs:404-410):
rust
#[derive(Clone, Default)]
struct Node {
inner: matchit::Router<RouteId>,
route_id_to_path: HashMap<RouteId, Arc<str>>,
path_to_route_id: HashMap<Arc<str>, RouteId>,
}三层结构,各有分工:
inner: matchit::Router<RouteId>:基数树,负责路径匹配。插入路径字符串时,matchit 将其解析为基数树节点;查询时沿树遍历,时间复杂度 O(k),k 为路径长度。这是整个路由系统的性能核心。route_id_to_path: HashMap<RouteId, Arc<str>>:从 RouteId 反查路径字符串。nest和merge操作需要遍历另一个 Router 的所有路由并重新注册,此时需要通过 RouteId 拿到原始路径。没有这个映射,merge 就无法知道"第 3 个路由对应的路径是什么"。path_to_route_id: HashMap<Arc<str>, RouteId>:从路径字符串正查 RouteId。.route()方法需要检测同一路径是否已经注册过 MethodRouter,如果是则合并而非新建。这使得.route("/", get(h1)).route("/", post(h2))能正确工作——第二次调用时,path_to_route_id发现/已存在,走合并逻辑。
为什么需要双向映射?看起来冗余,但它们服务于不同的操作路径。path_to_route_id 在注册阶段使用(检测重复路径),route_id_to_path 在组合阶段使用(nest/merge 需要反查路径)。两者不能互推,因为 path_to_route_id 的 key 是路径字符串,而 route_id_to_path 的 key 是 RouteId,反查方向不同。
Node::insert 的实现清晰地展示了双向映射的维护(routing/path_router.rs:413-427):
rust
fn insert(
&mut self,
path: impl Into<String>,
val: RouteId,
) -> Result<(), matchit::InsertError> {
let path = path.into();
self.inner.insert(&path, val)?;
let shared_path: Arc<str> = path.into();
self.route_id_to_path.insert(val, shared_path.clone());
self.path_to_route_id.insert(shared_path, val);
Ok(())
}先插入 matchit 树(如果路径冲突,insert 返回 InsertError),再维护两个 HashMap。Arc<str> 让路径字符串在 HashMap 间共享,避免重复分配。
注意插入顺序:matchit 树的 insert 必须先执行,因为它可能失败(路径冲突)。如果先插入 HashMap 再插入 matchit 树,matchit 失败后 HashMap 中就有脏数据。这个"先执行可能失败的操作,再执行必然成功的操作"的模式,是编写健壮代码的基本功。
注册路由的完整流程
当调用 Router::route("/users/{id}", get(handler)) 时,调用链如下:
Router::route(routing/mod.rs:192-196):调用tap_inner!宏取出RouterInner,调用path_router.route(path, method_router)。注意panic_on_err!宏——如果路径校验或插入失败,直接 panic。这是 Axum 的设计选择:路由注册错误属于编程错误,不应该被静默忽略。PathRouter::route(routing/path_router.rs:66-93):- 先调用
validate_path校验路径合法性(空路径、缺少前导/、v0.7 语法检查)。 - 查
path_to_route_id看该路径是否已有 MethodRouter。若有,执行merge_for_path合并方法(比如先注册了 GET,再注册 POST,合并为一个 MethodRouter,同时支持两种方法)。这个合并机制让用户可以分多次注册同一路径的不同方法处理器,而不需要一次性写完。 - 若无,调用
new_route创建新端点。
- 先调用
PathRouter::new_route(routing/path_router.rs:139-144):RouteId(self.routes.len())——用当前 Vec 长度作为新 RouteId。这个设计保证了 RouteId 与 Vec 索引的一致性:第 N 个注册的路由,RouteId 就是 N。self.set_node(path, id)——插入 matchit 树和双向 HashMap。self.routes.push(endpoint)——加入处理器列表。
路径匹配:从 matchit 到处理器调用
matchit 的基数树
matchit 是 Axum 的路径匹配引擎,其核心数据结构是基数树(radix tree),也称为压缩前缀树。与普通 Trie 不同,基数树将只有单个子节点的中间路径压缩为一个边,减少树的深度。这种结构在 Go 生态的 httprouter 中被广泛使用,matchit 是它的 Rust 移植。
为了理解基数树为什么适合 HTTP 路由匹配,我们对比三种常见方案:
| 方案 | 数据结构 | 查询复杂度 | 内存 | 参数提取 |
|---|---|---|---|---|
| 线性扫描 | Vec<(String, Handler)> | O(n) | 低 | 手动 |
| HashMap | HashMap<String, Handler> | O(1) 均摊 | 中 | 不支持 |
| 基数树 | radix tree | O(k) | 中 | 内建 |
线性扫描的问题是:100 条路由时每次请求要比较 100 个字符串。HashMap 的问题是:/users/{id} 和 /users/me 是同一个 key 的两种匹配模式——HashMap 无法处理带参数的路径。基数树在这两者之间取得了平衡:它把路径按 / 分段,每段是树的一个节点,参数段({id})是特殊节点,通配符段({*path})是叶子节点。查询时沿树遍历,时间复杂度 O(k),k 为路径长度——与路由数量无关。
基数树的关键特性:
- 静态路径优先于参数路径匹配。
/users/me优先于/users/{id}。这个优先级是通过节点类型实现的——基数树在每一层先检查静态子节点,再检查参数子节点。 - 通配符路径优先级最低。
/{*rest}只在静态和参数路径都不匹配时生效。通配符节点只能出现在路径末尾,且每条路径最多一个。 - 插入时检测冲突。如果两个路径在同一个位置既有静态段又有参数段(如
/users/{id}和/{entity}/me在根层级),matchit 的insert会返回InsertError。这个冲突检测是启动时而非运行时——你在开发阶段就会看到 panic,而不是在生产环境偶发 404。
一个常见的误解是"基数树比 HashMap 慢"。对于小规模路由表(少于 20 条),线性扫描可能更快(CPU 缓存友好)。但基数树的优势在路由数量增长后才会显现——100 条路由时,基数树的查询时间是稳定的 O(k),而线性扫描退化为 O(n)。Axum 选择基数树是正确的权衡:注册路由是一次性操作,匹配路由是每次请求都要执行的路径。
call_with_state 的匹配流程
当请求到达 Router<()> 时,Service::call 委托给 Router::call_with_state(routing/mod.rs:452-462):
rust
pub(crate) fn call_with_state(&self, req: Request, state: S) -> RouteFuture<Infallible> {
let (req, state) = match self.inner.path_router.call_with_state(req, state) {
Ok(future) => return future,
Err((req, state)) => (req, state),
};
self.inner.catch_all_fallback.clone().call_with_state(req, state)
}先尝试 path_router,匹配成功直接返回;失败则交给 catch_all_fallback。注意返回类型——path_router.call_with_state 返回 Result<RouteFuture, (Request, S)>。Err 变体不是错误,而是"未匹配"的信号,同时保留原始请求和状态,交给 fallback 处理。这个设计避免了重新构造请求的开销——请求的所有权和状态的所有权都被传递给下一个处理者。
进入 PathRouter::call_with_state(routing/path_router.rs:325-372),匹配过程分为以下步骤:
rust
match self.node.at(parts.uri.path()) {
Ok(match_) => {
let id = *match_.value;
// 设置 matched-path(feature gated)
url_params::insert_url_params(&mut parts.extensions, &match_.params);
let endpoint = self.routes.get(id.0).expect("...");
match endpoint {
Endpoint::MethodRouter(method_router) => {
Ok(method_router.call_with_state(req, state))
}
Endpoint::Route(route) => Ok(route.clone().call_owned(req)),
}
}
Err(MatchError::NotFound) => Err((Request::from_parts(parts, body), state)),
}逐一拆解:
self.node.at(uri.path()):调用 matchit 的基数树查询。at方法沿基数树遍历,返回Match<RouteId>或MatchError::NotFound。Match包含两部分:value(存储在树中的RouteId)和params(提取的路径参数)。取出 RouteId:
match_.value是&RouteId,解引用得到RouteId(usize)。这个 usize 就是routesVec 的索引。设置匹配路径:如果启用了
matched-pathfeature,调用set_matched_path_for_request将匹配的路径模板(如/users/{id})写入 request extensions。这个信息供MatchedPathextractor 使用——当你需要在 handler 中获取匹配的路径模板(而非实际请求路径)时,就用MatchedPath。这在日志和监控场景中特别有用:你想知道请求匹配了哪个路由模板,而不是具体的 URL。注入 URL 参数:
url_params::insert_url_params将 matchit 提取的路径参数写入 request extensions。matchit 的params是一个键值对列表,如id=42。取出 Endpoint:
self.routes.get(id.0)直接用 RouteId 索引 Vec。expect中的消息是 "no route for id. This is a bug in axum. Please file an issue"——如果走到这一步说明 Axum 内部数据不一致,是框架 bug。分发调用:MethodRouter 走
call_with_state(支持方法级分发,下一章详解),Route 走call_owned(全权接管,调用一次后消耗 Route 所有权,避免不必要的 clone)。
值得注意的一个设计选择:PathRouter::call_with_state 返回的是 Result<RouteFuture<Infallible>, (Request, S)>。匹配成功返回 Ok(future),匹配失败返回 Err((req, state))——把请求和状态原封不动地还给调用者。这个返回类型的设计不是随意的——它让 Router::call_with_state 可以把未匹配的请求直接传给 fallback,而不用重新构造 Request 或 clone state。对比一种可能的替代设计:让 PathRouter::call_with_state 内部直接调用 fallback。那种设计会把 fallback 的选择逻辑耦合进 PathRouter,违反单一职责——PathRouter 只负责"路径匹配和分发",不管"匹配失败后怎么办"。
另一个值得关注的性能细节:call_with_state 在匹配成功后直接调用 method_router.call_with_state(req, state),而不是先把 MethodRouter 取出来再调用。这意味着 MethodRouter 的调用是内联的——在 release 构建中,路径匹配和方法分发的代码会被编译器优化成一个紧凑的分支序列,没有任何虚函数调用或间接分派。这是 Axum 能在微秒级完成请求分发的原因之一。
URL 参数的注入
url_params::insert_url_params 的实现(routing/url_params.rs:12-47)值得仔细看,因为它处理了路径参数的嵌套和非法 UTF-8 两个棘手问题:
rust
pub(super) fn insert_url_params(extensions: &mut Extensions, params: &Params<'_, '_>) {
let current_params = extensions.get_mut();
// ...
let params = params
.iter()
.filter(|(key, _)| !key.starts_with(super::NEST_TAIL_PARAM))
.filter(|(key, _)| !key.starts_with(super::FALLBACK_PARAM))
.map(|(k, v)| {
if let Some(decoded) = PercentDecodedStr::new(v) {
Ok((Arc::from(k), decoded))
} else {
Err(Arc::from(k))
}
})
.collect::<Result<Vec<_>, _>>();
match (current_params, params) {
(Some(UrlParams::Params(current)), Ok(params)) => {
current.extend(params);
}
(None, Ok(params)) => {
extensions.insert(UrlParams::Params(params));
}
// ...
}
}两个关键点:
过滤内部参数:
NEST_TAIL_PARAM(__private__axum_nest_tail_param)和FALLBACK_PARAM(__private__axum_fallback)是 Axum 内部使用的隐藏参数名,不能暴露给用户的Pathextractor。这两个参数是nest_service和fallback_endpoint在 matchit 树中注册路由时使用的通配符参数,用于匹配嵌套路由的剩余路径和 fallback 的所有路径。如果不过滤,用户的Pathextractor 就会收到这些内部参数,造成混淆。百分号解码:
PercentDecodedStr::new(v)尝试对 URL 参数值做百分号解码。URL 中的非 ASCII 字符会被编码为%XX形式,Axum 在存储参数前尝试解码。如果解码失败(非法 UTF-8 字节序列),存储InvalidUtf8InPathParam,后续Pathextractor 会返回 400 Bad Request,而不是让非法数据流入 handler。
路径参数支持嵌套叠加——当 nest 多层 Router 时,每层的参数会被 extend 到同一个 UrlParams::Params 中。比如外层 nest("/orgs/{org_id}", inner_router),内层注册了 /{project_id}/tasks,请求 /orgs/42/7/tasks 会提取两个参数:org_id=42 和 project_id=7。current_params 分支就是处理这个场景——如果 extensions 中已有参数(来自外层),新参数追加到现有列表中。
路径校验:v0.7 的语法迁移
旧语法与新语法
Axum 0.7 做了一个破坏性变更:路径参数语法从 Go 风格的 :param 和 *wildcard 迁移到 OpenAPI 风格的 {param} 和 {*wildcard}。这个变更影响的不只是写法——它改变了 Axum 与 OpenAPI 生态的互操作性。
validate_v07_paths 的实现(routing/path_router.rs:36-56):
rust
fn validate_v07_paths(path: &str) -> Result<(), &'static str> {
path.split('/')
.find_map(|segment| {
if segment.starts_with(':') {
Some(Err("Path segments must not start with `:`. \
For capture groups, use `{capture}`. ..."))
} else if segment.starts_with('*') {
Some(Err("Path segments must not start with `*`. \
For wildcard capture, use `{*wildcard}`. ..."))
} else {
None
}
})
.unwrap_or(Ok(()))
}这段代码做的事情很简单:遍历路径的每一段,检查是否有以 : 或 * 开头的段。find_map 在找到第一个错误后立即返回,保证错误信息精确指向问题所在的段。
为什么做这个迁移?核心原因是 OpenAPI 兼容性。OpenAPI 3.1 规范定义路径参数为 /users/{id},而旧语法 /users/:id 在 OpenAPI 文档中没有标准定义。对于需要生成 OpenAPI spec 的项目(比如配合 utoipa 或 openapi3 使用),旧语法会造成不一致——路由定义用 :id,OpenAPI 文档用 {id},必须额外做一层转换。迁移到 {param} 语法后,Axum 的路由定义可以直接映射到 OpenAPI 路径模板,零转换。
语法迁移的另一层考量是语义清晰度。:param 语法来自 Express.js 的约定,但在 Rust 的语境下,冒号容易与类型标注混淆(fn foo(x: i32))。{param} 语法更接近 Rust 的结构体字段初始化(Foo { x: 1 }),视觉上更一致。{*wildcard} 中的 * 前缀明确表示"零个或多个路径段",比 *wildcard 更易读。
但这个迁移也有一个工程代价:matchit 内部的路径模板语法与 Axum 暴露给用户的语法不同。matchit 使用 :param 和 *wildcard 作为内部表示,Axum 在调用 matchit::Router::insert 之前会把 {param} 转换成 :param、把 {*wildcard} 转换成 *wildcard。这个转换发生在 PathRouter::route 和 PathRouter::nest 等方法内部——用户写的是 Axum 语法,matchit 看到的是自己的语法。这种适配层虽然增加了少量代码,但让用户 API 和底层实现可以独立演进。
如果你确实需要匹配以 : 或 * 字面量开头的路径段(极少数场景,比如旧系统兼容),可以调用 .without_v07_checks() 关闭校验:
rust
Router::new()
.without_v07_checks()
.route("/literal/:segment", get(handler))without_v07_checks 的实现(routing/mod.rs:184-188)只是把 PathRouter 内部的 v7_checks 标记设为 false,后续所有 validate_path 调用都会跳过 v0.7 校验。注意,关闭校验后 matchit 仍然能正确处理 :param 语法(matchit 本身支持两种语法),只是 Axum 不再帮你检查。这是一个有意的逃生舱——Axum 团队不希望你被框架的校验绊住,但默认行为仍然是最安全的选项。
nest 与 merge:路由的组合艺术
nest——路径前缀的递归合并
nest 是 Axum 中最容易被误解的 API 之一。它的签名(routing/mod.rs:220-237):
rust
pub fn nest(self, path: &str, router: Self) -> Self功能看起来很简单:将一个 Router 挂载到指定路径前缀下。但"挂载"这个词掩盖了实际发生的事情——nest 不是简单地拼接前缀,而是将子 Router 的所有路由展平后重新注册到父 Router 中。展平后,最终的 Router 中没有"子 Router"的概念,所有路由都在同一棵基数树中。
PathRouter::nest 的实现(routing/path_router.rs:172-210):
rust
pub(super) fn nest(
&mut self,
path_to_nest_at: &str,
router: Self,
) -> Result<(), Cow<'static, str>> {
let prefix = validate_nest_path(self.v7_checks, path_to_nest_at)?;
let Self { routes, node, v7_checks: _ } = router;
for (id, endpoint) in routes.into_iter().enumerate() {
let route_id = RouteId(id);
let inner_path = node.route_id_to_path.get(&route_id).expect("...");
let path = path_for_nested_route(prefix, inner_path);
let layer = (
StripPrefix::layer(prefix),
SetNestedPath::layer(path_to_nest_at),
);
match endpoint.layer(layer) {
Endpoint::MethodRouter(method_router) => {
self.route(&path, method_router)?;
}
Endpoint::Route(route) => {
self.route_endpoint(&path, Endpoint::Route(route))?;
}
}
}
Ok(())
}关键步骤:
遍历子 Router 的所有路由:通过
route_id_to_path反查每个路由的路径,拼接父路径前缀,得到最终路径。比如nest("/api", sub_router),子 Router 有/{id}路径,最终注册为/api/{id}。应用两层中间件:
StripPrefix::layer(prefix)剥离请求 URL 的前缀(让子 Router 的 handler 看到的是去掉前缀后的路径),SetNestedPath::layer(path_to_nest_at)设置嵌套路径(供NestedPathextractor 使用)。这两个层包裹在每个 endpoint 上,确保嵌套后的 handler 行为与独立运行时一致。逐条注册:每条路由独立注册到父 Router 的 PathRouter 中,包括 matchit 树和双向 HashMap。这意味着 nest 后的 Router 只有一次路径匹配,而不是先匹配前缀再匹配子路径。这比两层匹配更高效。
这种设计的一个副作用:子 Router 的 Fallback 不会被继承。源码注释(routing/mod.rs:225-232)明确说明了这一点——如果继承子 Router 的 catch-all fallback,它最终会匹配 /{path}/*,而通配符不匹配空路径,语义上不正确。另外,nest 在根路径 / 上是禁止的(routing/mod.rs:221-223),因为根路径的 nest 等价于 merge,Axum 强制你用 merge 来表达这个语义。
validate_nest_path 还有一个特殊校验(routing/path_router.rs:445-464):嵌套路径不能包含通配符。因为 nest 的语义是"将子路由挂载到前缀下",通配符会匹配不确定的路径段,破坏前缀的可预测性。
nest_service——单 Service 挂载
nest_service 与 nest 的区别在于,它将一个 Service 而非 Router 挂载到指定前缀下(routing/path_router.rs:212-249)。它的实现更精巧:
rust
pub(super) fn nest_service<T>(
&mut self,
path_to_nest_at: &str,
svc: T,
) -> Result<(), Cow<'static, str>>
where
T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
{
let path = validate_nest_path(self.v7_checks, path_to_nest_at)?;
let prefix = path;
let path = if path.ends_with('/') {
format!("{path}{{*{NEST_TAIL_PARAM}}}")
} else {
format!("{path}/{{*{NEST_TAIL_PARAM}}}")
};
// ...
self.route_endpoint(&path, endpoint.clone())?;
self.route_endpoint(prefix, endpoint.clone())?;
if !prefix.ends_with('/') {
self.route_endpoint(&format!("{prefix}/"), endpoint)?;
}
Ok(())
}它注册了三条路由:
/prefix/{*__private__axum_nest_tail_param}:匹配前缀后的所有路径,剩余部分存入隐藏参数。这是主要的匹配路径。/prefix:匹配前缀本身。/{*rest}这种通配符路径不匹配空路径,所以需要单独注册前缀本身的路径。/prefix/:匹配前缀加尾部斜杠。同样是通配符不匹配的边界情况。
三条路由指向同一个 Service,配合 StripPrefix 中间件剥离前缀。这个设计确保了 nest_service("/api", svc) 无论请求是 /api、/api/ 还是 /api/users/42,都能正确到达 svc。三个边界条件被逐一覆盖,不留死角。
merge——两个 Router 的路由合并
merge 将两个 Router 的路由合并为一个(routing/mod.rs:258-293)。它的实现比 nest 简单——不需要拼接前缀,只需要把另一个 Router 的路由逐条添加进来:
rust
pub fn merge<R>(self, other: R) -> Self
where
R: Into<Self>,
{
let other: Self = other.into();
let RouterInner {
path_router,
default_fallback,
catch_all_fallback,
} = other.into_inner();
map_inner!(self, mut this => {
match (this.default_fallback, default_fallback) {
(_, true) => {} // other 有默认 fallback,不影响
(true, false) => { // this 有默认,other 有自定义
this.default_fallback = false;
}
(false, false) => { // 双方都有自定义 fallback
panic!("Cannot merge two `Router`s that both have a fallback")
}
};
panic_on_err!(this.path_router.merge(path_router));
this.catch_all_fallback = this.catch_all_fallback
.merge(catch_all_fallback)
.unwrap_or_else(|| panic!("..."));
this
})
}PathRouter::merge 的实现(routing/path_router.rs:146-170)遍历另一个 PathRouter 的所有路由,通过 route_id_to_path 反查路径,然后逐条注册。如果两个 Router 有相同路径的 MethodRouter,会自动合并方法——这和 PathRouter::route 中的合并逻辑完全一致。
merge 的核心约束是:两个 Router 不能同时拥有自定义 fallback。这个约束在 Router::merge 的两个地方检查:先检查 default_fallback 布尔值,再检查 catch_all_fallback.merge 的返回值。双重检查是因为 fallback_endpoint 方法不仅设置了 catch_all_fallback,还在 PathRouter 中注册了 / 和 /{*fallback} 两条路由——即使 catch_all_fallback 一方是 Default,PathRouter 的路由合并仍可能产生冲突。
Fallback 系统:路径不匹配时怎么办
Fallback 的三种形态
Fallback 是一个枚举(routing/mod.rs:710-714):
rust
enum Fallback<S, E = Infallible> {
Default(Route<E>),
Service(Route<E>),
BoxedHandler(BoxedIntoRoute<S, E>),
}三种变体对应三种使用方式,理解它们的区别对正确使用 Fallback 至关重要:
Default:默认 404 处理器。Router::new()创建时自动设置Fallback::Default(Route::new(NotFound))。NotFound是一个简单的 Service,始终返回 404 Not Found。这个变体在merge时具有"被覆盖"的语义——如果另一个 Router 有自定义 fallback,Default 会让位。Service:通过.fallback_service(service)设置。Service变体不依赖状态——它是一个已经完全构造好的Route,可以直接调用。适用于需要将未匹配的请求转发给另一个 Service 的场景(比如 SPA 应用的前端静态文件服务,所有未匹配 API 路由的请求都返回index.html)。BoxedHandler:通过.fallback(handler)设置。handler是一个实现了Handlertrait 的异步函数,可能依赖 State。在with_state时会被转换为Route(即Service变体)。BoxedIntoRoute是一个类型擦除的中间形态——它在存储时保留了 State 类型信息,在with_state时消耗 State 并转换为Route。
Fallback 的执行逻辑
在 Router::call_with_state 中,path_router 匹配失败后才会调用 catch_all_fallback。但 Fallback 的实现有一个微妙之处——fallback_endpoint 方法不仅设置了 catch_all_fallback,还在 PathRouter 中注册了两条特殊路径(routing/mod.rs:391-441):
rust
fn fallback_endpoint(self, endpoint: Endpoint<S>) -> Self {
tap_inner!(self, mut this => {
_ = this.path_router.route_endpoint(
"/",
endpoint.clone().layer(/* 移除 MatchedPath 的层 */)
);
_ = this.path_router.route_endpoint(
FALLBACK_PARAM_PATH, // "/{*__private__axum_fallback}"
endpoint.layer(/* 移除 MatchedPath 的层 */)
);
this.default_fallback = false;
})
}为什么要在 PathRouter 里也注册?因为 Fallback 需要处理两种"不匹配":
路径精确不匹配:请求路径在基数树中完全找不到。此时
PathRouter::call_with_state返回Err,交由catch_all_fallback处理。路径匹配但方法不匹配:请求路径匹配了某个端点,但 HTTP 方法不在允许列表中。此时
PathRouter匹配成功,但MethodRouter返回 405 Method Not Allowed。如果 Fallback 只靠catch_all_fallback,就无法拦截这种情况——因为PathRouter已经匹配成功了。
Fallback 的 / 和 /{*__private__axum_fallback} 注册确保了路径层面的兜底——即使是精确匹配 / 的请求,也能被 Fallback 拦截。同时,这两条特殊路径注册时都包裹了一个 service_fn 层,会移除 MatchedPath——因为 Fallback 不应该被认为"匹配了"某个特定路径。
这种"两套机制协作"的设计看起来冗余,但它是解决"路径匹配与方法分发在不同层级"这个问题的必然结果。如果 Axum 在路径匹配阶段就能判断方法是否允许,就不需要这种双重注册。但路径匹配(matchit)和方法分发(MethodRouter)是两个独立的抽象,前者不知道后者允许哪些方法。
Fallback 合并的冲突规则
Fallback::merge 的逻辑(routing/mod.rs:720-727):
rust
fn merge(self, other: Self) -> Option<Self> {
match (self, other) {
(Self::Default(_), pick) | (pick, Self::Default(_)) => Some(pick),
_ => None,
}
}只要有一方是 Default,就取另一方。如果两方都不是 Default(都是自定义 fallback),返回 None。在 Router::merge 中,None 会触发 panic。
这个设计原则是:两个自定义 Fallback 的合并语义不明确。router_a 的 fallback 返回自定义 404 页面,router_b 的 fallback 转发到前端静态文件服务,合并后该用哪个?与其猜测用户意图,不如在启动时 fail-fast,强制用户显式选择。
如果你确实需要合并两个带自定义 Fallback 的 Router,先用 .reset_fallback() 清除其中一个(routing/mod.rs:384-389):
rust
pub fn reset_fallback(self) -> Self {
tap_inner!(self, mut this => {
this.default_fallback = true;
this.catch_all_fallback = Fallback::Default(Route::new(NotFound));
})
}reset_fallback 把 default_fallback 重置为 true,把 catch_all_fallback 重置为 Default(NotFound)。调用后再 merge,就不会触发冲突。
route 与 route_service:Handler 与 Service 的双入口
Router::route 和 Router::route_service 都往路由表里注册端点,但它们的语义完全不同——这种差异直接反映了 Axum 的"Handler vs Service"双层设计。
route 接收 MethodRouter<S>,后者通过 get(handler)、post(handler) 等函数构建。MethodRouter 内部持有的是实现了 Handler<T, S> 的闭包——它们还没有被转换成 tower::Service,还在等待 .with_state(state) 的状态注入。这意味着你可以在 Router<AppState> 上链式调用 .route(path, get(handler)),handler 里的 State<AppState> 参数会自然地与 Router 的 S 参数匹配。类型系统保证了 handler 的状态类型与 Router 的状态类型一致。
route_service 接收的是一个已经实现了 tower::Service<Request, Error = Infallible> 的类型——它不需要状态注入、不需要 Handler 到 Service 的转换。route_service 适合两种场景:第一,集成第三方 Service(比如 tower_http::services::ServeDir 提供的静态文件服务);第二,当你想要完全控制 Service 的生命周期和错误处理时。
route_service 有一个特殊的安全检查(routing/mod.rs:200-215):
rust
pub fn route_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
{
let Err(service) = try_downcast::<Self, _>(service) else {
panic!(
"Invalid route: `Router::route_service` cannot be used with `Router`s. \
Use `Router::nest` instead"
);
};
// ...
}如果你试图把一个 Router 传给 route_service,它会在运行时 panic 并告诉你应该用 nest。这个 try_downcast 的类型检查揭示了一个常见错误:route_service 期望接收一个处理单个路径的 Service,而不是一个带完整路由树的 Router。后者应该用 nest 来挂载。
为什么 route_service 要求 Error = Infallible?这又回到了 Axum 的核心设计哲学——所有错误都必须被转换为响应,不能逃逸到 hyper。如果你的 Service 可能返回错误,你需要先用 HandleError 层把错误转成响应。这个约束看似严格,但它消除了整类生产事故——你永远不会在日志里看到"unhandled service error"。
layer 与 route_layer:中间件作用域的差异
Router 提供了两种方式添加 Tower 中间件:layer 和 route_layer。它们的区别在于作用域,理解这个区别对正确使用中间件至关重要。
Router::layer(routing/mod.rs:296-309)对所有路由生效——包括已注册的路由和 fallback。它通过递归地对 PathRouter 和 Fallback 应用 Layer 来实现:
rust
pub fn layer<L>(self, layer: L) -> Self {
map_inner!(self, this => RouterInner {
path_router: this.path_router.layer(layer.clone()),
default_fallback: this.default_fallback,
catch_all_fallback: this.catch_all_fallback.map(|route| route.layer(layer)),
})
}注意 layer.clone()——Layer 必须实现 Clone,因为 PathRouter::layer 内部需要对每个 Endpoint 分别应用 Layer。如果 Layer 是有状态的(比如 TimeoutLayer 内部持有一个 sleep),clone 的语义必须正确。
Router::route_layer(routing/mod.rs:312-326)只对已注册的路由生效,不影响 fallback。它通过 PathRouter::route_layer 实现,后者只遍历 routes Vec 中的 Endpoint,不触碰 Fallback。而且 route_layer 在没有注册路由时会 panic(routing/path_router.rs:281-286)——这是防止无操作错误的保护措施。
两者的典型使用场景:
layer:全局中间件。例如 tracing、compression、CORS——这些中间件应该对 404 响应也生效。route_layer:路由级中间件。例如认证中间件——你不想让 404 响应也带上WWW-Authenticate头。
Router 作为 Service
Service impl for Router<()>
只有 Router<()> 实现了 Service<Request<B>>(routing/mod.rs:599-618):
rust
impl<B> Service<Request<B>> for Router<()>
where
B: HttpBody<Data = bytes::Bytes> + Send + 'static,
B::Error: Into<axum_core::BoxError>,
{
type Response = Response;
type Error = Infallible;
type Future = RouteFuture<Infallible>;
#[inline]
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
#[inline]
fn call(&mut self, req: Request<B>) -> Self::Future {
let req = req.map(Body::new);
self.call_with_state(req, ())
}
}几个值得注意的细节:
poll_ready永远返回 Ready:Router 没有背压(backpressure)概念。路由表是静态的,不存在"还没准备好"的状态。这与下游的 handler 可能不同——handler 内部可能有连接池或限流,但 Router 本身不做任何资源管理。Tower 的 backpressure 机制在 Router 这一层被跳过,请求到达后立即进入匹配流程。Error类型是Infallible:Infallible是一个不可能构造的空枚举类型,意味着Service::call永远不会返回Err。所有错误(包括 404、405、500)都被转换为正常的Response返回。这是 Axum 的核心设计哲学——HTTP 服务不应该有"未处理的错误",每一个请求都必须有响应。错误是响应的一种,不是异常。Body 类型转换:
req.map(Body::new)将任意满足约束的请求体转换为 Axum 内部的Body类型。Body是对hyper::Body的封装,提供统一的 body 抽象。这个转换是必要的,因为 Tower 的Servicetrait 允许不同的 body 类型,但 Axum 内部需要统一的 body 来处理提取器和响应。
into_make_service 的急切转换
into_make_service 是 Router<()> 的专属方法(routing/mod.rs:558-562):
rust
pub fn into_make_service(self) -> IntoMakeService<Self> {
IntoMakeService::new(self.with_state(()))
}看起来只是对 with_state(()) 的简单封装,但注释揭示了意图:
call
Router::with_statesuch that everything is turned intoRouteeagerly rather than doing that per request
"eagerly"是关键词。with_state 会将所有 MethodRouter<S> 转换为 MethodRouter<()>,并将其内部的 handler 从"需要状态注入的闭包"变为"状态已捕获的 Route"。这个转换只做一次,之后每次请求直接调用已准备好的 Route,避免重复转换的开销。
如果不做急切转换会怎样?每次请求到来时,Router 需要检查每个 MethodRouter 是否已经注入状态,如果没有则注入。这个检查虽然开销不大,但在高并发下会累积。急切转换把这个一次性工作前移到启动阶段,让请求处理路径最短。
同样,在 axum::serve 的 Service<IncomingStream> 实现中(routing/mod.rs:579-596),call 方法也是急切地调用 with_state(()):
rust
fn call(&mut self, _req: serve::IncomingStream<'_, L>) -> Self::Future {
std::future::ready(Ok(self.clone().with_state(())))
}每个新连接到来时,MakeService::call 产出一个新的 Router<()>,而 with_state(()) 的急切转换确保了后续的请求处理路径最短。
with_state:类型状态的转换枢纽
签名与语义
with_state 的签名(routing/mod.rs:443-450):
rust
pub fn with_state<S2>(self, state: S) -> Router<S2> {
map_inner!(self, this => RouterInner {
path_router: this.path_router.with_state(state.clone()),
default_fallback: this.default_fallback,
catch_all_fallback: this.catch_all_fallback.with_state(state),
})
}它消耗 self(Router 不再可用),接收一个 S 类型的状态值,返回 Router<S2>。通常 S2 是 (),表示"状态已经注入到所有处理器中,Router 不再缺少任何东西"。
注意 state.clone()——with_state 需要为 path_router 和 catch_all_fallback 各提供一份 state。path_router.with_state 内部还会为每个 MethodRouter clone 一次 state。这意味着如果有 N 个 MethodRouter,state 会被 clone N+2 次。这要求 S: Clone,而且 clone 的代价应该足够低(通常 Arc<AppState> 是最佳实践——clone 只是增加引用计数,开销极小)。
PathRouter 的 with_state
PathRouter::with_state 的实现(routing/path_router.rs:305-322):
rust
pub(super) fn with_state<S2>(self, state: S) -> PathRouter<S2> {
let routes = self
.routes
.into_iter()
.map(|endpoint| match endpoint {
Endpoint::MethodRouter(method_router) => {
Endpoint::MethodRouter(method_router.with_state(state.clone()))
}
Endpoint::Route(route) => Endpoint::Route(route),
})
.collect();
PathRouter {
routes,
node: self.node,
v7_checks: self.v7_checks,
}
}三个要点:
MethodRouter 被转换:
method_router.with_state(state.clone())将MethodRouter<S>转换为MethodRouter<S2>,内部将每个 handler 从"等待状态注入"变为"状态已捕获的闭包"。这是状态注入的核心——handler 闭包捕获 state 的副本,后续调用时直接使用,不再需要从外部传入。Route 不变:通过
.route_service()注册的原始 Service 不依赖 State,所以直接透传。这也是为什么route_service不要求S参数——它注册的 Service 与状态无关。Node 共享:
self.node是Arc<Node>,直接转移所有权,不需要重建基数树。这是with_state只影响处理器不影响路由结构的关键——路径匹配逻辑和状态无关,基数树的结构不因状态注入而改变。
Fallback 的 with_state
Fallback::with_state 的实现(routing/mod.rs:743-749):
rust
fn with_state<S2>(self, state: S) -> Fallback<S2, E> {
match self {
Self::Default(route) => Fallback::Default(route),
Self::Service(route) => Fallback::Service(route),
Self::BoxedHandler(handler) => Fallback::Service(handler.into_route(state)),
}
}Default 和 Service 变体不依赖状态,直接透传。BoxedHandler 变体(通过 .fallback(handler) 设置的 handler)需要状态注入,调用 into_route(state) 转换为 Route,同时变体从 BoxedHandler 变为 Service。这个转换是一次性的——状态注入后,fallback 从"需要状态的 handler"变为"不需要状态的 service",后续请求直接调用,不再需要状态参数。
注意类型变化:Fallback<S, E> 变为 Fallback<S2, E>。S2 通常是 (),表示 fallback 不再依赖外部状态。这个类型变化与 Router<S> 到 Router<S2> 的变化一致,保证了类型状态的端到端一致性。
与 Hyper Dispatcher 的对照
如果你读过《Hyper与Tower》第 12 章,你会注意到 Hyper 的 Dispatcher 和 Axum 的 Router 解决的是同一个问题的不同层面。
Hyper 的 Dispatcher 在协议层做路由——它判断当前连接是 HTTP/1 还是 HTTP/2,选择对应的状态机来驱动请求-响应循环。对于 HTTP/2,Dispatcher 还需要在多个并发流之间做多路复用调度。它的输入是 TCP 字节流,输出是结构化的 Request<Body> 和 Response<Body>。
Axum 的 Router 在应用层做路由——它接收 Request<Body>(Hyper 已经把字节流解析成了结构化请求),根据 URL 路径和 HTTP 方法选择对应的 handler。它的输入是 Request,输出是 Response。
两者的共同点是:都是 Service 分发器——接收请求,根据某种策略选择下游 Service。区别在于分发的维度和粒度。Hyper 看协议版本和流 ID,Axum 看 URL 路径和 HTTP 方法。这个分层让两个系统各司其职:Hyper 不需要知道 URL 是什么,Axum 不需要管连接是 HTTP/1 还是 HTTP/2。
更深层的设计一致性在于"不可变路由表 + Clone 分发"的模式。Hyper 的 Server 对每个连接 clone 一个 Service;Axum 的 Serve 对每个连接 clone 一个 Router。两者都用 Arc 来避免深拷贝。这不是巧合——这是 Tower 的 Service trait 的 &mut self 语义所决定的:既然 call 需要 &mut self,而同一个 Service 实例不能被多个并发调用共享,那就只能 clone。
另一个值得对比的细节是错误处理。Hyper 的 Dispatcher 可能返回 hyper::Error(比如连接中断、协议错误),而 Axum 的 Router<()> 的 Error 类型是 Infallible——所有错误在到达 Router 之前都已经被 Handler 和中间件转换成了 Response。Hyper 的 Dispatcher 负责"协议层的错误必须被处理",Axum 的 Router 负责"应用层的错误必须变成响应"。两层各自守住了自己的边界。
小结
本章从 .route("/users/{id}", get(handler)) 这一行代码出发,拆解了 Axum 路由系统的完整架构:
Router<S>用Arc<RouterInner<S>>实现零成本 clone,用类型状态模式在编译期保证状态注入。S参数编码了"缺少什么状态",只有Router<()>才能作为Service被 serve。PathRouter<S>用Vec<Endpoint>存处理器、matchit::Router<RouteId>做路径匹配、双向 HashMap 维护路径与 ID 的双向映射。RouteId 是 Vec 索引的 newtype,匹配后 O(1) 取出 Endpoint。Node封装 matchit 基数树,提供 O(k) 的路径查询和参数提取。三个数据结构(基数树 + 两个 HashMap)协同工作:基数树做匹配,HashMap 做去重和反查。nest和merge是路由组合的两种方式。nest 将子 Router 展平后重新注册(附带前缀剥离和嵌套路径设置),merge 将两个 Router 的路由和 fallback 合并(自定义 fallback 不允许冲突)。Fallback三种变体覆盖默认 404、Service 接管、Handler 注入三种兜底场景,合并时遵守"唯一自定义 fallback"原则。fallback_endpoint在 PathRouter 中注册两条特殊路径,确保 Fallback 在路径匹配和方法匹配两个层面都能生效。with_state是类型状态的转换枢纽,将Router<S>变为Router<()>,急切地把所有 MethodRouter 转为已捕获状态的 Route。Node 共享不需要重建,转换只影响处理器。Router<()>实现Service,Error类型为Infallible,确保所有请求都有响应,错误不会逃逸到 hyper。into_make_service的急切转换确保每个连接的处理路径最短。route与route_service映射了 Handler 与 Service 的双层设计——前者接收待状态注入的 Handler,后者接收已就绪的 Service。layer与route_layer的区别在于中间件作用域——前者覆盖所有路由和 fallback,后者只覆盖已注册路由。
路由系统决定"请求该由谁处理",而 MethodRouter 决定"同一路径下不同 HTTP 方法该由谁处理"——这就是下一章要拆开的核心机制。