第十章:实现 Valve 和 Pipeline 机制

本章目标

本章旨在实现 Valve(阀门)和 Pipeline(管道)机制,允许在请求处理流程中插入额外的控制逻辑和功能扩展。这是模仿 Tomcat 核心架构的关键步骤,具体目标如下:

  • Valve 机制:作为一种过滤器链机制,Valve 可以在请求和响应过程中插入额外的处理逻辑,例如权限控制、日志记录和请求过滤等。
  • Pipeline 机制:负责管理多个 Valve 的执行顺序,确保每个请求都按顺序通过 Pipeline 中的 Valve 执行操作。
  • 实现内容

    • 定义 Valve 接口,让每个 Valve 实现特定的逻辑,并将 Valve 按顺序添加到 Pipeline 中。
    • Pipeline 作为容器,管理多个 Valve 的执行顺序。
    • 每个请求都将通过一系列的 Valve,允许灵活地扩展请求处理逻辑。
  • 示例功能:实现一个日志记录 Valve,记录每个请求的 URI 和执行时间,并将该 Valve 添加到 Pipeline 中进行测试。

10.1 Valve 接口的设计

Valve 接口定义了一个处理请求和响应的标准规范。每个实现了 Valve 接口的类都可以在请求流中执行特定的功能。

package com.daicy.minitomcat;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface Valve {
    void invoke(HttpServletRequest request, HttpServletResponse response, ValveContext context);
}
  • invoke(HttpServletRequest request, HttpServletResponse response, ValveContext context)

    • 该方法会在请求进入 Valve 时被调用。
    • requestresponse 对象表示当前请求和响应。
    • ValveContext 提供了继续传递请求和响应到下一个 Valve 的能力,是链式调用的关键。

10.2 ValveContext 类

ValveContext 类用于在 Valve 之间传递请求,它负责控制请求在 Valve 链中的传递流程。

package com.daicy.minitomcat;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

public class ValveContext {
    private int currentIndex = -1;
    private final List<Valve> valves;

    public ValveContext(List<Valve> valves) {
        this.valves = valves;
    }

    public void invokeNext(HttpServletRequest request, HttpServletResponse response) {
        currentIndex++;
        if (currentIndex < valves.size()) {
            valves.get(currentIndex).invoke(request, response, this);
        }
    }
}
  • ValveContext 保存了请求的处理流程顺序,并通过 invokeNext() 方法依次执行 Valve 链中的下一个 Valve。
  • 通过维护 currentIndex,确保每个 Valve 只被执行一次,并按序传递。

10.3 Pipeline 类的设计

Pipeline 是一个管理 Valve 顺序的容器,它确保每个请求都按照预定的顺序通过各个 Valve 进行处理。

package com.daicy.minitomcat;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

public class Pipeline {
    private final List<Valve> valves = new ArrayList<>();
    private Valve basicValve; // BasicValve 处理最后的逻辑

    public void addValve(Valve valve) {
        valves.add(valve);
    }

    public void setBasicValve(Valve basicValve) {
        this.basicValve = basicValve;
    }

    public void invoke(HttpServletRequest request, HttpServletResponse response) {
        List<Valve> allValves = new ArrayList<>(valves);
        if (basicValve != null) {
            allValves.add(basicValve);
        }
        new ValveContext(allValves).invokeNext(request, response);
    }
}
  • addValve(Valve valve):将一个新的 Valve 添加到 Pipeline 中。
  • setBasicValve(Valve basicValve):设置最后一个负责具体业务逻辑的 Valve。
  • invoke(HttpServletRequest request, HttpServletResponse response):启动处理流程,将所有 Valve 整合后通过 ValveContext 执行。

10.4 示例:日志记录 Valve

接下来,我们实现一个简单的日志记录 Valve,它记录每个请求的 URI 和执行时间。

package com.daicy.minitomcat;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LogValve implements Valve {

    @Override
    public void invoke(HttpServletRequest request, HttpServletResponse response, ValveContext context) {
        System.out.println("LogValve: Logging request " + request.getRequestURI());
        context.invokeNext(request, response); // 调用下一个 Valve
    }
}
  • LogValve 在请求到达时记录 URI 信息。
  • 通过 context.invokeNext(),它将请求传递给 Pipeline 中的下一个 Valve,确保链式调用不中断。

10.5 BasicValve:具体逻辑调用的 Valve

BasicValve 通常作为 Pipeline 中的最后一个节点,负责具体的请求分发与处理逻辑(如静态资源、Servlet 调用或 404 处理)。

class BasicValve implements Valve {
    private OutputStream outputStream;

    @Override
    public void invoke(HttpServletRequest request, HttpServletResponse response, ValveContext context) {
        // 默认的 Valve,处理请求
        HttpServletRequestImpl requestImpl = (HttpServletRequestImpl) request;
        HttpServletResponseImpl responseImpl = (HttpServletResponseImpl) response;
        String uri = request.getRequestURI();
        WebXmlServletContainer parser = HttpServer.parser;
        String servletName = parser.getServletName(uri);
        
        if (uri.endsWith(".html") || uri.endsWith(".css") || uri.endsWith(".js")) {
            staticProcessor.process(requestImpl, responseImpl);
        } else if (null != servletName) {
            // 普通请求处理
            servletProcessor.process(request, response);
        } else {
            send404Response(outputStream);
        }
    }
}
  • BasicValve 不再调用 context.invokeNext(),因为它是链路的终点。
  • 它根据请求 URI 的类型(静态资源或 Servlet)分发处理逻辑,或返回 404 响应。

10.6 整合 Valve 和 Pipeline

最后,我们需要将 ValvePipeline 整合到 Web 服务器的请求处理过程中。每次请求都会通过一个由多个 Valve 组成的 Pipeline 进行处理。

package server;

import javax.servlet.*;
import java.io.IOException;

public class HttpProcessor {
    ....
    public void process() {
        boolean keepAlive = false;
        HttpServletRequestImpl request = null;
        try {
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            // 解析请求
            request = HttpRequestParser.parseHttpRequest(inputStream);
            // 构建响应
            HttpServletResponseImpl response = new HttpServletResponseImpl(outputStream);
            if (null == request) {
                return;
            }
            keepAlive = parseKeepAliveHeader(request) && !isCloseConnection(request);
            
            // 初始化 Pipeline 并添加 Valve
            Pipeline pipeline = new Pipeline();
            pipeline.addValve(new LogValve());
            pipeline.setBasicValve(new BasicValve());
            pipeline.invoke(request, response);

        } catch (Exception e) {
            System.out.println("HttpProcessor error " + e.getMessage());
        } finally {
            try {
                // 如果是 keep-alive,连接保持打开,否则关闭连接
                if (!keepAlive && (null != request && !request.isAsyncSupported())) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    ....
}
  • HttpProcessor 中,我们创建了一个 Pipeline 实例,并将 LogValve 添加到 Pipeline 中。
  • process() 方法启动请求的处理流程,通过 ValveContext 来依次执行 Pipeline 中的 Valve,最终由 BasicValve 完成业务处理。

10.7 学习收获

通过实现 Valve 和 Pipeline 机制,我们加深了对请求处理链的理解,并掌握了如何设计灵活的请求处理流程。具体收获如下:

  • Valve 机制:通过实现自定义的 Valve,我们可以在请求处理过程中插入各种功能,如权限控制、日志记录、请求修改等。
  • Pipeline 机制:Pipeline 管理着多个 Valve,确保它们按照指定顺序执行。每个请求都会按照相同的流程通过 Pipeline 中的 Valve,保证了请求处理的灵活性和可扩展性。
  • 扩展性:Valve 和 Pipeline 机制使得 Web 服务器的功能扩展更加简洁和模块化,类似于 Tomcat 中的 Valve 和 Pipeline 设计,方便开发者根据需求进行定制。

通过这一机制,我们能够实现更复杂、更灵活的请求处理功能,为后续的功能扩展提供了强大的支持。

项目源代码地址:

https://github.com/daichangya/MiniTomcat/tree/chapter10/mini-tomcat

说明:本文代码基于 javax.servlet 包,适用于 Tomcat 9 及以下版本。Tomcat 10+ 已迁移至 jakarta.servlet 包,若在新版本环境下运行需调整导入路径。