ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

axum Router::with_state 深度指南:状态注入、`Router<S>` 泛型语义与最佳实践

axum Router::with_state 深度指南:状态注入、`Router<S>` 泛型语义与最佳实践 axum Router::with_state 深度指南状态注入、RouterS泛型语义与最佳实践【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axumRouter::with_state是 axum 中为路由树注入全局共享状态的核心方法也是理解RouterS泛型参数、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): StateAppState| async { // use state })) .with_state(AppState {}); let listener tokio::net::TcpListener::bind(0.0.0.0:3000).await.unwrap(); axum::serve(listener, routes).await;关键点有三状态类型必须Clonewith_state内部会多次克隆 state如 routing/mod.rs#L444-L450 中分别传给路径路由器与兜底 fallback 时都会state.clone()因此 state 类型必须实现Clone。处理器通过StateS提取器访问见 extract/state.rs#L300-L331State实现了FromRequestPartsOuterState其约束为InnerState: FromRefOuterState也就是说处理器声明的状态类型可以与注入的状态类型不同通过FromRef转换后文详述。状态在每次请求时被克隆正如 lib.rs#L186-L192 所述state 对每个请求都会 clone。若字段本身克隆开销大应将状态包裹进Arc或确保所有字段本身是廉价的克隆类型如reqwest::Client、AWS SDK 客户端等内部已共享所有权、克隆廉价的类型就无需再套一层Arc。全局状态 vs 请求派生数据何时用Extensionwith_state注入的状态是全局的不适合存放从请求中派生出来的数据例如在中间件中解析出来的授权authorization信息。这类与单个请求绑定的数据应使用Extension在请求处理链路中传递。选择依据很简单与请求无关、全局唯一 →with_stateState提取器由请求本身派生、每个请求不同 →Extension请求扩展。二、RouterS到底意味着什么缺失状态而非拥有状态理解RouterS的语义是正确使用with_state的前提。官方文档明确强调RouterS表示一个缺少类型为S的状态、因而还无法处理请求的路由器它不表示一个拥有类型为S的状态的路由器。use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 一个需要 AppState 才能处理请求的路由器 let router: RouterAppState Router::new() .route(/, get(|_: StateAppState| async {})); // 调用 with_state 后缺失的状态被补上类型变为 Router() // 即不再缺失任何状态 let router: Router() router.with_state(AppState {}); // 只有 Router() 才有 into_make_service 方法 // 你不能在 RouterAppState 上调用它因为它仍缺失 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_stateS2(self, state: S) - RouterS2routing/mod.rs#L444返回类型的泛型参数S2由调用者自行决定——你可以选择下一个缺失的状态类型是什么use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} let router: RouterAppState Router::new() .route(/, get(|_: StateAppState| async {})); // 调用 with_state 时我们可以挑选下一个缺失的状态类型 // 这里挑选 String let string_router: RouterString router.with_state(AppState {}); // 于是可以继续添加依赖 String 状态的新路由 let string_router string_router .route(/needs-string, get(|_: StateString| 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;这种链式补状态的能力让路由器可以在不同阶段注入不同类型的依赖非常适合分层组装例如先注入数据库层状态再注入业务层配置是RouterS泛型设计的精髓所在。三、从函数返回带状态的路由器三种推荐姿势在大型应用中路由通常由多个函数组装。官方文档给出了三种场景下的推荐写法核心原则是尽量推迟调用with_state。场景一不嵌套、不合并——不要在函数内调用with_state如果路由器后续要作为整体启动推荐函数内不调用with_state返回RouterAppState在真正运行服务器之前再注入状态use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 不要在函数里调用 Router::with_state fn routes() - RouterAppState { Router::new() .route(/, get(|_: StateAppState| 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进其他路由器那么返回值应写成不带类型参数的Routeruse axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 不要返回 RouterAppState fn routes(state: AppState) - Router { Router::new() .route(/, get(|_: StateAppState| async {})) .with_state(state) } let routes routes(AppState {});原因是Router::into_make_service只能作用于Router()不能作用于RouterAppState详见第二节的类型语义。而Router默认泛型即()恰好满足要求。场景三会被嵌套/合并——返回泛型状态RouterS如果该路由器会被嵌套或合并进另一个路由器推荐在返回类型上使用泛型状态RouterS这样外层路由器可以自由决定最终的状态类型use axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} fn routesS(state: AppState) - RouterS { Router::new() .route(/, get(|_: StateAppState| 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类型。函数返回泛型RouterS可以把缺失什么状态的决定权交给调用方是最灵活的组合方式。反例返回RouterAppState却不提供状态下面的代码无法编译因为它声称自己仍缺失AppState但调用方拿到的RouterAppState无法调用into_make_serviceuse axum::{Router, routing::get, extract::State}; #[derive(Clone)] struct AppState {} // 无法工作返回 RouterAppState 意味着仍缺失 AppState fn routes(state: AppState) - RouterAppState { Router::new() .route(/, get(|_: StateAppState| async {})) .with_state(state) } let app routes(AppState {}); // 只有 Router() 才能调用 into_make_service但 app 是 RouterAppState 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(|_: StateAppState| 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_servicerouting/mod.rs#L558-L562与into_make_service_with_connect_inforouting/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_stateS2(self, state: S) - RouterS2 { 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-L322PathRouter::with_state会遍历所有路由端点对MethodRouter调用其自身的with_statemethod_routing.rs#L837从而把状态烧录进每个方法的处理器中而已是Route的端点保持不变。整个调用链展示了 axum 如何将泛型状态在类型层面逐层下放、最终落到每个具体处理器。State提取器的实现extract/state.rs#L303-L331则揭示了处理器侧的状态访问机制implOuterState, InnerState FromRequestPartsOuterState for StateInnerState where InnerState: FromRefOuterState, OuterState: Send Sync, { type Rejection Infallible; async fn from_request_parts( _parts: mut Parts, state: OuterState, ) - ResultSelf, Self::Rejection { let inner_state InnerState::from_ref(state); Ok(Self(inner_state)) } }它不消费请求体只取Parts因此必须放在任何 body 提取器之前提取永远不会失败Rejection Infallible通过FromRef支持子状态substate提取StateS实现了Deref/DerefMutextract/state.rs#L319-L331使用起来几乎等同于直接持有S。子状态Substate与FromRefState只允许一种状态类型但可以利用FromRef提取子状态顶层状态持有多个子状态字段处理器各自声明自己需要的类型。FromRef可通过#[derive(FromRef)]派生见 extract/mod.rs#L21-L27 中FromRef宏的 re-export。这一模式常用于按模块拆分状态配合第三节的泛型RouterS组合方式可以构建出边界清晰的模块化路由结构。共享可变状态由于with_state注入的状态在Router内是全局的无法直接获取其可变引用extract/state.rs#L256-L296。需要共享可变状态时基本方案是ArcMutex_若需跨.await持有锁应使用tokio::sync::Mutex持锁std::sync::Mutex跨.await会产生!Send的 future与 axum 不兼容否则可用std::sync::Mutex。六、最佳实践总结场景推荐写法原因顶层应用启动Router::new()...with_state(AppState{})后直接serve得到Router()可直接运行组装函数返回路由器不嵌套函数内不调with_state返回RouterAppState运行前再注入保持类型信息注入时机推迟到最后一刻组装函数内必须注入不嵌套返回Router即Router()只有Router()可调用into_make_service会被nest/merge的路由器返回泛型RouterS让外层决定最终状态类型组合最灵活无状态但需要Service的路由器先调用with_state(())触发内部急切转换提升性能、减少分配请求派生数据如授权信息使用Extension状态是全局的不适合请求级数据共享可变状态ArcMutex_跨 await 用tokio::sync::Mutex全局状态无法直接取可变引用核心心法一句话with_state是补上缺失的状态RouterS的S是还缺什么而不是已经有什么。理解这一点配合尽量推迟注入、返回泛型RouterS的组装策略就能写出类型安全、模块化、可测试的 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),仅供参考
返回列表