Skip to content

第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。用函数式的话说,LayerService 范畴上的一个函子(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)
TimeoutLayerTimeout<S>
RateLimitLayerRateLimit<S>
BufferLayerBuffer<S, Req>
ConcurrencyLimitLayerConcurrencyLimit<S>
RetryLayer<P>Retry<P, S>
FilterLayer<F>Filter<S, F>

左边只描述"怎么配置",右边是真正跑业务的 Service。这让配置本身可以独立存在、可以 Clone、可以存到 Vec 里、可以通过 serde 反序列化——而不需要提前绑定到一个具体的 S

3.2.1 为什么不是直接给 Service 加构造函数?

你可能会问:既然每个 Middleware 都有 MiddlewareLayerMiddleware 成对出现,为什么不直接用 Middleware::new(svc, config) 的构造函数,何必多出一个 Layer trait?

答案在"组合"二字。如果没有 Layer trait,每一个"组合工具"都要手动适配每一个中间件——ServiceBuilder::buffer 知道 Buffer::new 的参数顺序、ServiceBuilder::timeout 知道 Timeout::new……每加一个新中间件,ServiceBuilder 都得改代码。

有了 Layer trait 之后,组合工具只需要面对一个统一接口:fn layer(inner) -> wrappedServiceBuilder 完全不用知道每个中间件的构造函数长什么样——它只负责"把一个 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)
    }
}

拆开看:

  1. Stack<Inner, Outer> 自己还是一个 Layer<S>——它本身还能被继续叠。
  2. layer(s) 的实现是"先让 Inner 包一层,再让 Outer 包一层"。
  3. 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 成为一个可以在 VecHashMap、函数返回值里自由携带的一等公民。它不绑定到一个特定的 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),
        }
    }
}

三件事:

  1. 把当前的 self.layer(类型 L)扔进 Stack::new第一个参数位置(即 inner);
  2. 新传入的 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.layerouter

等等,这和前面讲的"先加入 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.layerStack<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))?三个小原因:

  1. 少写一个类型名——buffer(100)layer(BufferLayer::new(100)) 短得多。
  2. 避免导入——用户不用 use tower::buffer::BufferLayer,读 ServiceBuilder 的 docs 就知道有这个能力。
  3. 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>StackIdentity 三者构成一个 monoid

  • 集合:所有 Layer<S> 实例;
  • 二元运算Stack,满足结合律(Stack::new(Stack::new(A, B), C)Stack::new(A, Stack::new(B, C)) 产生相同的 Service);
  • 单位元Identity,满足 Stack(Identity, L) = LStack(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 提供了 BoxServiceBoxCloneService

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 → MyHandler
  • MyHandler::Error = MyError
  • Buffer<MyHandler>::Error = BoxError(因为 Buffer 需要统一 drop 错误)
  • Retry<Buffer<MyHandler>>::Error = BoxError
  • Timeout<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 就是这个容器。

实务上你会看到两种应对:

  1. 让中间件只处理超集错误。Tonic 的 tonic::Status 就是一个覆盖所有可能 gRPC 错误的超集。Tonic 里每个中间件的 Error 都是 tonic::Status,不走 BoxError 那一套。
  2. 顶层统一 downcast。Axum 在最外层做一个统一的 ErrorHandler,把 BoxError 下塑回具体类型、生成恰当的 HTTP 响应。

这两个模式在第 22 章(Axum / Tonic 如何构建在 Hyper + Tower 之上)会再详细讨论。

3.11 小结:落到你键盘上

我们本章做了五件事:

  1. 读完了 Layer trait 的全部源码——4 行。
  2. 拆清楚 Stack 的字段语义,解开"为什么 inner 字段存的是后加入的 layer"的困惑。
  3. 读完 ServiceBuilder 的构造、.layer(T).service(S) 三个关键方法的源码,完整手工 monomorphize 了一条中间件链。
  4. 讨论了 Layer 的 monoid 代数结构,和 Serde、Vue 3、函数式 compose 的思想对照。
  5. 手写了一个 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、为什么很多人用错了

基于 VitePress 构建