1419 字
4 分钟
中间件/管道链(Middleware / Pipeline Chain)
一、为什么需要中间件/管道链
你写了一个 HTTP 服务,最初只有业务逻辑。后来要加日志,就在处理函数开头写几行;要加认证,又在开头加一段校验;要加限流,再加一段计数逻辑。三个月后,业务逻辑被埋在几十行横切代码中间,改一个地方要翻半天。
更麻烦的是顺序问题。如果限流在认证之前运行,未认证的请求也会消耗限流配额,攻击者发一堆无效请求就能耗尽合法用户的配额。这种 bug 不是逻辑错误,而是代码组织方式导致的隐含依赖。
中间件/管道链把每个关注点抽成独立的中间件,按顺序串成链条。每个中间件只做一件事,然后调用 next() 把控制权交给下一个。不调用 next() 就短路整条链。请求向内流入,响应向外流出,形成「洋葱模型」。
二、现实类比
机场安检通道。你的行李先过 X 光机(日志记录),然后过金属探测器(身份认证),再查验证件(参数校验)。每个关卡只做一件事,然后送你到下一个。任何一个关卡都可以拒绝你通过,相当于不调用 next() 短路整条链。
三、核心思想
每个中间件接收一个上下文和一个 next() 函数。调用 next() 将控制传递给链中下一个中间件;next() 返回后,中间件可以运行后处理逻辑。不调用 next() 则短路整个链。这创建了一个双向管道,请求向内流入,响应向外流出。
flowchart TD
A["中间件 A(日志)"] --> B["中间件 B(认证)"]
B --> C["中间件 C(处理器)"]
C --> B2["B 后处理"]
B2 --> A2["A 后处理"]
执行顺序:A.pre → B.pre → C.pre → C.post → B.post → A.post。即使 B 短路(不调用 next()),A 的后处理仍然会执行,这就是为什么日志中间件即使对被拒绝的请求也能正确记录。
| 属性 | 值 |
|---|---|
| 组合 | 每请求执行 O(n) 个中间件 |
| 短路 | 任何中间件可通过不调用 next() 跳过后续 |
| 上下文共享 | 所有中间件共享同一个可变上下文对象 |
| 方向 | 双向,进入时前处理,返回时后处理 |
四、变体与对比
| 模式 | 关系 | 区别 |
|---|---|---|
| 观察者(Observer) | 中间件可以观察和修改流经管道的请求/响应 | 观察者是一对多广播,中间件是一对一链 |
| 迭代器(Iterator) | 中间件链像迭代器遍历序列 | 迭代器只读遍历,中间件可修改和短路 |
| 注册表(Registry) | 注册表可以存储和管理链中的中间件 | 注册表管发现,中间件管执行流 |
| 虚函数表(Vtable) | 每个中间件是实现通用接口的函数指针 | Vtable 是类型分发,中间件是顺序组合 |
五、多语言实现
package pipeline
// Handler 是管道终端的处理函数type Handler func(ctx map[string]any)
// Middleware 是中间件函数,接收上下文和下一个处理器type Middleware func(ctx map[string]any, next Handler)
// Chain 将多个中间件组合为单一处理器func Chain(middlewares ...Middleware) Handler { return func(ctx map[string]any) { var run func(i int) run = func(i int) { if i < len(middlewares) { // 将后续链包装为 next handler middlewares[i](ctx, func(c map[string]any) { run(i + 1) }) } } run(0) }}func logging(ctx map[string]any, next Handler) { start := time.Now() next(ctx) // 调用下游 fmt.Printf("耗时: %v\n", time.Since(start))}
func auth(ctx map[string]any, next Handler) { token, _ := ctx["token"].(string) if token == "" { ctx["error"] = "未认证" // 短路:不调用 next return } next(ctx)}
// 业务处理器也需要符合 Middleware 签名(双参数),// 在写入结果后调用 next(ctx) 把控制权交还上游做后处理business := func(ctx map[string]any, next Handler) { ctx["result"] = "成功" next(ctx)}
// 组合:日志 → 认证 → 业务处理handler := Chain(logging, auth, business)// 中间件类型:接收上下文和 next 函数type Middleware<T> = (ctx: T, next: () => void) => void;
class Pipeline<T> { private middlewares: Middleware<T>[] = [];
// 添加中间件到链尾 use(middleware: Middleware<T>): void { this.middlewares.push(middleware); }
// 执行中间件链 execute(ctx: T): void { let index = 0; const next = (): void => { if (index < this.middlewares.length) { const mw = this.middlewares[index]!; index++; mw(ctx, next); } }; next(); }}六、生产验证
- gRPC-Go — server.go#L1224-L1260 中的
chainUnaryServerInterceptors将拦截器链接为单一处理器,getChainUnaryHandler递归构建链,用于生产 gRPC 服务中的认证、日志、追踪和限流。 - Koa.js — application.js#L152-L204 中
use()将中间件推入数组,callback()通过koa-compose组合为单一函数。Koa 开创了异步洋葱模型,每个await next()创建一个栈帧,使下游中间件可以使用try/catch/finally。 - Express.js —
app.use()链接中间件用于 HTTP 请求处理,是 Node.js 生态最广泛使用的中间件实现。
七、小结
何时使用:
- HTTP 请求处理,认证、日志、CORS、压缩、限流作为可组合层
- RPC 拦截器,gRPC 拦截器用于追踪、认证、重试和指标
- 构建/编译管道,Webpack loader、Babel 转换各自处理后传递给下一个
- CLI 命令处理,参数解析、验证、帮助生成作为中间件
何时不用:
- 事件扇出(一对多),需要多个独立处理器响应同一事件时,用观察者模式
- 无状态转换,每步只是转换数据不需要包裹下一步时,用
array.map().filter().reduce() - 性能关键热路径,每个中间件增加一次函数调用和闭包分配,紧密循环中开销不可忽视
八、参考资料
- Koa 中间件机制 - 异步洋葱模型的原始实现
- gRPC-Go 拦截器链 - 生产级 RPC 中间件组合
- Express 中间件 - Node.js HTTP 中间件的事实标准
- Redux applyMiddleware - 状态管理中的中间件模式
- ASP.NET Core Middleware - .NET 请求管道实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
中间件/管道链(Middleware / Pipeline Chain)
https://blog.souloss.cn/posts/programming/system-patterns/system-patterns-middleware-chain/ 部分信息可能已经过时
相关文章 智能推荐
1
批处理(Batch Processing)
程序设计 把多个小操作攒成一个大操作一起执行
2
注册表(Registry)
程序设计 全局查找表,让组件在运行时发现和注册自己,无需硬编码依赖
3
控制面与数据面分离(Control Plane / Data Plane)
程序设计 管理面与数据面分离,K8s、Istio、云原生架构的标配设计
4
限流器(Rate Limiter)
程序设计 通过维护一个按固定速率补充的令牌桶来控制吞吐量
5
流式输出 / SSE(Streaming / Server-Sent Events)
程序设计 实时增量输出,降低首字延迟,从 ChatGPT 打字效果到实时推荐的通用工程模式






