Appearance
第3章 Layer 与 ServiceBuilder:类型级中间件组合
3.1 问题:洋葱要正着剥
上一章结尾我们手写了一段假代码:
rust
let svc = Retry::new(policy,
RateLimit::new(rate,
Timeout::new(MyHandler)));这个写法至少有两件事让人不舒服。
第一,顺序反了。请求先过 Retry、再过 RateLimit、再过 Timeout、最后到达 MyHandler——这是它真实的执行顺序,但代码写出来却是反向嵌套的。读者看到 Retry::new(..., Timeout::new(handler)) 必须在大脑里把整个栈倒过来才能理解语义。
第二,类型会越堆越深。两层还勉强能看,堆到十层就变成无法排版的一团 <<<<>>>>。编辑器、rust-analyzer、编译错误信息一起遭罪。
Tower 的回答是 Layer trait + ServiceBuilder。它们是一套把嵌套结构拍平成链式方法的小语法糖。你在 Axum、Tonic 里看到的那种:
rust
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(CorsLayer::permissive())
.service(router)顺着写、按执行顺序读、每一行只讲一件事。背后不是运行时魔法,是两个 trait 加起来不到一百行代码。这一章我们把它读透。
3.2 Layer trait:只有四行
rust
// tower-layer/src/lib.rs:100-106
pub trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}一个关联类型,一个方法,四行。
它表达的意思是:
我知道怎么"包装"一个
S——喂给我一个S,我还给你一个Self::Service。
如果 Service 是"async fn(Req) -> Res",那么 Layer 就是"fn(Service) -> Service"——Service 的 Service。用函数式的话说,Layer 是 Service 范畴上的一个函子(functor),它把一个 Service 映射成另一个。
读一个例子(把上一章 Timeout 的 layer 版本展开):
rust
// tower/src/timeout/layer.rs 全文
#[derive(Debug, Clone, Copy)]
pub struct TimeoutLayer {
timeout: Duration,
}
impl TimeoutLayer {
pub const fn new(timeout: Duration) -> Self {
TimeoutLayer { timeout }
}
}
impl<S> Layer<S> for TimeoutLayer {
type Service = Timeout<S>;
fn layer(&self, service: S) -> Self::Service {
Timeout::new(service, self.timeout)
}
}Layer 自己不是 Service——它只是一个"工厂",把任意 S 翻译成 Timeout<S>。TimeoutLayer::new 只配置"超时多久",真正产生 Service 发生在 layer(svc) 被调用的那一刻。
这种"配置与实例化分离"的设计,是整个 Tower 中间件的统一模式。每个中间件都有一对结构体:
| 配置结构体(Layer) | 实例结构体(Service) |
|---|---|
TimeoutLayer | Timeout<S> |
RateLimitLayer | RateLimit<S> |
BufferLayer | Buffer<S, Req> |
ConcurrencyLimitLayer | ConcurrencyLimit<S> |
RetryLayer<P> | Retry<P, S> |
FilterLayer<F> | Filter<S, F> |
左边只描述"怎么配置",右边是真正跑业务的 Service。这让配置本身可以独立存在、可以 Clone、可以存到 Vec 里、可以通过 serde 反序列化——而不需要提前绑定到一个具体的 S。
3.2.1 为什么不是直接给 Service 加构造函数?
你可能会问:既然每个 Middleware 都有 MiddlewareLayer 和 Middleware 成对出现,为什么不直接用 Middleware::new(svc, config) 的构造函数,何必多出一个 Layer trait?
答案在"组合"二字。如果没有 Layer trait,每一个"组合工具"都要手动适配每一个中间件——ServiceBuilder::buffer 知道 Buffer::new 的参数顺序、ServiceBuilder::timeout 知道 Timeout::new……每加一个新中间件,ServiceBuilder 都得改代码。
有了 Layer trait 之后,组合工具只需要面对一个统一接口:fn layer(inner) -> wrapped。ServiceBuilder 完全不用知道每个中间件的构造函数长什么样——它只负责"把一个 Layer 堆到下一个 Layer 上面",剩下的事情由每个 Layer 自己的 impl Layer for MyLayer 负责。
这就是工程上著名的 open-closed 原则:对扩展开放(任何人可以实现 Layer),对修改封闭(ServiceBuilder 不需要修改)。
3.3 Stack:两个 Layer 叠起来
既然 Layer 是"Service 到 Service 的函数",把两个 Layer 组合起来,就是函数复合。Tower 把它叫做 Stack:
rust
// tower-layer/src/stack.rs:21-53
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)
}
}拆开看:
Stack<Inner, Outer>自己还是一个Layer<S>——它本身还能被继续叠。layer(s)的实现是"先让 Inner 包一层,再让 Outer 包一层"。- where 子句是关键:
Inner: Layer<S>说 Inner 能包 S;Outer: Layer<Inner::Service>说 Outer 能包 Inner 产出的那个 Service。类型系统在这里钉住了顺序——你不能把两个不兼容的 Layer 叠起来,compiler 会在类型检查阶段拒绝。
注意字段名的语义倒置:inner 存的其实是"先被应用的"那个 layer,outer 存的是"后被应用的"。这两个词的含义是从"最终产出的 Service 洋葱"角度来看:inner 更靠近业务核心、outer 更靠近请求入口。
这件事第一次看容易绕。再用一张图:
ServiceBuilder::new()
.layer(A) // outer-most
.layer(B)
.layer(C) // inner-most
.service(svc)
│
▼
请求流向: req ─> A ─> B ─> C ─> svc ─> C' ─> B' ─> A' ─> resp先加入 builder 的 layer 在外、后加入的在内。请求从外向内穿透,响应再从内向外返回。Stack<Inner, Outer> 的字段名正好反映了洋葱结构——inner 在里、outer 在外。
3.3.1 Identity:零元
rust
// tower-layer/src/identity.rs:22-45
pub struct Identity { _p: () }
impl Identity {
pub const fn new() -> Identity { Identity { _p: () } }
}
impl<S> Layer<S> for Identity {
type Service = S;
fn layer(&self, inner: S) -> Self::Service { inner }
}一个"什么都不做"的 Layer。它的存在是出于代数完整性——任何 monoid(幺半群)结构都需要一个零元。Stack 是乘法,Identity 就是 1。
看这个 ServiceBuilder::new:
rust
// tower/src/builder/mod.rs:117-123
impl ServiceBuilder<Identity> {
pub const fn new() -> Self {
ServiceBuilder { layer: Identity::new() }
}
}刚创建出来的 ServiceBuilder<Identity> 表示"还没加任何中间件的空栈"。当你调用 .layer(A),类型变为 ServiceBuilder<Stack<Identity, A>>;再调用 .layer(B),变成 ServiceBuilder<Stack<Stack<Identity, A>, B>>。每次调用都把类型"加深"一层。
Stack<Identity, A> 和直接 A 在语义上是等价的(因为 Identity.layer(s) = s),但 compiler 不会把它们视为同一个类型。这一般不是问题——只是单态化之后会多生成一层 wrapper struct。LLVM 的 inliner 在 release 模式下会把这一层彻底消除,但 debug 版本的类型名会很长。
3.3.2 tuple impls:糖里糖外
tower-layer 还给 tuple 写了一系列 Layer impl(tower-layer/src/tuple.rs):
rust
impl<S, L1, L2> Layer<S> for (L1, L2)
where L1: Layer<L2::Service>, L2: Layer<S>,
{
type Service = L1::Service;
fn layer(&self, s: S) -> Self::Service {
let (l1, l2) = self;
l1.layer(l2.layer(s))
}
}
impl<S, L1, L2, L3> Layer<S> for (L1, L2, L3)
where L1: Layer<L2::Service>, L2: Layer<L3::Service>, L3: Layer<S>,
{ ... }以及更多元组元数(最多到 16 元组)。一个空元组 () 也是 Layer,它和 Identity 效果一样:
rust
impl<S> Layer<S> for () {
type Service = S;
fn layer(&self, s: S) -> Self::Service { s }
}有了 tuple impls,你可以不用 ServiceBuilder 也能快速组合:
rust
let layers = (LogLayer::new(), TimeoutLayer::new(d), MetricsLayer::new());
let svc = layers.layer(handler);这让 Layer 成为一个可以在 Vec、HashMap、函数返回值里自由携带的一等公民。它不绑定到一个特定的 Service 实例,只在需要的时候 .layer(something) 就地实例化。
3.4 ServiceBuilder:糖
rust
// tower/src/builder/mod.rs:106-108
pub struct ServiceBuilder<L> {
layer: L,
}就这一个字段。看起来根本没必要单独搞一个 struct——直接用 Layer 本身不就行了?
原因有两个。第一,名字要好看。ServiceBuilder::new().layer(A).layer(B) 读起来像流畅的业务代码;而写成 Stack::new(B, Stack::new(A, Identity)) 就不像了。第二,ServiceBuilder 在纯粹的 Layer 之上还提供了"快捷方法"——.timeout(duration) 等价于 .layer(TimeoutLayer::new(duration)),省掉两个字符和一次命名选择。
3.4.1 .layer(T) 到底做了什么
rust
// tower/src/builder/mod.rs:132-136
impl<L> ServiceBuilder<L> {
pub fn layer<T>(self, layer: T) -> ServiceBuilder<Stack<T, L>> {
ServiceBuilder {
layer: Stack::new(layer, self.layer),
}
}
}三件事:
- 把当前的
self.layer(类型L)扔进Stack::new的第一个参数位置(即inner); - 新传入的
layer(类型T)扔到第二个参数位置(即outer)——等等,真的是 outer 吗?
让我们对着 Stack::new 的签名再看一眼:
rust
// tower-layer/src/stack.rs:36-38
pub const fn new(inner: Inner, outer: Outer) -> Self {
Stack { inner, outer }
}Stack::new(inner, outer)——第一个参数是 inner。
再看 ServiceBuilder::layer(T) 里那一句 Stack::new(layer, self.layer)——传入的新 layer 是 inner,旧的 self.layer 是 outer。
等等,这和前面讲的"先加入 builder 的 layer 在外"冲突了吗?
其实没冲突。让我们把一个具体例子完整展开一次:
rust
let sb0 = ServiceBuilder::new(); // ServiceBuilder<Identity>
let sb1 = sb0.layer(A); // ServiceBuilder<Stack<A, Identity>>
// sb1.layer.inner = A, sb1.layer.outer = Identity
let sb2 = sb1.layer(B); // ServiceBuilder<Stack<B, Stack<A, Identity>>>
// sb2.layer.inner = B, sb2.layer.outer = Stack<A, Identity>
let svc = sb2.service(handler);
// = sb2.layer.layer(handler)
// = Stack<B, Stack<A, Identity>>::layer(handler)
// = Stack<A, Identity>::layer(B::layer(handler)) <-- 注意:outer.layer(inner.layer(s))
// = Stack<A, Identity>::layer(WrappedBy_B)
// = Identity::layer(A::layer(WrappedBy_B))
// = Identity::layer(WrappedBy_A_then_B)
// = WrappedBy_A_then_B看懂了没?虽然字段命名是 Stack { inner, outer },但 Stack::layer 的定义是:
rust
fn layer(&self, s: S) -> Self::Service {
let inner = self.inner.layer(s); // 先应用 inner
self.outer.layer(inner) // 再让 outer 包在 inner 的结果外
}Stack::new(layer, self.layer) 里,新的 layer(B)被存为 inner——意味着在执行 stack.layer(handler) 时,B 会先应用到 handler 上。而旧的 self.layer(Stack<A, Identity>,代表 A)被存为 outer——A 会后应用。
但是"B 先应用到 handler"意味着什么?意味着 handler 被 B 包了一层,然后这个结果又被 A 包一层——最终形成 A<B<handler>>。从请求流向看:请求先进 A、再进 B、最后到 handler。
所以虽然字段命名直觉上让人困惑,结论是对的:先加入 builder 的 layer(A)在最外层,后加入的 B 在内层。
这个"字段名倒过来"的小细节来自一个历史修改——早期版本 Stack 的字段命名是反的,后来 @hawkw 在 tower#438 PR 里把字段重命名为 inner/outer 以反映"组合结果"的结构,而不是"传参顺序"的结构。注释里也明确写道:
Also, the order of [outer, inner] is important, since it reflects the order that the layers were added to the stack. (见
tower-layer/src/stack.rs:77)
一旦你理解这件事,后面读源码不会再有任何模糊。
3.4.2 快捷方法:便利但不神奇
ServiceBuilder 定义了大量 .timeout()、.buffer()、.concurrency_limit()、.rate_limit() 之类的快捷方法,它们的实现无一例外是"调用 self.layer(SomeLayer::new(...))":
rust
// tower/src/builder/mod.rs 典型片段
#[cfg(feature = "buffer")]
pub fn buffer<Request>(
self,
bound: usize,
) -> ServiceBuilder<Stack<crate::buffer::BufferLayer<Request>, L>> {
self.layer(crate::buffer::BufferLayer::new(bound))
}
#[cfg(feature = "limit")]
pub fn concurrency_limit(self, max: usize)
-> ServiceBuilder<Stack<crate::limit::ConcurrencyLimitLayer, L>>
{
self.layer(crate::limit::ConcurrencyLimitLayer::new(max))
}为什么要单独提供这些方法,而不是让用户都写 .layer(BufferLayer::new(bound))?三个小原因:
- 少写一个类型名——
buffer(100)比layer(BufferLayer::new(100))短得多。 - 避免导入——用户不用
use tower::buffer::BufferLayer,读 ServiceBuilder 的 docs 就知道有这个能力。 - feature gate 可以集中——每个快捷方法都带
#[cfg(feature = "xxx")],用户不开 feature 时方法直接消失,错误信息会指向 ServiceBuilder 而不是遥远的 crate。
3.4.3 .service(s):收官
rust
// tower/src/builder/mod.rs:489-494
pub fn service<S>(&self, service: S) -> L::Service
where L: Layer<S>,
{
self.layer.layer(service)
}所有的 .layer(...) 调用都只是在积累类型——直到 .service(s) 被调用,才真正把这堆 Layer 应用到 Service 上。整个函数只有一行:self.layer.layer(service)。
这一行的意思是:把 self.layer 这个 Layer(可能是一棵很深的 Stack 树)应用到给定的 service 上。编译器会顺着 Stack::layer 的 impl 递归展开,最终单态化成一个具体的、没有虚方法的大 struct。
你会看到 ServiceBuilder 还有一个 into_inner 方法——返回内部的 Layer:
rust
pub fn into_inner(self) -> L { self.layer }这让你可以"把 ServiceBuilder 当一个 Layer 工厂用":构造好整条链,但暂时不绑定 Service,把 Layer 存起来或传给别人。Axum 的 Router::layer() 就接受 impl Layer<Route>——你可以直接把 ServiceBuilder 里摘出来的 Layer 传过去。
3.5 让编译器展开一次
让我们做一个思想实验——给一段真实代码手工做 monomorphization。
rust
let svc = ServiceBuilder::new()
.layer(LogLayer)
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.service(handler);按照上面讨论的类型演化:
| 表达式 | 类型 |
|---|---|
ServiceBuilder::new() | ServiceBuilder<Identity> |
.layer(LogLayer) | ServiceBuilder<Stack<LogLayer, Identity>> |
.layer(TimeoutLayer::new(...)) | ServiceBuilder<Stack<TimeoutLayer, Stack<LogLayer, Identity>>> |
.service(handler) | Timeout<LogService<Handler>> |
最后一步是怎么算出来的?
Stack<TimeoutLayer, Stack<LogLayer, Identity>>::layer(handler)
= outer.layer( inner.layer(handler) )
= TimeoutLayer::layer( Stack<LogLayer, Identity>::layer(handler) )
= TimeoutLayer::layer( LogLayer::layer(Identity::layer(handler)) )
= TimeoutLayer::layer( LogLayer::layer(handler) )
= TimeoutLayer::layer( LogService<Handler> )
= Timeout<LogService<Handler>>最终 svc 的类型就是 Timeout<LogService<Handler>>。编译器把所有的 Layer 抽象消除得一干二净——没有 Box,没有 vtable,没有任何运行时间接跳转。请求过来时:
svc.call(req)
= Timeout::<LogService<Handler>>::call(&mut svc, req) // 展开为 ResponseFuture { resp: inner.call(req), sleep }
= LogService::<Handler>::call(&mut inner, req) // 打 log,然后 handler.call(req)
= Handler::call(&mut h, req)最后链式调用全部在编译期确定、全部被 inliner 展平。一个十层中间件的栈和手写十个嵌套函数的性能完全一致。
这种"类型金字塔"的代价是类型名巨长。Rust 编译器 debug 模式下会生成 "core::pin::Pin<alloc::boxed::Box<dyn core::future::future::Future<Output = ...>>>" 这种名字——但错误信息里出现的超长类型是代价,不是 bug。
3.6 Layer 的代数结构
从数学上看,Layer<S>、Stack、Identity 三者构成一个 monoid:
- 集合:所有
Layer<S>实例; - 二元运算:
Stack,满足结合律(Stack::new(Stack::new(A, B), C)和Stack::new(A, Stack::new(B, C))产生相同的 Service); - 单位元:
Identity,满足Stack(Identity, L) = L和Stack(L, Identity) = L。
这不是随意的数学点缀——如果 Layer 没有 monoid 性质,你就没法写出 ServiceBuilder 这样的链式 API。因为链式构造本质上是一个幺半群的左折叠(left fold):
foldl Stack Identity [Layer1, Layer2, Layer3]
= Stack(Stack(Stack(Identity, Layer1), Layer2), Layer3)monoid 性质保证了加入顺序和实际嵌套层级可以任意拆分,不会影响最终产物。写代码时这件事你感觉不到,但编译器在推导 Stack<Stack<Stack<...>>> 时就是在做这个折叠。
顺带一提,读过卷四《Serde 元编程》的读者会想起第 8 章讨论过的 quote::quote! 宏——它也在拼接 TokenStream,用的同样是 monoid 折叠思路。再往上,读过 Haskell 的 Endo、Scala 的 Monad transformer stacks、甚至 React 的 compose(f, g, h) ——这些模式共享同一个数学骨架。整个程序员行业,都在不同层次重新发明 monoid。
3.7 写自己的 Layer
掌握 Layer trait 之后,写一个自定义中间件是件轻松事。给一个完整的例子——ElapsedLayer,给每个请求打印 handler 的耗时:
rust
use std::task::{Context, Poll};
use std::time::Instant;
use std::pin::Pin;
use std::future::Future;
use tower::{Service, Layer};
use pin_project_lite::pin_project;
#[derive(Clone, Copy, Default)]
pub struct ElapsedLayer;
impl<S> Layer<S> for ElapsedLayer {
type Service = Elapsed<S>;
fn layer(&self, inner: S) -> Self::Service { Elapsed { inner } }
}
#[derive(Clone)]
pub struct Elapsed<S> { inner: S }
impl<S, Req> Service<Req> for Elapsed<S>
where S: Service<Req>,
{
type Response = S::Response;
type Error = S::Error;
type Future = ElapsedFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Req) -> Self::Future {
ElapsedFuture {
inner: self.inner.call(req),
start: Instant::now(),
}
}
}
pin_project! {
pub struct ElapsedFuture<F> {
#[pin] inner: F,
start: Instant,
}
}
impl<F, T, E> Future for ElapsedFuture<F>
where F: Future<Output = Result<T, E>>,
{
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let out = std::task::ready!(this.inner.poll(cx));
println!("handled in {:?}", this.start.elapsed());
Poll::Ready(out)
}
}这段代码严格按照 Tower 的中间件模式写:
- Layer struct 只存配置(这里没有任何配置,所以是 ZST);
- Service struct 持有被包裹的 inner Service;
- Future struct 持有计时起点 + inner future;
- poll_ready 透传;
- call 构造 future,不做 await;
- Future::poll 做真正的测量逻辑。
用法:
rust
let svc = ServiceBuilder::new()
.layer(ElapsedLayer)
.timeout(Duration::from_secs(10))
.service(handler);就加一行。运行起来你会看到每个请求打印耗时。
3.8 层数深了怎么办——类型擦除
Layer 的"类型金字塔"在大多数情况下不是问题——编译器帮你处理。但有些场景下你必须把它"拍平":
- 需要把多种不同链路的 Service 放进一个
Vec<Service>; - 需要把 Service 作为
dyn Trait对象跨模块传递; - 编译时间被类型推导拖慢(罕见,但确实发生过)。
Tower 提供了 BoxService 和 BoxCloneService:
rust
use tower::util::BoxCloneService;
let svc: BoxCloneService<Request, Response, BoxError> =
BoxCloneService::new(
ServiceBuilder::new()
.timeout(Duration::from_secs(10))
.service(handler)
);BoxCloneService 内部是 Arc<dyn Service<...>>,牺牲一次虚方法调用换取类型擦除。代价是每次 call 都要堆分配一个 future(因为 trait object 的 future 类型必须被 boxed)。
什么时候该擦除:业务路由分发层——各路由的中间件栈可能完全不同,需要统一类型装到路由表里。什么时候不该擦除:hot path(每秒百万请求的代理),类型擦除的成本会累积起来。工程上的判断线大约在"每请求多余 100-300 纳秒的开销是否可接受"。
3.9 和 Vue 3 的 computed effect 对照一眼
我们读过卷五《Vue 3 设计与实现》关于 alien-signals 的章节——Vue 的响应式系统也有一套"一层包一层"的抽象:ref → computed → effect,每一层都接受底下一层产生的响应式对象,返回一个新的响应式对象。
两者的相似点:
- 都是函数式组合——A(B(C(x))) 形式的层层装饰。
- 都依赖编译期(或运行时)类型推导——Vue 的 computed 用 JS 闭包推导依赖,Tower 的 Layer 用 trait 推导。
- 都有**"什么时候开始传播"的显式动作**——Vue 需要
effect.run(),Tower 需要.service(handler)才触发实际应用。
两者的差别:
- Vue 的组合是运行时发生的(每次
computed(() => ...)都在构造闭包);Tower 的组合是编译期发生的(所有 Layer 组合被单态化成具体类型)。 - Vue 的目标是追踪响应式变化——订阅和更新是核心;Tower 的目标是装饰行为——每层都可能修改请求/响应的处理路径。
理解这类"装饰器 + 单态化"模式,让你在 Rust 以外的语言里也能快速看穿类似代码。它们本质都是 monoid 折叠。
3.10 与错误处理的暗礁
Layer 的组合看起来优雅,但隐藏一个工程陷阱:错误类型会越来越复杂。
假设你要做这么一个栈:
Timeout → Retry → Buffer → MyHandlerMyHandler::Error = MyErrorBuffer<MyHandler>::Error = BoxError(因为 Buffer 需要统一 drop 错误)Retry<Buffer<MyHandler>>::Error = BoxErrorTimeout<Retry<Buffer<MyHandler>>>::Error = BoxError
请求过来,最里层的 MyHandler 产生了 MyError::NotFound,它被封装成 Box<dyn Error + Send + Sync>。Timeout 层看到的是 BoxError,完全不知道底下是什么具体错误——它只能透传。
上层拿到 Result<Response, BoxError> 时,想判断"是不是 NotFound"就得做 downcast:
rust
match result {
Err(e) => match e.downcast_ref::<MyError>() {
Some(MyError::NotFound) => ...,
_ => ...,
}
}这是 Tower 的一个长期痛点——错误类型擦除是组合性的代价。不同中间件用不同的错误类型(Timeout 的 Elapsed、Retry 的 Retries、RateLimit 的 Overloaded),合起来必须有一个统一的容器,BoxError 就是这个容器。
实务上你会看到两种应对:
- 让中间件只处理超集错误。Tonic 的
tonic::Status就是一个覆盖所有可能 gRPC 错误的超集。Tonic 里每个中间件的 Error 都是tonic::Status,不走 BoxError 那一套。 - 顶层统一 downcast。Axum 在最外层做一个统一的
ErrorHandler,把 BoxError 下塑回具体类型、生成恰当的 HTTP 响应。
这两个模式在第 22 章(Axum / Tonic 如何构建在 Hyper + Tower 之上)会再详细讨论。
3.11 小结:落到你键盘上
我们本章做了五件事:
- 读完了
Layertrait 的全部源码——4 行。 - 拆清楚
Stack的字段语义,解开"为什么 inner 字段存的是后加入的 layer"的困惑。 - 读完
ServiceBuilder的构造、.layer(T)、.service(S)三个关键方法的源码,完整手工 monomorphize 了一条中间件链。 - 讨论了 Layer 的 monoid 代数结构,和 Serde、Vue 3、函数式 compose 的思想对照。
- 手写了一个
ElapsedLayer完整实现,讨论了类型擦除、错误处理两个工程考量。
落到你键盘上的三件事:
- 打开
cargo expand,用上面ElapsedLayer的例子或者一段真实 axum 代码,展开看编译器生成了什么。你会看到一串impl Service for Timeout<LogService<Handler>>。 - 读
tower/src/builder/mod.rs的全部快捷方法。不长,大概 800 行,绝大部分是简单的self.layer(SomeLayer::new(...))。读完你会对 Tower 的全部内置中间件有个完整印象。 - 自己写一个非平凡的 Layer。比如一个
MetricsLayer——在call开始时记下 request 类型、在 response future 完成后记录耗时、失败时递增错误计数器。这是最好的学习方式。
下一章我们回到最微妙也最值得深挖的一件事:poll_ready 到底在做什么、为什么这套背压协议需要 &mut self、为什么很多人用错了。