02. Tomcat源代码—04.分析请求过程

1. 管道模式

1.1 管道与阀门

在一个比较复杂的大型系统中,如果一个对象或数据流需要进行繁杂的逻辑处理,我们可以选择在一个大的组件中直接进行这些繁杂的逻辑处理。这个方式虽然达到目的,但是拓展性和可重用性差。因为牵一发而动全身。

管道模式就像一条管道把多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,处理逻辑放在阀门上。

02. Tomcat源代码—04.分析请求过程

1.2 代码实现管道与阀门

/**
 * 阀门
 */
public interface Valve {
    Valve getNext();

    void setNext(Valve valve);

    void invoke(String handing);
}
public class BasicValve implements Valve {
    private Valve next = null;

    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        this.next = valve;
    }

    @Override
    public void invoke(String handing) {
        System.out.println("基础阀门");
    }
}

public class FirstValve implements Valve {
    private Valve next = null;

    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        this.next = valve;
    }

    @Override
    public void invoke(String handing) {
        System.out.println("定制阀门1");
        getNext().invoke(handing);
    }
}

public class SecondValve implements Valve {
    private Valve next = null;

    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        this.next = valve;
    }

    @Override
    public void invoke(String handing) {
        System.out.println("定制阀门2");
        getNext().invoke(handing);
    }
}
/**
 * 管道
 */
public interface Pipeline {
    // 获取第一个阀门
    Valve getFirst();

    Valve getBasic();

    // 设置基础阀门
    void setBasic(Valve valve);

    // 添加阀门
    void addValve(Valve valve);
}
public class StandardPipeline implements Pipeline {
    // 第一个阀门
    protected Valve first = null;
    // 基础阀门
    protected Valve basic = null;

    @Override
    public Valve getFirst() {
        return first;
    }

    @Override
    public Valve getBasic() {
        return basic;
    }

    @Override
    public void setBasic(Valve valve) {
        this.basic = valve;
    }

    // 添加阀门,链式构建阀门的执行顺序(先定制、最后基础阀门)
    @Override
    public void addValve(Valve valve) {
        if (first == null) {
            first = valve;
            valve.setNext(basic);
        } else {
            Valve current = first;
            while (current != null) {
                if (current.getNext() == basic) {
                    current.setNext(valve);
                    valve.setNext(basic);
                }
                current = current.getNext();
            }
        }
    }
}
public class Main {
    public static void main(String[] args) {
        // 新建管道,添加1个基础阀门,2个定制阀门
        StandardPipeline standardPipeline = new StandardPipeline();
        BasicValve basicValve = new BasicValve();
        standardPipeline.setBasic(basicValve);
        FirstValve firstValve = new FirstValve();
        standardPipeline.addValve(firstValve);
        SecondValve secondValve = new SecondValve();
        standardPipeline.addValve(secondValve);

        String handing = "这是一个Servlet请求";
        standardPipeline.getFirst().invoke(handing);
    }
}

执行结果:
02. Tomcat源代码—04.分析请求过程

2. Tomcat请求过程

Tomcat中Container有4种:Engine、Host、Context、Wrapper。它们各自的实现类分别是 StandardEngine、StandardHost、StandardContext、StandardWrapper,他们都在 tomcat 的 org.apache.catalina.core 包下。

Standard 的 container 都是直接继承抽象类:org.apache.catalina.core.ContainerBase。
02. Tomcat源代码—04.分析请求过程
Host 设计的目的是:Tomcat 诞生时,服务器资源很贵,所以一般一台服务器其实可以有多个域名映射。为了满足这种需求,设计了Host。

3. Tomcat 处理一个 HTTP 请求的过程

  1. 用户点击网页内容,请求被发送到本机端口 8080,被在那里监听的 Connector 获得。
  2. Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待 Engine 的回应。
  3. Engine 获得请求 localhost/test/index.jsp,匹配所有的虚拟主机 Host。Engine 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因为该 Host 被定义为该 Engine 的默认主机)。
  4. 名为 localhost 的 Host 获得请求/test/index.jsp,匹配它所拥有的所有的 Context。Host 匹配到路径为/test 的 Context(如果匹配不到就把该请求交给路径名为“ ”的 Context 去处理)。
  5. path=“/test”的 Context 获得请求/index.jsp,在它的 mapping table 中寻找出对应的 Servlet。Context 匹配到 URL PATTERN 为*.jsp 的 Servlet,对应于 JspServlet类。构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet 的 doGet() 或 doPost(),执行业务逻辑、数据存储等程序。Context 把执行完之后的 HttpServletResponse 对象返回给 Host。
  6. Host 把 HttpServletResponse 对象返回给 Engine。
  7. Engine 把 HttpServletResponse 对象返回 Connector。
  8. Connector 把 HttpServletResponse 对象返回给客户 Browser。

4. Tomcat管道模式

Tomcat每一种 container 都有一个自己的 StandardValve,分别是:

  1. StandardEngineValve
  2. StandardHostValve
  3. StandardContextValve
  4. StandardWrapperValve
    四个 container 就相当于有四个生产线(Pipeline)。

02. Tomcat源代码—04.分析请求过程

02. Tomcat源代码—04.分析请求过程

在 CoyoteAdapter 的 service(),由下面这一句就进入 Container 的,connector.getContainer().getPipeline().getFirst().invoke(request, response)。
是的,这就是进入 container 迷宫的大门,欢迎来到 Container。
02. Tomcat源代码—04.分析请求过程

StandardEngineValve的service()
02. Tomcat源代码—04.分析请求过程
其他类似的StandardValue还有StandardHostValve、StandardContextValve、StandardWrapperValve。

4.1 Tomcat 中定制阀门

管道机制给我们带来了更好的拓展性,例如要添加一个额外的逻辑处理阀门是很容易的:

  1. 自定义阀门 PrintIPValve,只要继承 ValveBase 并重写 invoke()即可。注意在 invoke()中一定要执行调用下一个阀门的操作,否则会出现异常。
public class PrintIPValve extends ValveBase {
      @Override
      public void invoke(Request request, Response response) throws IOException, ServletException {
          System.out.println("------自定义阀门 PrintIPValve:" + request.getRemoteAddr());
          getNext().invoke(request, response);
      }
  }
  1. 配置Tomcat的核心配置文件server.xml,这里把阀门配置到Engine容器下,作用范围就是整个引擎,也可以根据作用范围配置在Host或者是Context下。
<Valve className="org.apache.catalina.valves.PrintIPValve" />

4.2 Tomcat 中提供常用的阀门

  1. AccessLogValve:请求访问日志阀门,通过此阀门可以记录所有客户端的访问日志,包括远程主机 IP、远程主机名、请求方法、请求协议、会话 ID、请求时间、处理时长、数据包大小等。它提供任意参数化的配置,可以通过任意组合来定制访问日志的格式
  2. JDBCAccessLogValve:同样是记录访问日志的阀门,但是它有助于将访问日志通过 JDBC 持久化到数据库中
  3. ErrorReportValve:这是一个将错误以 HTML 格式输出的阀门
  4. PersistentValve:这是对每一个请求的会话实现持久化的阀门
  5. RemoteAddrValve:访问控制阀门。可以通过配置决定哪些 IP 可以访问 WEB 应用
  6. RemoteHostValve:访问控制阀门,通过配置觉得哪些主机名可以访问 WEB 应用
  7. RemoteIpValve:针对代理或者负载均衡处理的一个阀门,一般经过代理或者负载均衡转发的请求都将自己的 IP 添加到请求头”X-Forwarded-For”中,此时,通过阀门可以获取访问者真实的 IP
  8. SemaphoreValve:这个是一个控制容器并发访问的阀门,可以作用在不同容器上
上一篇:一些有用的huginn Agent


下一篇:Tomcat中的Valve以及自定义Valve