当前位置: 首页 > news >正文

Tomcat 的 Pipeline 比你写的责任链复杂10倍

很多人学责任链模式,写出来的就是 Handler1 → Handler2 → Handler3,一路调下去。这不叫责任链,这叫 for 循环套了个壳。

Tomcat 的 Pipeline-Valve 机制才是责任链模式的真正形态。它跟你写的 Handler 链至少有三个本质区别:可中断、可分支、有容器层级。

Pipeline-Valve:不是 Handler 链,是阀门链

Tomcat 的请求处理流程是这样的:

Connector → Engine Pipeline → Host Pipeline → Context Pipeline → Wrapper Pipeline → Servlet

每个容器(Engine/Host/Context/Wrapper)都有一个 Pipeline,Pipeline 里有多个 Valve。请求从第一个 Valve 进入,每个 Valve 可以选择:

  1. 继续传给下一个 Valve
  2. 直接返回(中断链)
  3. 在调用下一个 Valve 前后加自己的逻辑

```java // Valve 接口——比 Handler 复杂在哪? public interface Valve { Valve getNext(); // 获取下一个 Valve void setNext(Valve valve); // 设置下一个 Valve void invoke(Request request, Response response) // 处理请求 throws IOException, ServletException; }

// Pipeline 接口 public interface Pipeline { Valve getFirst(); // 获取第一个 Valve void addValve(Valve valve); // 动态添加 Valve void removeValve(Valve valve); // 动态移除 Valve } ```

跟你写的 Handler 链对比一下:

| 特性 | Handler 链 | Pipeline-Valve | |------|-----------|----------------| | 链路构建 | 构造时固定 | 运行时动态增删 | | 中断能力 | 需要约定返回值 | 天然支持(不调 getNext) | | 容器层级 | 扁平 | 嵌套(Engine→Host→Context→Wrapper) | | 分支能力 | 无 | Valve 可以改变请求流向 |

关键差异:Valve 是动态增删的。你可以在运行时通过addValve()给某个容器加一个阀门,不需要重启。这个能力在运维场景下非常有用——比如临时加一个限流 Valve,流量降下来后再移除。

一个真实的 Valve:AccessLogValve

Tomcat 自带的 AccessLogValve 是一个很好的学习案例:

```java public class AccessLogValve extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { // 1. 先调下一个 Valve(让请求继续处理) getNext().invoke(request, response);

// 2. 请求处理完后,记录访问日志 long duration = System.currentTimeMillis() - request.getCoyoteRequest().getStartTime(); log(request, response, duration); }

} ```

注意这里的执行顺序:先调用下一个 Valve,等它处理完了再记录日志。这就是 Valve 比 Filter 灵活的地方——你可以在后置处理里拿到 response 的状态码和耗时,Filter 要做到这一点需要包装 Response 对象。

我在一个项目里仿照这个模式写了一个 MetricsValve,统计每个请求的 QPS、延迟、错误率。因为是 Valve 而不是 Filter,所以可以拿到 Tomcat 内部的请求信息(比如哪个 Host/Context 处理的),这是 Filter 层面拿不到的。

模板方法模式:LifecycleBase

Tomcat 的组件生命周期管理是模板方法模式的经典实现:

```java public abstract class LifecycleBase implements Lifecycle { @Override public final void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(LifecycleState.BEFORE_INIT_EVENT); } setStateInternal(LifecycleState.INITIALIZING, null, false); try { initInternal(); // 模板方法,子类实现 } catch (Throwable t) { setStateInternal(LifecycleState.FAILED, null, false); throw t; } setStateInternal(LifecycleState.INITIALIZED, null, false); }

protected abstract void initInternal() throws LifecycleException;

} ```

这里有一个设计细节值得学习:状态转换和异常处理在模板方法里统一管理。子类的initInternal()只需要关注自己的初始化逻辑,不用担心状态是否正确、异常时状态怎么回退。

这比你写一个init()方法然后在里面 if-else 判断状态要安全得多——因为状态转换的代码只存在于LifecycleBase一处,不可能不一致。

组合模式:Container 的层级结构

Tomcat 的 Engine → Host → Context → Wrapper 是典型的组合模式:

java public interface Container extends Lifecycle { Container getParent(); void addChild(Container child); void removeChild(Container child); Container findChild(String name); Container[] findChildren(); }

但 Tomcat 的组合模式比教科书多了一个限制:子容器的类型是固定的。Engine 只能包含 Host,Host 只能包含 Context,Context 只能包含 Wrapper。这不是组合模式的标准做法(标准做法是所有节点都是 Component 类型),但这个限制是必要的——Tomcat 需要保证请求从外到内逐层传递,不能跳层。

这个设计决策说明一个问题:模式是工具,不是教条。当模式跟业务约束冲突时,优先满足业务约束。

观察者模式:Lifecycle 事件

Tomcat 的生命周期事件是观察者模式,但有个反直觉的地方:事件是同步分发的

java public abstract class LifecycleBase implements Lifecycle { protected void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(this, type, data); for (LifecycleListener listener : listeners) { listener.lifecycleEvent(event); // 同步调用 } } }

很多人一看到"事件"就想到异步,但 Tomcat 选择同步是有原因的:组件的生命周期状态转换必须是确定性的。如果异步分发事件,监听器可能在状态已经改变之后才收到通知,导致基于旧状态做决策。

这个取舍在业务代码里也经常遇到:你需要"事件通知"的解耦能力,但又不接受异步带来的时序不确定性。Tomcat 的方案是:同步分发事件,但在事件处理中避免重操作。监听器只做轻量级的状态同步,重操作放到独立的线程池。

真正的教训

Tomcat 的设计模式用得比大部分项目都复杂,但不是因为 Tomcat 的开发者喜欢炫技。原因是 HTTP 服务器的需求本身就复杂:动态增删组件、嵌套容器、状态一致性、请求的灵活拦截……每个需求都逼着你选一个比"简单版"更重的方案。

你写的责任链是 Handler1→Handler2→Handler3,Tomcat 的责任链是动态阀门+嵌套管道+状态管理。差距不在于你不会写,而在于你的问题域没复杂到需要这种程度。

但反过来,如果你的问题域确实需要这些能力,而你还在用 Handler 链,那就是欠债。


我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,感兴趣可以搜一下看看。

http://www.jsqmd.com/news/1006652/

相关文章:

  • 宿州唯品装饰的“砸无赦”:一套自我纠错的质量保障机制 - 装企自媒体训练营辉哥
  • 深入解析NXP Kinetis SIM模块:时钟、路由与低功耗配置实战
  • 深入解析NXP MCU ROM Bootloader通信协议:从数据包到固件更新实战
  • 打印机出现5B00,5B02,5B04,1700,1702,1704,P07,E08这些错误怎么办?其实小问题,别被维修店坑了,这个只需用清零软件清零一下即可完美修好,自己弄直接省钱100多,亲测完美
  • 终极指南:3分钟搞定Chrome Markdown阅读器,让技术文档阅读体验飞升
  • 2026年6月最新|充气帐篷厂家排名 行业内口碑好的生产厂家精选 - 商业新知
  • 【洛谷 P2249】查找(深基 13. 例 1)+ 详细分析
  • 承德隆化重卡维修标杆|解放重汽陕汽维修 承围线交叉口门店 24小时全天候货车救援维修服务 电话15831485236 - 速递信息
  • 新买的游戏本/轻薄本到手第一件事:别急着装软件,先打开这个电池保护设置!
  • 数字化转型新风口:AI知识库智能体重塑企业服务模式
  • 沈阳连锁黄金回收,CCIC 国检资质,大额交易专人陪同隐私保密 - 讯息早知道
  • WaveTools终极指南:3大核心功能解锁《鸣潮》完整游戏体验
  • 2026年6月盐城早茶哪家值得去?5家门店实测排行解析 - 奔跑123
  • VS Code语法检查进阶指南:Grammarly插件深度解析与实战应用
  • 四川成都市十大单招培训学校排名TOP10 - 四川单招培训
  • MC68377 QADC64模块时钟与中断机制深度解析与实战配置
  • 2026西安本地宝藏回收店,闲置奢品变现不用愁 - 讯息早知道
  • I2C中断机制深度解析:从轮询到事件驱动的效率跃迁
  • zxing-cpp跨平台实战:C++20赋能的多端条码处理库深度解析
  • 2026 年 6 月东莞黄金回收哪家强?综合评测:三家主流机构专业评定 - zzlzzl6688
  • 如何快速解决B站视频下架问题:m4s-converter的完整使用指南
  • 2026年设备 + 施工一站式,通风排风定制服务推荐 - 速递信息
  • USB-Disk-Ejector:终极Windows USB设备安全弹出解决方案
  • Unity卡牌游戏UI开发:从技术痛点到优雅解决方案
  • 靠谱的永康纯钛保鲜盒实力公司 - 速递信息
  • Skills实战:从0到1写一个你自己的接口签名Skill
  • 2026 年 6 月阳江黄金回收哪家无套路?综合评测:三家主流机构专业评定 - zzlzzl6688
  • 如何通过自动化脚本高效获取Oracle Cloud免费ARM服务器
  • 终极Markdown实时预览解决方案:Notepad++ MarkdownViewerPlusPlus插件完整指南
  • 舟山汽车内饰翻新|广粤汽车真皮内饰改装本地改装实测 - 百航