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

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都有一个 PipelinePipeline 里有多个 Valve。请求从第一个 Valve 进入每个 Valve 可以选择继续传给下一个 Valve直接返回中断链在调用下一个 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流量降下来后再移除。一个真实的 ValveAccessLogValveTomcat 自带的 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 层面拿不到的。模板方法模式LifecycleBaseTomcat 的组件生命周期管理是模板方法模式的经典实现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 只能包含 HostHost 只能包含 ContextContext 只能包含 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→Handler3Tomcat 的责任链是动态阀门嵌套管道状态管理。差距不在于你不会写而在于你的问题域没复杂到需要这种程度。但反过来如果你的问题域确实需要这些能力而你还在用 Handler 链那就是欠债。我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」23 个设计模式用漫画 答题的方式讲感兴趣可以搜一下看看。