` 和 `matchit::Router`,把路由树的构建和匹配过程彻底拆开。">
Skip to content

第2章 Router:路径匹配与路由树

从一行 route 调用说起

每个 Axum 应用的起点几乎都长这样:

rust
let app = Router::new().route("/users/{id}", get(get_user));

看起来平淡无奇——注册一个路径、绑定一个处理器。但当这个 Routeraxum::serve 驱动后,每个进来的 HTTP 请求都要在微秒内完成路径匹配、参数提取、处理器分发。这一行代码背后,牵出了 Axum 路由系统的三个核心问题:

  1. 路径字符串如何变成可快速查询的结构? /users/{id} 这类带参数的路径,不能简单用 HashMap 做 key-value 查找,因为 {id} 是参数占位符,实际请求中可能是 /users/42/users/alice。Axum 选择了 matchit——一个基于基数树(radix tree)的路由匹配库,时间复杂度接近 O(k),k 为路径长度。基数树能在静态路径、参数路径、通配符路径之间做出正确的优先级判断,这是 HashMap 做不到的。

  2. Router 如何在并发连接间零成本共享? Tower 的 Service trait 要求实现 Clone,而每个 TCP 连接都会 clone 一次 Router。如果每次 clone 都深拷贝整个路由表,包括基数树、所有 handler 闭包、所有中间件栈,开销不可接受。Axum 用 Arc<RouterInner<S>> 让所有连接共享同一份路由表,clone 代价仅仅是原子加一。

  3. 状态如何在编译期而非运行期保证注入? Router<AppState> 代表"缺少一个 AppState",只有 Router<()> 才能真正处理请求。这个类型状态模式,让"忘记调用 with_state"变成编译错误而非运行时 panic。不需要靠文档或测试来保证状态注入,类型系统本身就是安全网。

这三个问题不是孤立的。Arc 的零拷贝 clone 决定了 Router 的运行时性能上限,matchit 的基数树决定了路径匹配的时间复杂度,类型状态模式决定了状态安全的保证级别。它们共同构成了 Axum 路由系统的骨架。

本章将沿着这三个问题,从 Router<S> 的结构定义,一路深入到 PathRoutermatchit::Router<RouteId>Fallback 分发机制、with_state 的类型转换、nestmerge 的路由组合,以及 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_servicewith_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_statelayer),它把 RouterInner 解构到模式中再重构。tap_inner! 用于需要修改内部字段的场景(如 routefallback),它取出 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 type S to be able to handle requests. Thus, only Router<()> (i.e. without missing state) can be passed to serve.

"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> 可以存储异构类型。

RouteIdroutes 的索引(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 派生了 CopyHashOrd 等 trait,这让它可以作为 HashMap 的 key,也可以在双向映射中高效比较。

Node——matchit 基数树加双向 HashMap

NodePathRouter 的核心索引结构(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>,
}

三层结构,各有分工:

  1. inner: matchit::Router<RouteId>:基数树,负责路径匹配。插入路径字符串时,matchit 将其解析为基数树节点;查询时沿树遍历,时间复杂度 O(k),k 为路径长度。这是整个路由系统的性能核心。

  2. route_id_to_path: HashMap<RouteId, Arc<str>>:从 RouteId 反查路径字符串。nestmerge 操作需要遍历另一个 Router 的所有路由并重新注册,此时需要通过 RouteId 拿到原始路径。没有这个映射,merge 就无法知道"第 3 个路由对应的路径是什么"。

  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)) 时,调用链如下:

  1. Router::route(routing/mod.rs:192-196):调用 tap_inner! 宏取出 RouterInner,调用 path_router.route(path, method_router)。注意 panic_on_err! 宏——如果路径校验或插入失败,直接 panic。这是 Axum 的设计选择:路由注册错误属于编程错误,不应该被静默忽略。

  2. 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 创建新端点。
  3. 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)手动
HashMapHashMap<String, Handler>O(1) 均摊不支持
基数树radix treeO(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)),
}

逐一拆解:

  1. self.node.at(uri.path()):调用 matchit 的基数树查询。at 方法沿基数树遍历,返回 Match<RouteId>MatchError::NotFoundMatch 包含两部分:value(存储在树中的 RouteId)和 params(提取的路径参数)。

  2. 取出 RouteIdmatch_.value&RouteId,解引用得到 RouteId(usize)。这个 usize 就是 routes Vec 的索引。

  3. 设置匹配路径:如果启用了 matched-path feature,调用 set_matched_path_for_request 将匹配的路径模板(如 /users/{id})写入 request extensions。这个信息供 MatchedPath extractor 使用——当你需要在 handler 中获取匹配的路径模板(而非实际请求路径)时,就用 MatchedPath。这在日志和监控场景中特别有用:你想知道请求匹配了哪个路由模板,而不是具体的 URL。

  4. 注入 URL 参数url_params::insert_url_params 将 matchit 提取的路径参数写入 request extensions。matchit 的 params 是一个键值对列表,如 id=42

  5. 取出 Endpointself.routes.get(id.0) 直接用 RouteId 索引 Vec。expect 中的消息是 "no route for id. This is a bug in axum. Please file an issue"——如果走到这一步说明 Axum 内部数据不一致,是框架 bug。

  6. 分发调用: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 内部使用的隐藏参数名,不能暴露给用户的 Path extractor。这两个参数是 nest_servicefallback_endpoint 在 matchit 树中注册路由时使用的通配符参数,用于匹配嵌套路由的剩余路径和 fallback 的所有路径。如果不过滤,用户的 Path extractor 就会收到这些内部参数,造成混淆。

  • 百分号解码PercentDecodedStr::new(v) 尝试对 URL 参数值做百分号解码。URL 中的非 ASCII 字符会被编码为 %XX 形式,Axum 在存储参数前尝试解码。如果解码失败(非法 UTF-8 字节序列),存储 InvalidUtf8InPathParam,后续 Path extractor 会返回 400 Bad Request,而不是让非法数据流入 handler。

路径参数支持嵌套叠加——当 nest 多层 Router 时,每层的参数会被 extend 到同一个 UrlParams::Params 中。比如外层 nest("/orgs/{org_id}", inner_router),内层注册了 /{project_id}/tasks,请求 /orgs/42/7/tasks 会提取两个参数:org_id=42project_id=7current_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 的项目(比如配合 utoipaopenapi3 使用),旧语法会造成不一致——路由定义用 :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::routePathRouter::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(())
}

关键步骤:

  1. 遍历子 Router 的所有路由:通过 route_id_to_path 反查每个路由的路径,拼接父路径前缀,得到最终路径。比如 nest("/api", sub_router),子 Router 有 /{id} 路径,最终注册为 /api/{id}

  2. 应用两层中间件StripPrefix::layer(prefix) 剥离请求 URL 的前缀(让子 Router 的 handler 看到的是去掉前缀后的路径),SetNestedPath::layer(path_to_nest_at) 设置嵌套路径(供 NestedPath extractor 使用)。这两个层包裹在每个 endpoint 上,确保嵌套后的 handler 行为与独立运行时一致。

  3. 逐条注册:每条路由独立注册到父 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_servicenest 的区别在于,它将一个 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(())
}

它注册了三条路由:

  1. /prefix/{*__private__axum_nest_tail_param}:匹配前缀后的所有路径,剩余部分存入隐藏参数。这是主要的匹配路径。
  2. /prefix:匹配前缀本身。/{*rest} 这种通配符路径不匹配空路径,所以需要单独注册前缀本身的路径。
  3. /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 是一个实现了 Handler trait 的异步函数,可能依赖 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 需要处理两种"不匹配":

  1. 路径精确不匹配:请求路径在基数树中完全找不到。此时 PathRouter::call_with_state 返回 Err,交由 catch_all_fallback 处理。

  2. 路径匹配但方法不匹配:请求路径匹配了某个端点,但 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_fallbackdefault_fallback 重置为 true,把 catch_all_fallback 重置为 Default(NotFound)。调用后再 merge,就不会触发冲突。

route 与 route_service:Handler 与 Service 的双入口

Router::routeRouter::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 中间件:layerroute_layer。它们的区别在于作用域,理解这个区别对正确使用中间件至关重要。

Router::layer(routing/mod.rs:296-309)对所有路由生效——包括已注册的路由和 fallback。它通过递归地对 PathRouterFallback 应用 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 类型是 InfallibleInfallible 是一个不可能构造的空枚举类型,意味着 Service::call 永远不会返回 Err。所有错误(包括 404、405、500)都被转换为正常的 Response 返回。这是 Axum 的核心设计哲学——HTTP 服务不应该有"未处理的错误",每一个请求都必须有响应。错误是响应的一种,不是异常。

  • Body 类型转换req.map(Body::new) 将任意满足约束的请求体转换为 Axum 内部的 Body 类型。Body 是对 hyper::Body 的封装,提供统一的 body 抽象。这个转换是必要的,因为 Tower 的 Service trait 允许不同的 body 类型,但 Axum 内部需要统一的 body 来处理提取器和响应。

into_make_service 的急切转换

into_make_serviceRouter<()> 的专属方法(routing/mod.rs:558-562):

rust
pub fn into_make_service(self) -> IntoMakeService<Self> {
    IntoMakeService::new(self.with_state(()))
}

看起来只是对 with_state(()) 的简单封装,但注释揭示了意图:

call Router::with_state such that everything is turned into Route eagerly rather than doing that per request

"eagerly"是关键词。with_state 会将所有 MethodRouter<S> 转换为 MethodRouter<()>,并将其内部的 handler 从"需要状态注入的闭包"变为"状态已捕获的 Route"。这个转换只做一次,之后每次请求直接调用已准备好的 Route,避免重复转换的开销。

如果不做急切转换会怎样?每次请求到来时,Router 需要检查每个 MethodRouter 是否已经注入状态,如果没有则注入。这个检查虽然开销不大,但在高并发下会累积。急切转换把这个一次性工作前移到启动阶段,让请求处理路径最短。

同样,在 axum::serveService<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_routercatch_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.nodeArc<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)),
    }
}

DefaultService 变体不依赖状态,直接透传。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 做去重和反查。
  • nestmerge 是路由组合的两种方式。nest 将子 Router 展平后重新注册(附带前缀剥离和嵌套路径设置),merge 将两个 Router 的路由和 fallback 合并(自定义 fallback 不允许冲突)。
  • Fallback 三种变体覆盖默认 404、Service 接管、Handler 注入三种兜底场景,合并时遵守"唯一自定义 fallback"原则。fallback_endpoint 在 PathRouter 中注册两条特殊路径,确保 Fallback 在路径匹配和方法匹配两个层面都能生效。
  • with_state 是类型状态的转换枢纽,将 Router<S> 变为 Router<()>,急切地把所有 MethodRouter 转为已捕获状态的 Route。Node 共享不需要重建,转换只影响处理器。
  • Router<()> 实现 ServiceError 类型为 Infallible,确保所有请求都有响应,错误不会逃逸到 hyper。into_make_service 的急切转换确保每个连接的处理路径最短。
  • routeroute_service 映射了 Handler 与 Service 的双层设计——前者接收待状态注入的 Handler,后者接收已就绪的 Service。
  • layerroute_layer 的区别在于中间件作用域——前者覆盖所有路由和 fallback,后者只覆盖已注册路由。

路由系统决定"请求该由谁处理",而 MethodRouter 决定"同一路径下不同 HTTP 方法该由谁处理"——这就是下一章要拆开的核心机制。

基于 VitePress 构建