How tomcat works——5 容器

容器(container)是一用来处理 servlet 请求并填充返回对象给 web客户端的模块。接口org.apache.catalina.Container定义了容器的形式,有4种容器:引擎(Engine), 主机(Host), 上下文(Context), 和包装器(Wrapper)。本章将会介绍 context 和 wrapper,而 Engine 和 Host 会留到第13章介绍。本章从介绍容器接口开始,其次是容器中的流水线机制。然后介绍 Wrapper和 Context 接口。本章通过2个例子分别呈现简单的wrapper 和 context 容器。

5.1 容器接口

一个容器必须实现 org.apache.catalina.Container 接口。如第4章中看到,传递一个 Container 实例给 Connector 对象的 setContainer()方法,然后Connector 就可以调用 container 的 invoke() 方法,重新看第4章中Bootstrap 类的代码如下:

HttpConnector connector = new HttpConnector();
SimpleContainer container = new SimpleContainer();
connector.setContainer(container);

首先需要注意的是,对于 Catalina 容器,在不同的概念上,它一共有4种不同类型的容器:

1》Engine:表示整个 Catalina 的 servlet 引擎
2》Host:表示一拥有数个上下文(context)的虚拟主机
3》Context:表示一 Web 应用,一个 context 包含一个或多个wrapper
4》Wrapper:表示一个独立的 servlet

上面的每个概念级别都由org.apache.catalin包中的接口表示。Engine、Host、Context和 Wrapper 接口都实现了 Container 接口。它们的标准实现是 StandardEngine,StandardHost, StandardContext, StandardWrapper,它们都是org.apache.catalina.core 包的一部分。

图 5.1 表示了 Container 接口和它的子接口的结构图。注意接口都是org.apache.catalina 包的,而所有的类都是 org.apache.catalina.core 包的。
这里写图片描述

图 5.1: Container相关类图

注意,所有的类都扩展自抽象类 ContainerBase。

一Catalina 功能部署不一定需要所有的四种类型容器。例如本章第一个应用程序就仅包括一个 wrapper,而第二个应用程序包含 Context 和wrapper 容器模块。在本章附带的应用程序中不需要host和engine。

一个容器可以有一个或多个低层次上的子容器。例如,一个 Context 有一个或多个 wrapper;一个host有零个或多个context。 然而 wrapper 作为最底层容器,则不能包含子容器。把个容器添加到另一容器中可以使用 Container 接口中定义的 addChild()方法,如下定义:

public void addChild(Container child);

删除一个容器可以使用 Container 接口中定义的 removeChild()方法,删除方法如下表示:

public void removeChild(Container child);

另外容器接口支持子接口查找和获得所有子接口集合的方法 findChild()和findChildren ()方法,方法签名如下:

public Container findChild(String name);
public Container[] findChildren();

一个容器还包含一系列的部分如 Lodder、Loggee、Manager、Realm 和Resources。我们将会在后边章节中讨论这些组成部分。值得注意的一点是 Container 接口对于这些组件都定义了 set 和 get 方法包括:getLoader()、 setLoader()、 getLogger()、setLogger()、 getManager()、setManager()、getRealm()、setRealm()、getResources()、setResources()。

更有意思的是Container接口被设计成Tomcat管理员可以通过server.xml文件配置来决定其工作方式的模式。它通过一个 pipeline和容器中一系列的valves来实现,这些内容将会在下一节 “管道流水线任务”中讨论。

注意 :Tomcat4和 Tomcat5中的 Container 接口有稍许不同。如,Tomcat4此接口有一个 map 方法,但是在 Tomcat5 中已不存在。

5.2管道流水线任务

本章节介绍当connector 调用容器(container)的 invoke() 方法会发生什么。后续子章节中讨论org.apache.catalina 中4个相关接口:Pipeline, Valve, ValveContext和Contained。

一个管道(pipeline)中包含了该容器要调用的所有任务。每一个阀门(valve)表示着一特定任务。一个容器的管道中有一个基本的阀门,但是我们可以添加任意想要添加的阀门。阀门的数目定义为添加的阀门的个数(不包括基本阀门)。有趣的是,阀门可以通过编辑 Tomcat 的配置文件 server.xml 来动态地添加。如图5.2UML展示:

图5.2: 管道(pipeline)和阀门(valve)

如果我们已经理解了 servlet 过滤器(filter),那么管道和它的阀门的工作机制就不难想象。一个管道线就像一个过滤链,每一个阀门像一个过滤器。跟过滤器一样,一个阀门可以操作处理传递给它的 request 和 response 对象。一个阀门完成处理后,它则进一步调用管道中的下一个阀门,基本阀门总是在最后才被调用。

一个容器可以有一个管道。当容器的 invoke() 方法被调用时,容器将通过管道处理,且管理调用在其中的第一个阀门,一个接一个阀门的调用处理,直到所有阀门都被处理完毕。可以想象管道的 invoke() 方法的伪代码如下所示:

// invoke each valve added to the pipeline
for (int n=0; n<valves.length; n++) {
    valve[n].invoke( ... );
}

// then, invoke the basic valve
basicValve.invoke( ... );

但是,Tomcat 设计者通过引入org.apache.catalina.ValveContext接口选择了一种不同处理方式。这里将介绍它是如何工作的。

容器不会硬编码它的invoke()方法被调用时应该做什么。反而,容器调用的是管道的 invoke()方法。管道接口的 invoke() 方法跟容器接口的invoke() 方法签名相同,方法签名如下:

public void invoke(Request request, Response response) throws IOException, ServletException;

这里是 Container 接口中 invoke() 方法在org.apache.catalina.core.ContainerBase 的实现:

public void invoke(Request request, Response response) throws IOException, ServletException {
    pipeline.invoke(request, response);
}

这里pipeline是容器中 Pipeline 接口的一个实例。

现在,管道必须保证添加给它的阀门必须如基本阀门一样被调用一次。管道通过创建一
个 ValveContext 接口的实例来实现。ValveContext 是管道的内部类,这样 ValveContext 就可以访问管道中所有成员。ValveContext 中最重要的方法是 invokeNext() 方法:

public void invokeNext(Request request, Response response)
        throws IOException, ServletException

在创建一个 ValveContext 实例之后,管道调用 ValveContext 的 invokeNext()方法。ValveContext 会先唤起管道中的第一个阀门,然后第一个阀门会在完成它的任务之前继续唤起下一个阀门。ValveContext 将它自己传递给每一个阀门,那么该阀门就可以调用 ValveContext 的 invokeNext() 方法。Valve 接口的 invoke()签名如下:

public void  invoke(Request request, Response response,
    ValveContext ValveContext) throws IOException, ServletException

一个阀门的 invoke() 方法可以如下实现:

public void invoke(Request request, Response response,
    ValveContext valveContext) throws IOException, ServletException {
    // Pass the request and response on to the next valve in our pipeline
    valveContext.invokeNext(request, response);
    // now perform what this valve is supposed to do
    ...
}

org.apache.catalina.core.StandardPipeline 类是所有容器中Pipeline的实现。在Tomcat4 中,这个类中有一个内部类 StandardPipelineValveContext 实现了ValveContext 接口,Listing5.1 展示了 StandardPipelineValveContext 类:

Listing 5.1: Tomcat 4中的StandardPipelineValveContext 类

protected class StandardPipelineValveContext implements ValveContext {
    protected int stage = 0;
    public String getInfo() {
        return info;
    }
    public void invokeNext(Request request, Response response)
        throws IOException, ServletException {
        int subscript = stage;
        stage = stage + 1;
        // Invoke the requested Valve for the current request thread
        if (subscript < valves.length) {
            valves[subscript].invoke(request, response, this);
        }else if ((subscript == valves.length) && (basic != null)) {
            basic.invoke(request, response, this);
        }else {
            throw new ServletException (sm.getString("standardPipeline.noValve"));
        }
    }
}

invokeNext()方法中使用subscript和stage记住哪个阀门被唤醒。当第一次唤醒时,subscript的值是 0,stage的值是 1。所以,第一个阀门(数组下标是0)被唤醒,管道的阀门获得 ValveContext 实例接收到ValveContext实例并调用它的 invokeNext() 方法。这时subscript的值是 1, 所以第二个阀门被唤醒,然后一步步地这样继续进行。

当invokeNext()在最后一个阀门中调用时,subscript值等于总阀门的个数。因此这时,基本阀门被唤醒调用。

Tomcat5 从 StandardPipeline 中删除了 StandardPipelineValveContext 类,而是使用 org.apache.catalina.core.StandardValveContext类来代替,如 Listing5.2 所示:

Listing 5.2: Tomcat5中StandardValveContext类

package org.apache.catalina.core;
import java.io.IOException;
import javax.servlet.ServletException;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.Valve;
import org.apache.catalina.ValveContext;
import org.apache.catalina.util.StringManager;

public final class StandardValveContext implements ValveContext {
    protected static StringManager sm = StringManager.getManager(Constants.Package);
    protected String info = "org.apache.catalina.core.StandardValveContext/1.0";
    protected int stage = 0;
    protected Valve basic = null;
    protected Valve valves[] = null;

    public String getInfo() {
        return info;
    }

    public final void invokeNext(Request request, Response response)
        throws IOException, ServletException {

        int subscript = stage;
        stage = stage + 1;
        // Invoke the requested Valve for the current request thread
        if (subscript < valves.length) {
            valves[subscript].invoke(request, response, this);
        }else if ((subscript == valves.length) && (basic != null)) {
            basic.invoke(request, response, this);
        }else {
            throw new ServletException(sm.getString("standardPipeline.noValve"));
        }
    }

    void set(Valve basic, Valve valves[]) {
        stage = 0;
        this.basic = basic;
        this.valves = valves;
    }
}

能发现到 Tomcat4 中 StandardPipelineValveContext 和 Tomcat 5 中StandardValveContext 相似地方吗?

接下来我们会讨论Pipeline、Valve和ValveContext的更多细节。还要讨论一Valve类通常要实现的org.apache.catalina.Contained接口。

5.2.1 Pipeline接口

我们提到的Pipeline接口的第一种方法是invoke()方法,容器调用它来开始调用管道中的阀门和基本阀门。Pipeline接口允许我们通过addValve()方法添加一个新的阀门或者通过removeValve()方法删除一个阀门。最后,可以使用 setBasic()方法来分配一个基本阀门给管道,使用getBasic()方法会得到基本阀门。最后调用的基本阀门负责处理请求和相应的响应。
Pipeline 接口如 Listing5.3

Listing 5.3: Pipeline接口

package org.apache.catalina;
import java.io.IOException;
import javax.servlet.ServletException;

public interface Pipeline {
    public Valve getBasic();
    public void setBasic(Valve valve);
    public void addValve(Valve valve);
    public Valve[] getValves();
    public void invoke(Request request, Response response)
        throws IOException, ServletException;
    public void removeValve(Valve valve);
}

5.2.2 Valve接口

Value接口表示一个阀门,该组件负责处理请求。该接口有两个方法,invoke() 和getInfo()方法。invoke()方法如上面已讨论过,getInfo()方法返回阀门的信息。Value接口如 Listing5.4

Listing 5.4: Valve 接口

package org.apache.catalina;
import java.io.IOException;
import javax.servlet.ServletException;

public interface Valve {
    public String getInfo();
    public void invoke(Request request, Response response,
        ValveContext context) throws IOException, ServletException;
}

5.2.3 ValveCont ext接口

ValveContext接口有两个方法,invokeNext()方法如上已讨论,getInfo()方法会返回valveContext的实现信息。ValveContext 接口如Listing5.5:

Listing 5.5: ValveContext 接口

package org.apache.catalina;
import java.io.IOException;
import javax.servlet.ServletException;

public interface ValveContext {
    public String getInfo();
    public void invokeNext(Request request, Response response)
        throws IOException, ServletException;
}

5.2.4 Contained接口

阀门类可以选择性实现org.apache.catalina.Contained接口。此接口指定实现类最多与一个相关联容器实例。Contained接口 如 Listing5.6:

Listing 5.6: Contained 接口

package org.apache.catalina;

public interface Contained {
    public Container getContainer();
    public void setContainer(Container container);
}

5.3 Wrapper接口

org.apache.catalina.Wrapper 接口表示一个包装器。包装器是表示单个servlet定义的容器。包装器继承了Container接口,并且添加了几个方法。包装器的实现类负责管理其servlet 的生命中期,包括 servlet 的init()、service()、和 destroy()方法。由于包装器是最底层的容器,所以不可以将子容器添加给它。如果 addChild()方法被调用,则会产生IllegalArgumantException 异常。

包装器接口中重要方法有 allocate() 和 load() 方法。allocate() 方法负责定位该包装器表示的 servlet 的实例。allocate()方法必须考虑一个 servlet 是否实现了javax.servlet.SingleThreadModel 接口,该部分内容将会在 11 章中进行讨论。load() 方法负责加载和初始化 servlet 实例。它们的签名如下:

public javax.servlet.Servlet allocate() throws javax.servlet.ServletException;
public void load() throws javax.servlet.ServletException;

其它方法将会在第 11章中介绍 org.apache.catalina.core.StandardWrapper类时涉及到。

5.4 Context接口

一个 context 容器表示一个 web 应用。一个 context 通常含有一个或多个包装器作为其子容器。重要的方法包括 addWrapper(), createWrapper() 等方法。该接口将会在第 12 章中详细介绍。

5.5 Wrapper应用Demo

这个应用Demo展示了如何写一个简单的容器模型。该应用程序的核心类是ex05.pyrmont.core.SimpleWrapper,它实现了 Wrapper 接口。SimpleWrapper类包括一个 Pipeline(由 ex05.pyrmont.core.SimplePipeline 实现)和一个Loader 类(ex05.pyrmont.core.SimpeLoader)来加载一个 servlet。管道包括一个基本阀门(ex05.pyrmont.core.SimpleWrapperValve)和两个另外的阀门
(ex05.pyrmont.core.ClientIPLoggerValve 和ex05.pyrmont.core.HeaderLoggerValve)。该应用的类结构图如图 5.3 所示:

这里写图片描述
图 5.3

注意:该容器使用的是Tomcat 4的默认连接器

包装器包装的是前面章节已经使用过的 ModernServlet类。这个应用程序表示一个 servlet 容器可以只有一单一的包装器构成。这些类都没有完整的实现,只是实现了必要方法。接下来看程序的具体实现。

5.5.1 ex05.pyrmont.core.SimpleLoader

容器中加载 servlet 的任务分配给了 Loader 实现。在该程序中 SimpleLoader就是一个 Loader 实现。它知道如何定位一个 servlet,并且通过 getClassLoader()获得一个 java.lang.ClassLoader 实例用来查找 servlet 类位置。SimpleLoader定义了 3 个变量,第一个是 WEB_ROOT 用来指明在哪里查找 servlet 类。

public static final String WEB_ROOT =
        System.getProperty("user.dir") + File.separator + "webroot";

另外两个变量是 ClassLoader 和 Container:

ClassLoader classLoader = null;
Container container = null;

SimpleLoader 类的构造器初始化类加载器,以便于准备返回一个 SimpleWrapper实例。

public SimpleLoader() {
    try {
        URL[] urls = new URL[l];
        URLStreamHandler streamHandler = null;
        File classPath = new File(WEB_ROOT);
        String repository = (new URL("file", null,
            classPath.getCanonicalPath() + File.separator)).toString() ;
        urls[0] = new URL(null, repository, streamHandler);
        classLoader = new URLClassLoader(urls);
    }catch (IOException e) {
        System.out.println(e.toString() );
    }
}

该程序的构造器用于初始化一个类加载器如前面章节所用的一样。container变量表示容器跟该加载器是相关联。

注意:加载器将在第8章详细讨论。

5.5.2 ex05.pyrmont.core.SimplePipeline

SimplePipeline 实现了 org.apache.catalina.Pipeline 接口。该类中最重要的方法是 invoke() 方法,其中包括了一个内部类 SimplePipelineValveContext。SimplePipelineValveContext 实现了 org.apache.catalina.ValveContext 接口如上面章节所介绍。

5.5.3 ex05.pyrmont.core. SimpleWrapper

该类实现了 org.apache.catalina.Wrapper 接口并且实现了 allocate() 方法和load() 方法,并声明了如下变量:

private Loader loader;
protected Container parent = null;

loader 变量用于加载一个 servlet 类。parent 变量表示该包装器的父容器。这意味着,该容器可以是其它容器的子容器,例如 Context。

需要特别注意 getLoader()方法,如 Listing5.7 所示:

Listing 5.7:

public Loader getLoader() {
    if (loader != null)
        return (loader);
    if (parent != null)
        return (parent.getLoader());
    return (null);
}

getLoader()方法用于返回一个 Loader 对象用于加载一个 servlet 类。如果一个包装器跟一个加载器相关联,会返回该加载器。否则返回其父容器的加载器,如果没有父容器,则返回 null。

SimpleWrapper 类有一个管道和该管道的基本阀门。这些工作在SimpleWrapper 的构造函数中完成。

Listing 5.8:

public SimpleWrapper() {
    pipeline.setBasic(new SimpleWrapperValve());
}

其中,pipeline是 SimplePipeline 类的一个实例:

private SimplePipeline pipeline = new SimplePipeline(this);

5.5.4 ex05.pyrmont.core. SimpleWrapperValve

SimpleWrapperValve 类是一个给 SimpleWrapper 类专门处理请求的基本阀门。它实现了 org.apache.catalina.Valve 接口和org.apache.catalina.Contained接口。最重要的方法是 invoke() 方法,如 Listing5.9 所示:

Listing 5.9: SimpleWrapperValve类的invoke()方法:

public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    SimpleWrapper wrapper = (SimpleWrapper) getContainer();
    ServletRequest sreq = request.getRequest();
    ServletResponse sres = response.getResponse();
    Servlet servlet = null;
    HttpServletRequest hreq = null;
    if (sreq instanceof HttpServletRequest)
      hreq = (HttpServletRequest) sreq;
    HttpServletResponse hres = null;
    if (sres instanceof HttpServletResponse)
      hres = (HttpServletResponse) sres;

    // Allocate a servlet instance to process this request
    try {
      servlet = wrapper.allocate();
      if (hres!=null && hreq!=null) {
        servlet.service(hreq, hres);
      }
      else {
        servlet.service(sreq, sres);
      }
    }
    catch (ServletException e) {
    }
  }

由于 SimpleWrapperValve 被当做一基本阀门来使用,所以它的 invoke() 方法不需要invokeNext()方法。invoke()方法调用SimpleWrapper的allocate()方法获得servlet的实例。然后调用 servlet 的 service() 方法。注意包装器管道的基本阀门唤醒的是 servlet 的 service ()方法,而不是 wrapper自己。

5.5.5 ex05.pyrmont. valves. ClientIPLoggerValve

ClientIPLoggerValve 是一个阀门,它打印客户端的 IP 地址到控制台。该类如 Listing5.10:

Listing 5.10: ClientIPLoggerValve类

package ex05.pyrmont.valves;

import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletException;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.Valve;
import org.apache.catalina.ValveContext;
import org.apache.catalina.Contained;
import org.apache.catalina.Container;


public class ClientIPLoggerValve implements Valve, Contained {

  protected Container container;

  public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    // Pass this request on to the next valve in our pipeline
    valveContext.invokeNext(request, response);
    System.out.println("Client IP Logger Valve");
    ServletRequest sreq = request.getRequest();
    System.out.println(sreq.getRemoteAddr());
    System.out.println("------------------------------------");
  }

  public String getInfo() {
    return null;
  }

  public Container getContainer() {
    return container;
  }

  public void setContainer(Container container) {
    this.container = container;
  }
}

注意 invoke() 方法,它的第一件事情是调用阀门上下文 invokeNext ()方法来唤醒下
一个阀门,然后它会打印出请求对象的 getRemoteAddr() 方法的输出。

5.5.6 ex05.pyrmont. valves. HeaderLoggerValve

该类跟 ClientIPLoggerValve 类非常相似。HeaderLoggerValve 是一个阀门打印请求头部信息到控制台上。该类如 Listing5.11:

Listing 5.11: HeaderLoggerValve 类

package ex05.pyrmont.valves;

import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletRequest;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.Valve;
import org.apache.catalina.ValveContext;
import org.apache.catalina.Contained;
import org.apache.catalina.Container;


public class HeaderLoggerValve implements Valve, Contained {

  protected Container container;

  public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    // Pass this request on to the next valve in our pipeline
    valveContext.invokeNext(request, response);

    System.out.println("Header Logger Valve");
    ServletRequest sreq = request.getRequest();
    if (sreq instanceof HttpServletRequest) {
      HttpServletRequest hreq = (HttpServletRequest) sreq;
      Enumeration headerNames = hreq.getHeaderNames();
      while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement().toString();
        String headerValue = hreq.getHeader(headerName);
        System.out.println(headerName + ":" + headerValue);
      }

    }
    else
      System.out.println("Not an HTTP Request");

    System.out.println("------------------------------------");
  }

  public String getInfo() {
    return null;
  }

  public Container getContainer() {
    return container;
  }

  public void setContainer(Container container) {
    this.container = container;
  }
}

注意其 invoke() 方法,该方法首先调用阀门的 invokeNext()方法唤醒下一个阀门。然后打印出头部的值。

5.5.7 ex05.pyrmont.startup.Bootstrap1

Bootstrap1 用于启动这个应用程序。

Listing 5.12: Bootstrap1 类

package ex05.pyrmont.startup;

import ex05.pyrmont.core.SimpleLoader;
import ex05.pyrmont.core.SimpleWrapper;
import ex05.pyrmont.valves.ClientIPLoggerValve;
import ex05.pyrmont.valves.HeaderLoggerValve;
import org.apache.catalina.Loader;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Valve;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;

public final class Bootstrap1 {
  public static void main(String[] args) {

/* call by using http://localhost:8080/ModernServlet,
   but could be invoked by any name */

    HttpConnector connector = new HttpConnector();
    Wrapper wrapper = new SimpleWrapper();
    wrapper.setServletClass("ModernServlet");
    Loader loader = new SimpleLoader();
    Valve valve1 = new HeaderLoggerValve();
    Valve valve2 = new ClientIPLoggerValve();

    wrapper.setLoader(loader);
    ((Pipeline) wrapper).addValve(valve1);
    ((Pipeline) wrapper).addValve(valve2);

    connector.setContainer(wrapper);

    try {
      connector.initialize();
      connector.start();

      // make the application wait until we press a key.
      System.in.read();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

创建 HttpConnector 和 SimpleWrapper 类的实例后,分配ModernServlet 给 SimpleWrapper 的 setServletClass() 方法,告诉包装器要加载的类的名字以便于加载。

wrapper.setServletClass("ModernServlet");

然后创建了加载器和两个阀门,然后将其加载器赋给包装器:

Loader loader = new SimpleLoader();
Valve valve1 = new HeaderLoggerValve();
Valve valve2 = new ClientIPLoggerValve();
wrapper.setLoader(loader);

然后把两个阀门添加到包装器管道中:

((Pipeline) wrapper).addValve(valve1);
((Pipeline) wrapper).addValve(valve2);

最后,把包装器当做容器添加到连接器中,然后初始化并启动连接器:

connector.setContainer(wrapper);
try {
    connector.initialize();
    connector.start();

下一行允许用户在控制台键入回车键以停止程序。

// make the application wait until we press Enter.
System.in.read();

5.5.8 运行Demo

在 windows 下,可以在工作目录下面如下运行该程序:

java -classpath ./lib/servlet.jar;./ ex05.pyrmont.startup.Bootstrap1

在 Linux 下,使用冒号分开两个库:

java -classpath ./lib/servlet.jar:./ ex05.pyrmont.startup.Bootstrap1

可以使用下面的 URL 来请求servlet:

http://localhost:8080

浏览器将会显示从 ModernServlet 得到的响应回复。跟下面相似的内容会显示在控制台上:

ModernServlet -- init
Client IP Logger Valve
127.0.0.1
------------------------------------
Header Logger Valve
host:localhost:8080
user-agent:Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-language:zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
accept-encoding:gzip, deflate
connection:keep-alive
upgrade-insecure-requests:1
------------------------------------

5.6 Context应用Demo

在本章第一个Demo中,介绍了如何部署一个仅仅包括一个包装器的简单web应用。该程序仅包括一个 servlet。也许会有一些应用仅仅需要一个 servlet,可是大多数的网络应用需要多个 servlet。在这些应用中,我们需要一个跟包装器(wrapper)不同的容器:上下文(context)。

第二个Demo将会示范如何使用一个包含两个包装器的上下文来包装两个servlet 类。当有多于一个包装器时,需要一个 map 来处理这些子容器——本Demo中使用Context容器,对于特殊的请求可以使用特殊的子容器来处理。

注意 使用 map 方法是在 Tomcat4 中,Tomcat 5 使用了另一种机制来查找子容器。

在这个程序中,mapper 是 ex05.pyrmont.core.SimpleContextMapper 类的一个实例,它继承Tomcat 4 中org.apache.catalina.Mapper 接口。一个容器也可以有多个 mapper 来支持多协议。例如容器可以用一个 mapper 来支持 HTTP 协议,而使用另一个 mapper 来支持 HTTPS 协议。Listing5.13 提供了 Tomcat4 中的 Mapper 接口。

Listing 5.13: Mapper 接口

package org.apache.catalina;

public interface Mapper {
    public Container getContainer();
    public void setContainer(Container container);
    public String getProtocol();
    ublic void setProtocol(String protocol);
    ublic Container map(Request request, boolean update);
}

getContainer()获取该容器的 mapper,setContainer() 方法用于关联一个容器到mapper。getProtocol() 返回该 mapper 负责处理的协议,setProtocol()用于分配该容器要处理的协议。map()方法返回处理一个特殊请求的子容器。
这里写图片描述

图5.4

图 5.4 是该Demo结构图。

SimpleContext表示一个上下文,它使用SimpleContextMapper作为它的mapper,SimpleContextValve 作为它的基本阀门。该上下文包括两个阀门ClientIPLoggerValve 和 HeaderLoggerValve。用 SimpleWrapper 表示的两个包装器作为该上下文的子容器被添加到其中。包装器 SimpleWrapperValve 作为它的基阀门,但是没有其它的阀门了。

该Context应用程序使用同一个加载器、两个阀门。但是加载器和阀门是跟该上下文关联的,而不是跟包装器关联。这样,两个包装器就可以都使用该加载器。该上下文被当做连接器的容器。因此,连接器每次收到一个 HTTP 请求可以使用上下文的 invoke() 方法。根据前面介绍的内容,其余的不难理解。

1》一个容器有一个管道,容器的 invoke() 方法会调用管道的invoke()方法
2》管道的 invoke()方法会调用添加到容器中的阀门的 invoke()方法,然后调用基本阀门的 invoke()方法
3》在一包装器中,基本阀门负责加载相关的 servlet 类并对请求作出相应
4》在一个有子容器的上下文中,基阀门使用 mapper 来查找负责处理请求的子容器。如果一个子容器被找到,子容器的 invoke() 方法会被调用,然后返回步骤 1

现在让我们看看实现中的处理流程。

SimpleContext 的 invoke() 方法调用管道的 invoke() 方法:

public void invoke(Request request, Response response)
    throws IOException, ServletException {
        pipeline.invoke(request, response);
}

pipeline是SimplePipeline 类实例,用来表示管道,它的 invoke()方法如下所示:

public void invoke(Request request, Response response)
    throws IOException, ServletException {
    // Invoke the first Valve in this pipeline for this request
    (new SimplePipelineValveContext()).invokeNext(request, response);
}

如“管道任务”一节中介绍的,该段代码唤醒所有阀门,然后调用基阀门的invoke()方法。在SimpleContext中SimpleContextValve代表着基阀门。在它的invoke()方法中SimpleContextValve使用上下文的mapper是查找一个包装器:

// Select the Wrapper to be used for this Request
Wrapper wrapper = null;
try {
    wrapper = (Wrapper) context.map(request, true);
}

如果一个包装器被找到,它的invoke()方法会被调用。

wrapper.invoke(request, response);

本Demo着通过SimpleWrapper表示一个包装器。如下是其invoke()方法,和SimpleContext类的invoke()方法完全一样。

public void invoke(Request request, Response response)
    throws IOException, ServletException {
    pipeline.invoke(request, response);
}

管道是SimplePipeline的一个实例,其调用方法已在上面列出。本Demo中包装器是SimpleWrapperValve的实例,它除了有基阀门外没其它阀门。包装器的管道调用SimpleWrapperValve类的invoke()方法,它分配一个servlet并调用其service()方法,如上文“Wrapper应用Demo”一节中所述。

注意,包装器不与加载器相关联,而是上下文与其关联。 因此,SimpleWrapper类的getLoader()方法返回父级(Context)的加载器。

有4个类:SimpleContext, SimpleContextValve, SimpleContextMapper和Bootstrap2在前面小节没被提到,在下面将讨论。

5.6.1 ex05.pyrmont.core.SimpleContextValve

此类作为SimpleContext的基阀门。它的最重要的方法invoke()代码如Listing 5.14:

Listing 5.14: SimpleContextValve类的invoke()方法:

public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {
    // Validate the request and response object types
    if (!(request.getRequest() instanceof HttpServletRequest) ||
      !(response.getResponse() instanceof HttpServletResponse)) {
      return;     // NOTE - Not much else we can do generically
    }

    // Disallow any direct access to resources under WEB-INF or META-INF
    HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
    String contextPath = hreq.getContextPath();
    String requestURI = ((HttpRequest) request).getDecodedRequestURI();
    String relativeURI =
      requestURI.substring(contextPath.length()).toUpperCase();

    Context context = (Context) getContainer();
    // Select the Wrapper to be used for this Request
    Wrapper wrapper = null;
    try {
      wrapper = (Wrapper) context.map(request, true);
    }
    catch (IllegalArgumentException e) {
      badRequest(requestURI, (HttpServletResponse) response.getResponse());
      return;
    }
    if (wrapper == null) {
      notFound(requestURI, (HttpServletResponse) response.getResponse());
      return;
    }
    // Ask this Wrapper to process this Request
    response.setContext(context);
    wrapper.invoke(request, response);
  }

5.6.2 ex05.pyrmont.core.SimpleContextMapper

SimpleContextMapper类代码如Listing 5.15,它实现Tomcat4中的org.apache.catalina.Mapper接口,并被设计成和SimpleContext实例相关联。

Listing 5.15: The SimpleContext 类

package ex05.pyrmont.core;

import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.Container;
import org.apache.catalina.HttpRequest;
import org.apache.catalina.Mapper;
import org.apache.catalina.Request;
import org.apache.catalina.Wrapper;

public class SimpleContextMapper implements Mapper {

  /**
   * The Container with which this Mapper is associated.
   */
  private SimpleContext context = null;

  public Container getContainer() {
    return (context);
  }

  public void setContainer(Container container) {
    if (!(container instanceof SimpleContext))
      throw new IllegalArgumentException
        ("Illegal type of container");
    context = (SimpleContext) container;
  }

  public String getProtocol() {
    return null;
  }

  public void setProtocol(String protocol) {
  }


  /**
   * Return the child Container that should be used to process this Request,
   * based upon its characteristics.  If no such child Container can be
   * identified, return <code>null</code> instead.
   *
   * @param request Request being processed
   * @param update Update the Request to reflect the mapping selection?
   *
   * @exception IllegalArgumentException if the relative portion of the
   *  path cannot be URL decoded
   */
  public Container map(Request request, boolean update) {
    // Identify the context-relative URI to be mapped
    String contextPath =
      ((HttpServletRequest) request.getRequest()).getContextPath();
    String requestURI = ((HttpRequest) request).getDecodedRequestURI();
    String relativeURI = requestURI.substring(contextPath.length());
    // Apply the standard request URI mapping rules from the specification
    Wrapper wrapper = null;
    String servletPath = relativeURI;
    String pathInfo = null;
    String name = context.findServletMapping(relativeURI);
    if (name != null)
      wrapper = (Wrapper) context.findChild(name);
    return (wrapper);
  }
}

如果我们传递一个不是SimpleContext实例的容器给setContainer()方法时,它将抛出IllegalArgumentException异常。map()方法返回一个子容器(wrapper),它负责处理请求。map()方法接收二个参数,一个是请求对象,一个是布尔值。这里的方法实现忽略了第二个参数。该方法执行的操作是检索从请求对象中得到上下文路径,并使用context的findServletMapping()方法获取与其路径关联的名称。 如果找到名称,它使用context的findChild()方法来获取Wrapper的实例。

5.6.3 ex05.pyrmont.core.SimpleContext

SimpleContext类是本Demo中Context一个实现。它是分配给连接器的主容器。然而,每个单独的servlet的处理由包装器执行。本应用Demo有2个servlet:PrimitiveServlet和ModernServlet,如此一来就有2个包装器。每个包装器都有名字。名称为Primitive是PrimitiveServlet的包装器,Modern是ModernServlet的包装器。对于每个请求,SimpleContext要决定调用哪个包装器,这个必须使用包装器的名称映射匹配请求URL。在此应用程序中,我们有两个可用于调用2个包装器的URL模式。第一个模式是/ Primitive,它映射到Primitive包装器。 第二个模式是/ Modern,它被映射到Modern包装器。 当然,对于给定的servlet,我们可以使用多个模式。 我们只需要添加这些模式即可。

从Container和Context接口有很多方法,SimpleContext必须实现。 大多数方法是留白,但是这些与映射相关的方法给予了实现代码。 这些方法如下:

1》addServletMapping()方法——添加URL/包装器名称映射对。 添加的每一个,可用于调用具有给定名称的包装器
2》findServletMapping()方法——获取和URL对应的包装器名称。这个方法用于为一个指定的URL查找哪个包装器应该被调用。如果给定的模式之前未通过addServletMapping()方法添加过,那么此方法将返回null
3》addMapper()方法——添加一个mapper给context。SimpleContext申明mapper和mappers变量。mapper为默认的mapper,mappers包含SimpleContext实例中所有mapper。第一个添加到SimpleContext中的mapper将作为默认mapper
4》findMapper()方法——查找当前mapper。在SimpleContext中,返回默认mapper。
5》map()方法——返回负责处理此请求的包装器

此外SimpleContext也提供了addChild(),findChild(), 和findChildren()方法实现。addChild()用于添加一个wrapper给context;findChild()用于获取指定名称的wrapper;findChildren()返回SimpleContext实例当中的所有包装器。

5.6.4 ex05.pyrmont.startup.Bootstrap2

Bootstrap2类用于启动程序。它和Bootstrap1非常类似,如Listing 5.16:

Listing 5.16: The Bootstrap2 类

package ex05.pyrmont.startup;

import ex05.pyrmont.core.SimpleContext;
import ex05.pyrmont.core.SimpleContextMapper;
import ex05.pyrmont.core.SimpleLoader;
import ex05.pyrmont.core.SimpleWrapper;
import ex05.pyrmont.valves.ClientIPLoggerValve;
import ex05.pyrmont.valves.HeaderLoggerValve;
import org.apache.catalina.Context;
import org.apache.catalina.Loader;
import org.apache.catalina.Mapper;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Valve;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;

public final class Bootstrap2 {
  public static void main(String[] args) {
    HttpConnector connector = new HttpConnector();
    Wrapper wrapper1 = new SimpleWrapper();
    wrapper1.setName("Primitive");
    wrapper1.setServletClass("PrimitiveServlet");
    Wrapper wrapper2 = new SimpleWrapper();
    wrapper2.setName("Modern");
    wrapper2.setServletClass("ModernServlet");

    Context context = new SimpleContext();
    context.addChild(wrapper1);
    context.addChild(wrapper2);

    Valve valve1 = new HeaderLoggerValve();
    Valve valve2 = new ClientIPLoggerValve();

    ((Pipeline) context).addValve(valve1);
    ((Pipeline) context).addValve(valve2);

    Mapper mapper = new SimpleContextMapper();
    mapper.setProtocol("http");
    context.addMapper(mapper);
    Loader loader = new SimpleLoader();
    context.setLoader(loader);
    // context.addServletMapping(pattern, name);
    context.addServletMapping("/Primitive", "Primitive");
    context.addServletMapping("/Modern", "Modern");
    connector.setContainer(context);
    try {
      connector.initialize();
      connector.start();

      // make the application wait until we press a key.
      System.in.read();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

main()方法由实例化Tomcat默认连接器和两个wrappers,wrapper1和wrapper2开始。 这些包装器被命名为Primitive和Modern。 Primitive和Modern的servlet类是PrimitiveServlet和ModernServlet。

HttpConnector connector = new HttpConnector();
Wrapper wrapper1 = new SimpleWrapper();
wrapper1.setName("Primitive");
wrapper1.setServletClass("PrimitiveServlet");
Wrapper wrapper2 = new SimpleWrapper();
wrapper2.setName("Modern");
wrapper2.setServletClass("ModernServlet");

然后,main()方法创建一个SimpleContext实例,并将wrapper1和wrapper2添加为SimpleContext的子容器。它还实例化两个阀门ClientIPLoggerValve和HeaderLoggerValve,并将它们添加到SimpleContext。

Context context = new SimpleContext();
context.addChild(wrapper1);
context.addChild(wrapper2);
Valve valve1 = new HeaderLoggerValve();
Valve valve2 = new ClientIPLoggerValve();
((Pipeline) context).addValve(valve1);
((Pipeline) context).addValve(valve2);

接下来,它从SimpleMapper类构造一个映射器对象并将其添加到SimpleContext。 此映射器负责在上下文中查找子容器来处理HTTP请求。

Mapper mapper = new SimpleContextMapper();
mapper.setProtocol("http");
context.addMapper(mapper);

要加载servlet类,则需要一个加载器。 在这里我们使用SimpleLoader类,正如在第一个应用Demo一样。 但是,不是将它添加到两个包装器,而是loader被添加到context中。 包装器将使用它的getLoader()找到加载器,因为context是其父级。

Loader loader = new SimpleLoader();
context.setLoader(loader);

现在,是时候添加servlet映射了。 我们为2个包装器添加了2个匹配模式。

// context.addServletMapping(pattern, name);
context.addServletMapping("/Primitive", "Primitive");
context.addServletMapping("/Modern", "Modern");

最后,将上下文指定为连接器的容器,并初始化和启动连接器。

connector.setContainer(context);
try {
    connector.initialize();
    connector.start();

5.6.5 运行Demo

在 windows 下,可以在工作目录下面如下运行该程序:

java -classpath ./lib/servlet.jar;./ ex05.pyrmont.startup.Bootstrap2

在 Linux 下,使用冒号分开两个库:

java -classpath ./lib/servlet.jar:./ ex05.pyrmont.startup.Bootstrap2

调用PrimitiveServlet,可以使用下面的 URL 来请求:

http://localhost:8080/ Primitive

调用ModernServlet,可以使用下面的 URL 来请求:

http://localhost:8080/ Modern

5.7小结

容器是连接器之后的第二个主模块。 容器使用许多其它模块,如Loader,Logger,Manager等。有4种类型容器:Engine,Host,Context和Wrapper。 Catalina部署没有必需所有4个容器都存在。 本章中的两个应用Demo展现了:部署可以具有单个Wrapper或含有几个wrapper的Context。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值