axum Router::with_state 深度指南:状态注入、Router<S>泛型语义与最佳实践
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
Router::with_state是 axum 中为路由树注入全局共享状态的核心方法,也是理解Router<S>泛型参数、State提取器以及"为什么只有Router<()>才能启动服务"等一系列问题的关键入口。本文以官方文档 axum/src/docs/routing/with_state.md 为主体,结合 routing/mod.rs、path_router.rs 与 extract/state.rs 的源码实现,系统讲解with_state的用法、类型语义、从函数返回带状态路由器的正确姿势,以及与之相关的性能优化细节,帮助你写出类型安全、结构清晰、可复用的 axum 应用。
一、with_state是什么:为路由器提供全局状态
with_state为路由器提供状态(state)。传给该方法的 state 是全局的,路由器收到的所有请求都会使用它。这也是 axum 官方推荐的共享状态方式之一,用于存放数据库连接池、配置、服务客户端等跨请求共享的数据。
最基本的用法如下:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} let routes = Router::new() .route("/", get(|State(state): State<AppState>| async { // use state })) .with_state(AppState {}); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, routes).await;关键点有三:
- 状态类型必须
Clone:with_state内部会多次克隆 state(如 routing/mod.rs#L444-L450 中分别传给路径路由器与兜底 fallback 时都会state.clone()),因此 state 类型必须实现Clone。 - 处理器通过
State<S>提取器访问:见 extract/state.rs#L300-L331,State实现了FromRequestParts<OuterState>,其约束为InnerState: FromRef<OuterState>,也就是说处理器声明的状态类型可以与注入的状态类型不同(通过FromRef转换,后文详述)。 - 状态在每次请求时被克隆:正如 lib.rs#L186-L192 所述,state 对每个请求都会 clone。若字段本身克隆开销大,应将状态包裹进
Arc,或确保所有字段本身是廉价的克隆类型(如reqwest::Client、AWS SDK 客户端等内部已共享所有权、克隆廉价的类型,就无需再套一层Arc)。
全局状态 vs 请求派生数据:何时用Extension
with_state注入的状态是全局的,不适合存放从请求中派生出来的数据,例如在中间件中解析出来的授权(authorization)信息。这类与单个请求绑定的数据应使用Extension在请求处理链路中传递。选择依据很简单:
- 与请求无关、全局唯一 →
with_state+State提取器; - 由请求本身派生、每个请求不同 →
Extension请求扩展。
二、Router<S>到底意味着什么:缺失状态,而非拥有状态
理解Router<S>的语义是正确使用with_state的前提。官方文档明确强调:
Router<S>表示一个缺少类型为S的状态、因而还无法处理请求的路由器;它不表示一个"拥有"类型为S的状态的路由器。
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 一个"需要" AppState 才能处理请求的路由器 let router: Router<AppState> = Router::new() .route("/", get(|_: State<AppState>| async {})); // 调用 `with_state` 后,缺失的状态被补上,类型变为 Router<()> // 即"不再缺失任何状态" let router: Router<()> = router.with_state(AppState {}); // 只有 Router<()> 才有 into_make_service 方法 // 你不能在 Router<AppState> 上调用它,因为它仍缺失 AppState let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, router).await;这正是"为什么只有Router<()>才能启动服务"的原因:Router::into_make_service与Router::into_make_service_with_connect_info都只在impl Router(即Router<()>)上提供,见 routing/mod.rs#L538-L572。另外注意state 默认值是(),所以Router与Router<()>是同一个类型。
一个反直觉的事实:with_state不一定返回Router<()>
with_state的签名是pub fn with_state<S2>(self, state: S) -> Router<S2>(routing/mod.rs#L444),返回类型的泛型参数S2由调用者自行决定——你可以选择"下一个缺失的状态类型"是什么:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} let router: Router<AppState> = Router::new() .route("/", get(|_: State<AppState>| async {})); // 调用 with_state 时,我们可以挑选下一个缺失的状态类型 // 这里挑选 String let string_router: Router<String> = router.with_state(AppState {}); // 于是可以继续添加依赖 String 状态的新路由 let string_router = string_router .route("/needs-string", get(|_: State<String>| async {})); // 提供 String,并把新的缺失状态选为 () let final_router: Router<()> = string_router.with_state("foo".to_owned()); // 得到 Router<()> 即可启动服务 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, final_router).await;这种"链式补状态"的能力让路由器可以在不同阶段注入不同类型的依赖,非常适合分层组装(例如先注入数据库层状态,再注入业务层配置),是Router<S>泛型设计的精髓所在。
三、从函数返回带状态的路由器:三种推荐姿势
在大型应用中,路由通常由多个函数组装。官方文档给出了三种场景下的推荐写法,核心原则是:尽量推迟调用with_state。
场景一:不嵌套、不合并——不要在函数内调用with_state
如果路由器后续要作为整体启动,推荐函数内不调用with_state,返回Router<AppState>,在真正运行服务器之前再注入状态:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 不要在函数里调用 `Router::with_state` fn routes() -> Router<AppState> { Router::new() .route("/", get(|_: State<AppState>| async {})) } // 在运行服务器之前再提供状态 let routes = routes().with_state(AppState {}); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, routes).await;场景二:确实需要提供状态、且不嵌套/合并——返回Router(不带类型参数)
如果必须在函数内调用with_state,且该路由器不会被嵌套(nest)或合并(merge)进其他路由器,那么返回值应写成不带类型参数的Router:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 不要返回 Router<AppState> fn routes(state: AppState) -> Router { Router::new() .route("/", get(|_: State<AppState>| async {})) .with_state(state) } let routes = routes(AppState {});原因是:Router::into_make_service只能作用于Router<()>,不能作用于Router<AppState>(详见第二节的类型语义)。而Router默认泛型即(),恰好满足要求。
场景三:会被嵌套/合并——返回泛型状态Router<S>
如果该路由器会被嵌套或合并进另一个路由器,推荐在返回类型上使用泛型状态Router<S>,这样外层路由器可以自由决定最终的状态类型:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} fn routes<S>(state: AppState) -> Router<S> { Router::new() .route("/", get(|_: State<AppState>| async {})) .with_state(state) } let routes = Router::new().nest("/api", routes(AppState {})); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, routes).await;这正好呼应了 extract/state.rs#L59-L119 中关于"组合带状态路由器"的说明:nest/merge要求被组合的路由器具有相同的状态类型(一般可自动推断);当路由器定义在分离的作用域时,可能需要显式标注State类型。函数返回泛型Router<S>可以把"缺失什么状态"的决定权交给调用方,是最灵活的组合方式。
反例:返回Router<AppState>却不提供状态
下面的代码无法编译,因为它声称自己"仍缺失AppState",但调用方拿到的Router<AppState>无法调用into_make_service:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 无法工作:返回 Router<AppState> 意味着"仍缺失 AppState" fn routes(state: AppState) -> Router<AppState> { Router::new() .route("/", get(|_: State<AppState>| async {})) .with_state(state) } let app = routes(AppState {}); // 只有 Router<()> 才能调用 into_make_service,但 app 是 Router<AppState> let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await; // 编译错误正确做法是:既然状态已经全部提供,就返回Router<()>:
use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 已提供全部所需状态,因此返回 Router<()> fn routes(state: AppState) -> Router<()> { Router::new() .route("/", get(|_: State<AppState>| async {})) .with_state(state) } let app = routes(AppState {}); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await; // 编译通过四、性能提示:即使不需要状态,也建议调用with_state(())
如果你需要一个实现Service的Router,但并不需要任何状态(例如正在编写一个内部使用 axum 的库),官方文档建议在开始服务请求之前调用一次with_state(()):
use axum::{Router, routing::get}; let app = Router::new() .route("/", get(|| async { /* ... */ })) // 虽然不需要任何状态,还是调用一下 with_state(()) .with_state(());这不是必需的,但它会给 axum 一个更新路由器内部结构的机会,可能带来性能收益并减少内存分配。
源码层面可以印证这一点:Router::into_make_service(routing/mod.rs#L558-L562)与into_make_service_with_connect_info(routing/mod.rs#L567-L571)内部都会自动调用self.with_state(()),注释明确写道"调用Router::with_state以便把所有东西急切地(eagerly)转换为Route,而不是在每个请求时再做这件事"。这解释了为什么"先with_state再启动"会更快:状态注入触发了一次性的急切转换,避免了请求路径上的重复工作。
五、源码视角:with_state在底层做了什么
从 routing/mod.rs#L443-L450 可以看到with_state的实现:
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), }) }它做两件事:
- 把 state 克隆一份交给
path_router(负责匹配具体路径路由), - 把 state 交给
catch_all_fallback(兜底 fallback)。
继续深入 path_router.rs#L305-L322,PathRouter::with_state会遍历所有路由端点:对MethodRouter调用其自身的with_state(method_routing.rs#L837),从而把状态"烧录"进每个方法的处理器中;而已是Route的端点保持不变。整个调用链展示了 axum 如何将泛型状态在类型层面逐层下放、最终落到每个具体处理器。
State提取器的实现(extract/state.rs#L303-L331)则揭示了处理器侧的状态访问机制:
impl<OuterState, InnerState> FromRequestParts<OuterState> for State<InnerState> where InnerState: FromRef<OuterState>, OuterState: Send + Sync, { type Rejection = Infallible; async fn from_request_parts( _parts: &mut Parts, state: &OuterState, ) -> Result<Self, Self::Rejection> { let inner_state = InnerState::from_ref(state); Ok(Self(inner_state)) } }- 它不消费请求体(只取
Parts),因此必须放在任何 body 提取器之前; - 提取永远不会失败(
Rejection = Infallible); - 通过
FromRef支持"子状态"(substate)提取; State<S>实现了Deref/DerefMut(extract/state.rs#L319-L331),使用起来几乎等同于直接持有&S。
子状态(Substate)与FromRef
State只允许一种状态类型,但可以利用FromRef提取"子状态":顶层状态持有多个子状态字段,处理器各自声明自己需要的类型。FromRef可通过#[derive(FromRef)]派生(见 extract/mod.rs#L21-L27 中FromRef宏的 re-export)。这一模式常用于按模块拆分状态,配合第三节的泛型Router<S>组合方式,可以构建出边界清晰的模块化路由结构。
共享可变状态
由于with_state注入的状态在Router内是全局的,无法直接获取其可变引用(extract/state.rs#L256-L296)。需要共享可变状态时,基本方案是Arc<Mutex<_>>:若需跨.await持有锁,应使用tokio::sync::Mutex(持锁std::sync::Mutex跨.await会产生!Send的 future,与 axum 不兼容);否则可用std::sync::Mutex。
六、最佳实践总结
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 顶层应用启动 | Router::new()...with_state(AppState{})后直接serve | 得到Router<()>,可直接运行 |
| 组装函数返回路由器(不嵌套) | 函数内不调with_state,返回Router<AppState>;运行前再注入 | 保持类型信息,注入时机推迟到最后一刻 |
| 组装函数内必须注入(不嵌套) | 返回Router(即Router<()>) | 只有Router<()>可调用into_make_service |
会被nest/merge的路由器 | 返回泛型Router<S> | 让外层决定最终状态类型,组合最灵活 |
无状态但需要Service的路由器 | 先调用with_state(()) | 触发内部急切转换,提升性能、减少分配 |
| 请求派生数据(如授权信息) | 使用Extension | 状态是全局的,不适合请求级数据 |
| 共享可变状态 | Arc<Mutex<_>>(跨 await 用tokio::sync::Mutex) | 全局状态无法直接取可变引用 |
核心心法一句话:with_state是"补上缺失的状态",Router<S>的S是"还缺什么",而不是"已经有什么"。理解这一点,配合"尽量推迟注入、返回泛型Router<S>"的组装策略,就能写出类型安全、模块化、可测试的 axum 应用。更多状态共享模式的整体概览(State提取器、请求扩展、闭包捕获、任务局部变量等)可继续阅读 axum/src/lib.rs 中的 "Sharing state with handlers" 章节。
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考