设计模式与Tomcat:Pipeline中的责任链、Lifecycle中的观察者、Connector中的适配器

说实话,Tomcat 是我早期啃源码时最头疼的东西之一。那时候我刚从学校出来,觉得能写个 Servlet 就挺牛了。直到有一天线上 Tomcat 莫名其妙挂掉,我被迫去翻它的启动流程……嗯,那才是我真正理解设计模式的开端。

今天咱们就聊聊 Tomcat 里三个最经典的模式应用:责任链观察者适配器。这三个模式在 Tomcat 里不是生搬硬套的,而是为了解决真实问题长出来的。

Pipeline 与责任链模式

先说说 Pipeline。Tomcat 处理一个请求,不是一步到位的。它要经过一堆阀门(Valve),比如日志、认证、权限、解析……每个阀门干完自己的活,再传给下一个。

这不就是责任链模式吗?

我当年第一次看 Pipeline 源码时,心里想的是:这不就是个链表吗?每个 Valve 持有下一个 Valve 的引用,调用 invoke() 时传下去。但仔细看才发现,Tomcat 的设计更讲究——它把 Pipeline 和 Valve 分开了。

核心设计:Pipeline 是链条的容器,Valve 是链条上的节点。Pipeline 负责管理 Valve 的增删,Valve 只关心自己的业务逻辑。

来看一段简化后的代码,你就明白了:

// 阀门接口
public interface Valve {
    void invoke(Request request, Response response, ValveChain chain);
}

// 管道接口
public interface Pipeline {
    void addValve(Valve valve);
    Valve getFirst();
}

// 基础阀门实现
public class AccessLogValve implements Valve {
    @Override
    public void invoke(Request req, Response res, ValveChain chain) {
        // 记录访问日志
        System.out.println("Access log: " + req.getUri());
        // 传给下一个阀门
        chain.invokeNext(req, res);
    }
}

这里有个细节我特别想提:ValveChain 这个对象。它其实是个迭代器,内部维护了当前执行到的位置。每个 Valve 调用 chain.invokeNext() 时,链条就往前走一步。

我在项目中遇到过类似场景——一个支付网关,需要经过风控、费率计算、渠道分发等多个步骤。当时我直接抄了 Tomcat 的 Pipeline 设计,效果非常好。每个步骤独立测试,新增一个阀门只需要加一行配置。

避坑指南:我曾经在责任链里搞过「全局异常捕获」,结果某个阀门抛异常后,整个链条断了,后面的阀门全没执行。后来我改成每个阀门自己 try-catch,异常信息包装后继续往下传。记住:责任链的每个节点都应该独立处理异常,不要指望上游帮你兜底。

Lifecycle 与观察者模式

Tomcat 的组件是有生命周期的。启动、初始化、开始、停止、销毁……每个阶段都要通知一堆监听器。比如你启动 Tomcat 时,它要通知 JMX 注册 MBean,通知日志系统初始化,通知连接器打开端口。

这就是观察者模式。

Tomcat 定义了一个 Lifecycle 接口,里面有几个核心方法:init()start()stop()destroy()。每个组件实现这个接口,然后注册一堆 LifecycleListener

我画了一张图,帮你理清这个关系:

Tomcat Lifecycle 观察者模式结构 LifecycleBase Listener 列表 StandardEngine StandardHost StandardContext EngineConfig HostConfig ContextConfig fireLifecycleEvent()

你看,LifecycleBase 是抽象基类,它维护了一个监听器列表。当 start() 被调用时,它会遍历所有监听器,挨个调用 lifecycleEvent() 方法。

代码大概长这样:

public abstract class LifecycleBase implements Lifecycle {
    private List<LifecycleListener> listeners = new ArrayList<>();

    public void addLifecycleListener(LifecycleListener listener) {
        listeners.add(listener);
    }

    protected void fireLifecycleEvent(String type, Object data) {
        LifecycleEvent event = new LifecycleEvent(this, type, data);
        for (LifecycleListener listener : listeners) {
            listener.lifecycleEvent(event);
        }
    }

    public final synchronized void start() {
        // 状态检查...
        fireLifecycleEvent(BEFORE_START_EVENT, null);
        startInternal();  // 子类实现具体逻辑
        fireLifecycleEvent(AFTER_START_EVENT, null);
        setState(LifecycleState.STARTED);
    }
}

我个人习惯把这种设计叫做「事件驱动的初始化」。你想想看,如果没有观察者模式,每个组件启动时都要硬编码调用一堆初始化逻辑,耦合度会高得吓人。

注意:Tomcat 的 Lifecycle 事件是同步的。也就是说,监听器里如果抛异常,整个启动流程会中断。我曾经踩过这个坑——一个自定义监听器里做了网络请求,超时了,结果 Tomcat 启动到一半卡死了。后来我改成异步处理,或者至少加个超时机制。

Connector 与适配器模式

最后聊聊 Connector。Tomcat 要支持多种协议:HTTP、AJP、HTTPS……每种协议的请求格式都不一样。但到了 Servlet 容器内部,它只认 ServletRequestServletResponse

怎么把不同协议的请求转成统一的接口?

适配器模式登场。

Tomcat 的 CoyoteAdapter 就是干这个的。它把底层 Socket 来的字节流,解析成 org.apache.catalina.connector.Request 对象,再传给 Pipeline。

我举个例子,HTTP 请求长这样:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0

AJP 协议是二进制的,格式完全不同。但经过适配器之后,到了 Servlet 手里,都是 request.getMethod()request.getRequestURI()

适配器的核心代码其实很简单:

public class CoyoteAdapter implements Adapter {
    @Override
    public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
        // 1. 将 coyote 的 Request 转换为 catalina 的 Request
        Connector connector = getConnector();
        Request catalinaRequest = connector.createRequest();
        catalinaRequest.setCoyoteRequest(req);
        
        // 2. 设置上下文路径、URI 等
        catalinaRequest.getMappingData().setContextPath(...);
        
        // 3. 交给 Pipeline 处理
        connector.getService().getContainer().getPipeline()
                 .getFirst().invoke(catalinaRequest, catalinaResponse);
    }
}

说白了,适配器就是一层「翻译」。它不改变底层协议,只是让上层接口统一。我在做微服务网关时也用过类似思路——把 HTTP、gRPC、Dubbo 的请求统一转成内部 RPC 调用,网关核心逻辑完全不用关心底层协议。

我的经验:适配器模式最容易犯的错误是「过度适配」。我曾经见过一个项目,为了统一接口,把完全不同的业务逻辑也塞进适配器里,结果适配器变得又臭又长。记住:适配器只做格式转换,不做业务处理。业务逻辑应该交给后面的 Pipeline 或 Service 层。

三个模式如何协同

你可能会问:这三个模式在 Tomcat 里是各自为战,还是互相配合?

答案是:它们串成了一条完整的请求处理链路。

我画个流程图你就明白了:

Connector 接收 Socket 请求 CoyoteAdapter 协议转 Servlet 接口 Pipeline 责任链处理 Servlet 业务逻辑 生命周期管理(观察者模式)贯穿始终 LifecycleListener 监听启动/停止事件 在 init/start/stop 时触发配置加载、资源释放等

你看,整个流程非常清晰:

  1. Connector 接收网络请求,拿到原始字节流
  2. CoyoteAdapter 把字节流解析成统一的 Request/Response 对象
  3. Pipeline 带着 Request 穿过一个个 Valve,做各种前置处理
  4. 最终到达 Servlet,执行你的业务代码

Lifecycle 观察者模式,则负责管理这些组件从生到死的整个过程。启动时按顺序初始化,关闭时按顺序清理。

说实话,Tomcat 的架构设计是我见过最优雅的之一。它没有过度设计,每个模式都用得恰到好处。你想想看,如果让你从头写一个 Web 服务器,你能把这三个模式用得这么自然吗?

总结一下

  • Pipeline + Valve = 责任链模式,解决请求处理流程的灵活编排
  • Lifecycle + Listener = 观察者模式,解决组件生命周期的事件通知
  • Connector + Adapter = 适配器模式,解决多协议统一接入

这三个模式在 Tomcat 里不是孤立的,它们共同构建了一个高内聚、低耦合的架构。下次你写中间件或者框架时,不妨想想 Tomcat 是怎么做的。


公众号:蓝海资料掘金营,微信deep3321