一个 HTTP Server 内部包括很多部分:协议实现(h1、h2、压缩等)、连接状态管理(keepalive)、请求分发、中间件、业务逻辑等。用户可以全部自行实现,但其实除了业务逻辑之外的部分都是较为通用的能力。我们将这些通用能力与用户的业务逻辑解耦,即 HTTP 框架。
由于 Rust 生态中 hyper 已经提供了较为完备的 HTTP 协议实现,基于 hyper 实现 HTTP 框架就只需要提供包括路由、共享状态、中间件等能力。
本文从 HTTP 框架的设计角度,以新版本 Axum 作为例子,分析 Rust 下 HTTP 框架如何提供合理抽象与类型约束。基于 Rust 强大的类型系统,我们可以写出高效且正确的代码。
Handler 抽象
简单路由
通常对于不同请求 Path 的不同 Method,我们的行为是独立的。
根据 Path 和 Method 做分发属于 HTTP 框架需要提供的通用能力之一:比如我们需要能够将 GET / 和 POST /login 分发到不同的处理函数上。
最简单的路由可以通过 HashMap 实现。如下所示,Key 是 Path 和 Method,Value 是对应的处理函数,Path 和 Method 我们可以简单的用 String 来表示,处理函数该如何表达呢?
HashMap<(Path, Method), Handler>
我们先来解决 Handler 的问题,后面会重新讨论更合理的路由设计。
由于我们需要将 Handler 存储在 HashMap 中,所以我们需要一个固定类型来表示它(否则 HashMap 的 Value 大小无法确定)。在 Rust 中我们常常利用 Box<dyn Trait> 来将不同的具体类型统一为同一类型(在其他语言中对应指针)。
这就要求我们需要定义一个 Trait 来描述处理逻辑。
最简单的方案: 约束处理函数实现固定 Fn
由于处理逻辑一定是处理 HTTP Request,并得到 HTTP Response 的,所以一个直观的做法是直接将 hyper decode 出的 HTTP Request 结构作为参数,要求该函数返回 HTTP Response。
如果 handler 内部需要某个请求参数,那么它需要自行从 HTTP Request 中取并可能需要自行做反序列化等处理(如 Json)。
add_route(handler: H, ...)
where H: Fn(req: http::Request) -> http::Response { ... }
更用户友好的接口
先前方案的缺点是,用户使用非常麻烦,要自己提取参数。一个更用户友好的实现是框架来处理参数提取,用户只需要在参数中声明自己需要的东西,以及描述业务逻辑。
我们期望的效果是:
async fn index() -> &'static str {
"it works!"
}
async fn login(form: Form<...>) -> (StatusCode, String) { ... }
要将为这些 Handler 实现统一 Trait,我们要解决这几个问题:
-
约束每个参数都能从 HTTP Request 中提取到
-
约束函数返回值可以转换到 HTTP Response
-
支持变长参数(不同 Handler 的参数个数可以不一致)
-
支持 Async Fn
参数约束
我们期望能够在通过编译检查确保这些参数能够从 Request 中提取到。我们可以定义 Trait:
// framework code
pub trait FromRequest {
type Error;
fn from_request(req: &http::Request) -> Result<Self, Self::Error>;
}
例如对于需要从 HTTP Query 中提取参数的需求,用户就只需要实现这种代码:
// user code
#[derive(Deserilize)]
struct Filter {
keyword: String,
count: u32,
}
async fn search(Query(f): Query<Filter>) -> (StatusCode, String) { ... }
框架内实现辅助结构 Query 实现从 Query 参数中提取。
// framework code
pub struct Query<Q>(pub Q);
impl FromRequest for Query<Q>
where ... {
type Error = ...;
fn from_request(req: &http::Request) -> Result<Self, Self::Error> { ... }
}
除了使用框架内建的 Query、Form 等辅助结构,用户也可以自己实现 FromRequest。
通过这种方式,我们将请求参数提取与业务逻辑解耦,并做了类型约束,确保该结构必须实现如何从 Request 中抽取,该函数才实现 Handler,才能注册到 Router 上。
返回值约束
返回值约束和参数约束类似,我们约束返回值能够转换为 Response:
pub trait IntoResponse {
fn into_response(self) -> http::Response;
}
作为框架可以提供一些实现:
impl IntoResponse for &'static str {...}
impl IntoResponse for String {...}
impl IntoResponse for (StatusCode, String) {...}
借助 IntoResponse 我们可以支持用户 handler 返回自定义结构,将 Response 构建与业务逻辑解耦。
支持变长参数
有的 handler 不需要什么输入参数(如前面提到的返回 “hello world” 的 index 接口),有的需要一个或多个 Request 中提取到的参数。
那么这类函数如何统一抽象呢?在 Rust 中我们惯用的操作是定义 Trait,并为满足条件的 Fn 实现这个 Trait。对于参数数量不同的 Fn,我们需要手动为它们每个都实现 Trait。
由于这个工作枯燥无聊充满重复,所以往往使用宏来实现,最后看起来代码块像是一个“梯形”。
// from axum
macro_rules! all_the_tuples {
($name:ident) => {
$name!([], T1);
$name!([T1], T2);
$name!([T1, T2], T3);
$name!([T1, T2, T3], T4);
$name!([T1, T2, T3, T4], T5);
$name!([T1, T2, T3, T4, T5], T6);
$name!([T1, T2, T3, T4, T5, T6], T7);
$name!([T1, T2, T3, T4, T5, T6, T7], T8);
$name!([T1, T2, T3, T4, T5, T6, T7, T8], T9);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], T14);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], T15);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
};
}
我们需要一个统一的调用接口,并利用宏实现它:
pub trait Handler {
fn call(&self, req: http::Request) -> http::Response;
}
// illustrate for macro expand
impl<F, O> Handler for F
where F: Fn() -> O, O: IntoResponse { ... }
// illustrate for macro expand result
impl<F, O, T1> Handler for F
where F: Fn(T1) -> O, O: IntoResponse, T1: FromRequest {
fn call(&self, req: http::Request) -> http::Response {
let param1 = match T1::from_request(&req) {
Ok(p) => p,
Err(e) => return e.into(),
};
let resp = (self)(param1).into_response();
resp
}
}
// illustrate for macro expand
impl<F, O, T1, T2> Handler for F
where F: Fn(T1, T2) -> O, O: IntoResponse, T1: FromRequest, T1: FromRequest { ... }
// other impl blocks ...
最终我们调用时还是直接传入 http::Request 得到 http::Response。
Request 所有权问题
前面的接口中,我们约束请求参数提取器只能拿到一个 Request 的只读引用,原因是我们可能需要提取多个参数。这里可以做一个小优化:如果只有一个参数提取器,那么传入所有权;否则对最后一个传入所有权,前面的传入引用。
在 Axum 内有两个相关 trait: FromRequestParts 和 FromRequest。FromRequestParts 可以从 HTTP Request Head 中提取内容(传递引用),FromRequest 可以从 HTTP Request Head 和 Body 中提取(传递整个 Request 的所有权)。FromRequest 对于需要消费 Body 的提取器来说非常友好,但只会应用于最后一个提取器。
支持 Async Fn
通过上一小节的 Handler Trait,我们已经可以较为友好地定义 Handler 了。但是往往我们 handler 处理的过程会涉及数据库读写、下游 RPC 调用等,而这些网络操作是异步的,所以 handler 就不得不实现为 async fn。
Async Fn 本质上也是一个 Fn,和普通的 Fn 对比起来就是,async 解糖之后会将定义的返回值变为一个实现了 Future 的匿名结构,例如:
// before desuger
async fn example() -> String { ... }
// after desuger
fn example() -> AnonymousFut { ... }
struct AnonymousFut { ... }
impl Future for AnonymousFut {
type Output = String;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { ... }
}
(延伸阅读:关于 Rust 异步系统的一些基础知识可以参考这个分享)
为了支持 async fn 实现 Handler Trait,我们需要做出一些改动:
pub trait Handler {
fn call(&self, req: http::Request) -> BoxFuture<'static, http::Response>;
}
因为涉及到 Future 的具体类型,这里其实我们有两种做法:一种是将 type Future: Future<Output = http::Response> 作为 Trait 的关联类型,另一种是直接用 BoxFuture 将类型擦掉。
使用哪种主要取决于要不要擦除类型,以及想在哪里做类型擦除。一般来说,出现可能嵌套调用的场景(比如 tower Service 嵌套),最好在最后做类型擦除,这样可以尽可能减少 Box 使用,增大可以内联的代码段以达到更好的性能。
我们修改了 Trait,同样我们需要修改实现。以下是宏展开后的一个 demo 例子:
// illustrate for macro expand result
impl<F, O, T1> Handler for F
where F: Fn(T1) -> Fut, Fut: Future<Output = O>,
O: IntoResponse, T1: FromRequest {
fn call(&self, req: http::Request) -> BoxFuture<'static, http::Response> {
Box::pin(async move {
let param1 = match T1::from_request(&req) {
Ok(p) => p,
Err(e) => return e.into(),
};
let resp = (self)(param1).into_response();
resp
})
}
}
注:这里限制了我们一定要使用异步接口。如果要为返回 T: IntoResponse 的结构也实现 Handler,这样是有实现冲突的(因为都是 blanket impl)。
总结一下,这个小节里我们定义清楚了 Handler Trait 并自动为用户定义的符合某些条件的 async fn / fn 实现了 Handler。用户可以在函数的参数和返回值上使用一些提取器和生成器,让 handler 本身专注于业务逻辑。
共享状态
Handler 内部可能会需要一些外部量,比如初始化好的 db、或者用于缓存的 HashMap 等。我们一般会允许用户在创建 Route 的时候能够 attach 上某个 Stste(或者叫 Data),在 handler 函数参数中提取这个结构。
共享状态提取
我们完全可以将共享状态视作 Request 的一部分。
这样我们可以复用先前定义的 FromRequest。但是怎么区分我们想提取的 S(这是一个用户定义的类型)到底从哪里提取到呢?或者说如果用户想提取的 S 就是我们用于请求参数提取的 Query 呢?如果直接将 S 放在参数中,这时语义上就会有歧义。
为了解决这个歧义,我们可以定一个 State 类型。
pub struct State<S>(pub S);
之后为 State 实现 FromRequest 时,我们便知道,这个 S 一定是要从共享状态中提取的。
共享状态存储
顺着我们前面的思路,既然将共享状态视作 Request 的一部分,我们要么将 state 放入 Request 的某个地方,要么定义一个新的带有 state 的 Request 或通过新增参数传入。
利用 Request 本身存储 State
在 Actix 和之前版本的 Axum 中,状态存储都是采用这种方案。Request 内部有一个 type map 叫 extension,我们直接向其插入一个 KV:key = S; val = S{…} 即可。在我们提取时,使用 S 类型作为 key 即可查到我们在调用 handler 之前插入的这个 val。
看起来很方便且容易实现,但是这个方案放弃了静态检查类型的机会。如果用户 handler 参数中需要的 State 和 Route 上 attach 的 State 类型不一致的话,就只能在运行时处理了。这样会导致错误的代码可以编译通过,但运行时 100% 出错。
通过新增参数传入并添加类型约束
利用 Rust 的类型系统,我们可以将前面提到的问题提前到编译期暴露。
我们可以在 Handler、 FromRequest 和 Route 上添加泛型 S,并在 Route<S> 添加路由时做限定:
pub trait Handler<S> {
fn call(&self, req: http::Request, state: S) -> BoxFuture<'static, http::Response>;
}
pub trait FromRequest<S> {
type Error;
fn from_request(req: &http::Request, state: &S) -> Result<Self, Self::Error>;
}
pub struct Route<S> { ... }
impl<S> Route<S> {
pub fn new(state: S) -> Self { ... }
// we can only add Handler<S> to our self, not Handler<A> or Handler<B>
pub fn route<H: Handler<S>>(self, hander: H) -> Self { ... }
}
// illustrate for macro expand result
impl<F, O, S, T1> Handler<S> for F
where F: Fn(T1) -> Fut, Fut: Future<Output = O>,
O: IntoResponse, T1: FromRequest {
fn call(&self, req: http::Request, state: S) -> BoxFuture<'static, http::Response> {
Box::pin(async move {
let param1 = match T1::from_request(&req, &state) {
Ok(p) => p,
Err(e) => return e.into(),
};
let resp = (self)(param1).into_response();
resp
})
}
}
这样我们便能限制 handler fn 上的 State 类型。最后我们实现 State 辅助结构用于参数提取:
pub struct State<S>(pub S);
impl<S> FromRequest<S> for State<S>
where S: Clone {
type Error = Infallible;
fn from_request(req: &http::Request, state: &S) -> Result<Self, Self::Error> {
Ok(state.clone())
}
}
大功告成!我们现在可以放心地使用共享状态而不用担心传错类型导致运行时出问题了。If it compiles, it works.
更用户友好的共享状态
和我们前面讲的从 Request 提取一样,用户可能仅仅需要 Request 或 State 中的一部分。为了让它更易用,我们可以允许用户仅接收 State 的一部分。例如:
struct MyShared {
db: DBHandler,
cache: Cache,
counter: Counter,
}
async fn get_cache(State(cache): State<Cache>) -> String { ... }
async fn update_cache(State(cache): State<Cache>, State(db): State<DBHandler>) -> String { ... }
类似 FromRequest,我们可以定义一个 Trait 允许从一个大结构衍生出子结构。
// in `from` style
pub trait FromRef<T> {
fn from_ref(input: &T) -> Self;
}
// in `into` style
pub trait Param<T> {
fn param(&self) -> T;
}
FromRef 与 Param 两种风格对比
类似 From
和 Into
的关系,我们可以提供这两种风格的 Trait。这两种是等价的,只需要选择一种使用即可。
Axum 内使用 FromRef
这种定义,而 linkerd2-proxy 中我们可以找到 Param
这种风格的定义。
这两种使用哪种比较好呢?这个就取决于自己喜好啦(标准库提倡定义 From)~
注:这里并不违背孤儿原则,虽然 FromRef
和 String
都不是我们定义的,但因为 Example
是,所以 FromRef<Example>
不算 foreign type 。
struct Example {
demo: String,
}
impl FromRef<Example> for String { ... }
或:
struct Example {
demo: String,
}
impl Param<String> for Example { ... }
我们选取 Into 风格的 Param Trait 来实现本小节的功能:
impl<S, I> FromRequest<S> for State<I>
where S: Param<I> {
type Error = Infallible;
fn from_request(req: &http::Request, state: &S) -> Result<Self, Self::Error> {
Ok(state.param())
}
}
路由
路由是对 Handler 的组合与分发系统。
之前我们以一种最简单的路由引出了 Handler 概念,并介绍了 Handler Trait 约束并基于宏自动给用户定义的函数实现了 Handler。但距离真正易用的路由还有一段距离。
路由查找与合并
对于路由查找我们可以使用现成的库 matchit。使用 matchit,它允许我们注册 Path 和 Value,当我们请求匹配某个路径时,将符合的 Path 对应的 Value 返回给我们。Matchit 基于 radix trie 实现,比起 HashMap 支持前缀匹配和参数提取(如 /user/:id)。
简单实现的话,直接包装 matchit 即可做到路径匹配,但 matchit 的缺点是不支持遍历拿到注册时传入的 Path 和 Value。所以为了支持更多的功能(如 merge),我们就需要自己也维护一份全量的映射关系。
在 Axum 中,Handler 被表示为 Endpoint 类型。Router 中维护了两个信息:
-
RouteId → Endpoint
-
Node
-
matchit Router<RouteId>: 用于路径匹配
-
RouteId → Path
-
Path → RouteId
-
当查找路径时,直接丢给 matchit Router 查到对应的 RouteId,之后使用 RouteId 查找到 Endpoint。
当合并路由时,假设我们要将 B 合并到 A,我们会遍历 B 里的路由信息,得到所有的 (RouterId, Path, Endpoint) tuple,之后尝试合并到 A 中同 Path 上(对于 Path 相同 Method 不同的),如果不存在再另外创建新的。
例如,/login 和 /logout 合并时,会向 matchit Router 中插入对应 Path 和 RouterId;而 GET /login 和 POST /login 合并时,就不需要修改 matchit Router,直接合并对应 Endpoint 即可。
Endpoint
Axum 定义了 Endpoint 用于支持嵌套 Route 和根据 Method 分发。
enum Endpoint<S, B> {
MethodRouter(MethodRouter<S, B>),
Route(Route<B>),
NestedRouter(BoxedIntoRoute<S, B, Infallible>),
}
Route
pub struct Route<B = Body, E = Infallible>(BoxCloneService<Request<B>, Response, E>);
Route 是一个 async fn (Request) → Result<Response, Error>,它可以直接用来处理请求。当我们不需要根据 Method 做分发时,或者已经完成分发时,就可以直接将请求打到对应 Route 上处理。
BoxedIntoRoute
pub(crate) struct BoxedIntoRoute<S, B, E>(Box<dyn ErasedIntoRoute<S, B, E>>);
pub(crate) trait ErasedIntoRoute<S, B, E>: Send {
fn clone_box(&self) -> Box<dyn ErasedIntoRoute<S, B, E>>;
fn into_route(self: Box<Self>, state: S) -> Route<B, E>;
fn call_with_state(self: Box<Self>, request: Request<B>, state: S) -> RouteFuture<B, E>;
}
BoxedIntoRoute 擦掉了 Handler 类型,所以我们在其泛型上找不到 Handler 相关的标记,或 Handler 的关联类型等。
其 State 是待填充的,我们可以将 BoxedIntoRoute 理解为一个没有附加 State 的 Route。当附加 State 后它会变成一个真正的 Route,就是名字里 Into Route 的含义。
MethodRouter
pub struct MethodRouter<S = (), B = Body, E = Infallible> {
get: MethodEndpoint<S, B, E>,
head: MethodEndpoint<S, B, E>,
delete: MethodEndpoint<S, B, E>,
options: MethodEndpoint<S, B, E>,
patch: MethodEndpoint<S, B, E>,
post: MethodEndpoint<S, B, E>,
put: MethodEndpoint<S, B, E>,
trace: MethodEndpoint<S, B, E>,
fallback: Fallback<S, B, E>,
allow_header: AllowHeader,
}
enum MethodEndpoint<S, B, E> {
None,
Route(Route<B, E>),
BoxedHandler(BoxedIntoRoute<S, B, E>),
}
MethodRouter 内包含不同 Method 对应的 MethodEndpoint。MethodEndpoint 可能对应 None(该方法上没设置),也可能是先前提到的 Route 或 BoxedIntoRoute。
路由组合
我们以一个从 axum 中抄来的 example 看一下 Router 到底怎么创建的:
async fn main() {
...
let app = Router::new()
.route("/keys", get(list_keys))
.nest("/admin", admin_routes())
.with_state(Arc::clone(&shared_state));
...
}
async fn list_keys(State(state): State<SharedState>) -> String { ... }
fn admin_routes() -> Router<SharedState> {
async fn delete_all_keys(State(state): State<SharedState>) { ... }
async fn remove_key(Path(key): Path<String>, State(state): State<SharedState>) { ... }
Router::new()
.route("/keys", delete(delete_all_keys))
.route("/key/:key", delete(remove_key))
.layer(RequireAuthorizationLayer::bearer("secret-token"))
}
-
首先是 .route("/keys", get(list_keys)),这里通过 get 将一个函数 Box 起来转为了 MethodRouter<S, B, Infallible>;之后通过 route 将其插入到 Endpoint::MethodRouter 。由于当前还没附加 State,所以其内部类型对应 BoxedIntoRoute,即缺少 State 的那个类型。
-
之后我们看 .with_state(Arc::clone(&shared_state)):
pub fn with_state<S2>(self, state: S) -> Router<S2, B> { ... }
这里会附加 S 类型的 state 并重写泛型为新的类型 S2(S2 此时没有任何约束)。
with_state 内部会对所有 BoxedIntoRoute 附加 State 将其变为 Route 类型。
- .nest 会接收一个 prefix 和子路由。这时只需要将子路由作为 NestedRouter 插入即可。需要注意的是这里在丢给子路由之前,还需要额外去掉 prefix。
中间件
Tower Service
在介绍路由系统之前,我们先来熟悉一下用到的组件。
Tower 是 Rust 生态中的一个通用逻辑描述组件。Tower 提供一个 Service Trait 描述一个输入 Request 输出 Result<Response, Error> 的异步逻辑。只要是一个输入对应一个响应的逻辑基本都可以用这个 Trait 描述。
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
fn call(&mut self, req: Request) -> Self::Future;
}
Service 不只能帮助不同组件之间的接口对齐,我们还可以利用 tower 的一些辅助组件来做逻辑的组合。Tower 提供了 Layer 用于对 Service 做修饰,还提供了 Stack 用于 Layer 之间的嵌套。
pub trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}
pub struct Stack<Inner, Outer> {
inner: Inner,
outer: Outer,
}
impl<S, Inner, Outer> Layer<S> for Stack<Inner, Outer>
where
Inner: Layer<S>,
Outer: Layer<Inner::Service>,
{
type Service = Outer::Service;
fn layer(&self, service: S) -> Self::Service {
let inner = self.inner.layer(service);
self.outer.layer(inner)
}
}
简单总结一下就是:
-
Service 描述了 async fn (Request) → Result<Response, Error> 的通用逻辑。
-
Layer 是 Service 的修饰器,传入一个 Service 可以返回一个修饰后的 Service。
-
Stack 是 Layer 的组合,其本身也实现了 Layer,在 .layer 时会依次调用内部的所有 layer。
由于我们采用了 Service 中间件,并且 Route 本身实现了 Service,所以我们接收 layer 即可包装 Route。
对于当前还缺少 State 的 BoxedIntoRoute,我们先将 layer 暂存起来,等待填充 State 后再 layer 包装。
Server 创建及使用
我们将前面提到的路由、Handler 和 中间件组合到一起,就构成一个 Server,Server 允许用户 bind 并 serve MakeService:
axum::Server::bind(&addr)
.serve(router.into_make_service())
.await;
IntoMakeService<S> 实现了 Service<T, Response = S, Error = Infallible>。
Server::bind(…).serve(…) 会创建出来一个 Server;Server 实现了 Future,所以可以直接 await。在其 Future 实现中,会首先 accept 连接并利用 IntoMakeService<S> 生成 S,即用于处理请求的 Service(就是我们的 router)。
之后 io(连接)会和处理 Service 一起包装在一个 hyper::Connecting 结构中,构造 hyper::NewSvcTask 并将这个 task spawn 出去。
连接上的读写、解码等均由 hyper 负责,最后 hyper 使用将解码好的 http::Request 调用我们传入的 Service,即 Router。
Router 和 State 的泛型 trick
到这里本文已经基本分析完了:
-
如何约束 Handler
-
如何做路由查找与路由管理
-
如何支持共享状态
在阅读代码过程中,注意到 Axum 的有关 State 泛型的 trick,感觉很有新意。
如果你认真阅读了前面有关共享状态的部分,那么你会发现我的写法是这样的:
async fn index() -> String { ... }
let route = Route::new(state).route("index", get(index));
而 Axum 里支持这种做法:
let route = Route::new()
.route("1", get(index1))
.with_state(s1)
.route("2", get(index2))
.route("2", post(index2))
.with_state(s2)
.route("3", post(index3))
.with_state(s3)
Axum 中可以以链式的形式串起来不同的 Handler 和 State。这是怎么实现的呢?
Router 的 new 方法都没有对 S 加直接约束,也就是说可以理解为这里的 S 是 Any。当调用 route 方法时,Handler 实现可能会对 S 产生约束(也可能不产生)。在调用 with_state 方法时,S 的类型会被确定下来,此时:
-
会检查这个 S 能不能满足前面的约束
-
重置泛型 S 为 S2(即清空关联,清空约束)
最终在使用 router 时,会调用 into_make_service 并且会约束 Router 实现 Service,而这两个实现都是对 Router<()> 的实现,即 S 可以为 ()。
我们考虑下面几种情况:
-
前面注册的 handler 约束了 S,但没有 with_state:此时对 S 有约束(这个约束正常人不会写成 (),否则没有意义),那么 S 就不可能为 (),所以如果忘了附加 state 或者附加的 state 不对,那么就会编译失败。
-
前面注册的 handler 没有约束 S,也没有 with_state:此时对 S 没有约束,所以 S 可以为 (),那么即便没有调用 with_state ,一样是能编译通过的。
-
前面注册的 handler 约束了 S,最后又 with_state:这时 with_state 将 S 约束清空,所以 S 可以为 (),编译通过。
-
和前面的例子一样链式调用:每次 with_state 都会清空前面的约束,并在再次 .route 的时候添加约束,但只要最终是 with_state 或最后的一系列 .route 没有对 S 加以限制,那么 S 就可以为 (),编译通过。
通过这种 trick,Axum 中做到了 State 的静态检查,并支持在没有用到 State 的时候无需附加 State。