How tomcat works——5 容器

来源:互联网 发布:网络推广经理职责 编辑:程序博客网 时间:2024/05/29 10:09

容器(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 pipelinefor (int n=0; n<valves.length; n++) {    valve[n].invoke( ... );}// then, invoke the basic valvebasicValve.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 -- initClient IP Logger Valve127.0.0.1------------------------------------Header Logger Valvehost:localhost:8080user-agent:Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8accept-language:zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3accept-encoding:gzip, deflateconnection:keep-aliveupgrade-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 RequestWrapper 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