1.Tomcat 系统架构与设计模式,第 1 部分: 工作原理

来源:互联网 发布:先导爱知出现集数 编辑:程序博客网 时间:2024/06/05 04:30

这个分为两个部分的系列文章将研究 Apache Tomcat 的系统架构以及其运用的很多经典设计模式。本文是第 1 部分,将主要从 Tomcat 如何分发请求、如何处理多用户同时请求,还有它的多级容器是如何协调工作的角度来分析 Tomcat 的工作原理,这也是一个 Web 服务器首要解决的关键问题。



本文以 Tomcat 5 为基础,也兼顾最新的 Tomcat 6 和 Tomcat 4。Tomcat 的基本设计思路和架构是具有一定连续性的。

Tomcat 总体结构

Tomcat 的结构很复杂,但是 Tomcat 也非常的模块化,找到了 Tomcat 最核心的模块,您就抓住了 Tomcat 的“七寸”。下面是 Tomcat 的总体结构图:

图 1.Tomcat 的总体结构
图 1.Tomcat 的总体结构

从上图中可以看出 Tomcat 的心脏是两个组件:Connector 和 Container,关于这两个组件将在后面详细介绍。Connector 组件是可以被替换,这样可以提供给服务器设计者更多的选择,因为这个组件是如此重要,不仅跟服务器的设计的本身,而且和不同的应用场景也十分相关,所以一个 Container 可以选择对应多个 Connector。多个 Connector 和一个 Container 就形成了一个 Service,Service 的概念大家都很熟悉了,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了。所以整个 Tomcat 的生命周期由 Server 控制。

以 Service 作为“婚姻”

我们将 Tomcat 中 Connector、Container 作为一个整体比作一对情侣的话,Connector 主要负责对外交流,可以比作为 Boy,Container 主要处理 Connector 接受的请求,主要是处理内部事务,可以比作为 Girl。那么这个 Service 就是连接这对男女的结婚证了。是 Service 将它们连接在一起,共同组成一个家庭。当然要组成一个家庭还要很多其它的元素。

说白了,Service 只是在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。这个 Service 接口的方法列表如下:

图 2. Service 接口
图 2. Service 接口

从 Service 接口中定义的方法中可以看出,它主要是为了关联 Connector 和 Container,同时会初始化它下面的其它组件,注意接口中它并没有规定一定要控制它下面的组件的生命周期。所有组件的生命周期在一个 Lifecycle 的接口中控制,这里用到了一个重要的设计模式,关于这个接口将在后面介绍。

Tomcat 中 Service 接口的标准实现类是 StandardService 它不仅实现了 Service 借口同时还实现了 Lifecycle 接口,这样它就可以控制它下面的组件的生命周期了。StandardService 类结构图如下:

图 3. StandardService 的类结构图
图 3. StandardService 的类结构图

从上图中可以看出除了 Service 接口的方法的实现以及控制组件生命周期的 Lifecycle 接口的实现,还有几个方法是用于在事件监听的方法的实现,不仅是这个 Service 组件,Tomcat 中其它组件也同样有这几个方法,这也是一个典型的设计模式,将在后面介绍。

下面看一下 StandardService 中主要的几个方法实现的代码,下面是 setContainer 和 addConnector 方法的源码:

清单 1. StandardService. SetContainer
public void setContainer(Container container) {    Container oldContainer = this.container;    if ((oldContainer != null) && (oldContainer instanceof Engine))        ((Engine) oldContainer).setService(null);    this.container = container;    if ((this.container != null) && (this.container instanceof Engine))        ((Engine) this.container).setService(this);    if (started && (this.container != null) && (this.container instanceof Lifecycle)) {        try {            ((Lifecycle) this.container).start();        } catch (LifecycleException e) {            ;        }    }    synchronized (connectors) {        for (int i = 0; i < connectors.length; i++)            connectors[i].setContainer(this.container);    }    if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) {        try {            ((Lifecycle) oldContainer).stop();        } catch (LifecycleException e) {            ;        }    }    support.firePropertyChange("container", oldContainer, this.container);}

这段代码很简单,其实就是先判断当前的这个 Service 有没有已经关联了 Container,如果已经关联了,那么去掉这个关联关系—— oldContainer.setService(null)。如果这个 oldContainer 已经被启动了,结束它的生命周期。然后再替换新的关联、再初始化并开始这个新的 Container 的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改 Container 时要将新的 Container 关联到每个 Connector,还好 Container 和 Connector 没有双向关联,不然这个关联关系将会很难维护。

清单 2. StandardService. addConnector
public void addConnector(Connector connector) {    synchronized (connectors) {        connector.setContainer(this.container);        connector.setService(this);        Connector results[] = new Connector[connectors.length + 1];        System.arraycopy(connectors, 0, results, 0, connectors.length);        results[connectors.length] = connector;        connectors = results;        if (initialized) {            try {                connector.initialize();            } catch (LifecycleException e) {                e.printStackTrace(System.err);            }        }        if (started && (connector instanceof Lifecycle)) {            try {                ((Lifecycle) connector).start();            } catch (LifecycleException e) {                ;            }        }        support.firePropertyChange("connector", null, connector);    }}

上面是 addConnector 方法,这个方法也很简单,首先是设置关联关系,然后是初始化工作,开始新的生命周期。这里值得一提的是,注意 Connector 用的是数组而不是 List 集合,这个从性能角度考虑可以理解,有趣的是这里用了数组但是并没有向我们平常那样,一开始就分配一个固定大小的数组,它这里的实现机制是:重新创建一个当前大小的数组对象,然后将原来的数组对象 copy 到新的数组中,这种方式实现了类似的动态数组的功能,这种实现方式,值得我们以后拿来借鉴。

最新的 Tomcat6 中 StandardService 也基本没有变化,但是从 Tomcat5 开始 Service、Server 和容器类都继承了 MBeanRegistration 接口,Mbeans 的管理更加合理。

以 Server 为“居”

前面说一对情侣因为 Service 而成为一对夫妻,有了能够组成一个家庭的基本条件,但是它们还要有个实体的家,这是它们在社会上生存之本,有了家它们就可以安心的为人民服务了,一起为社会创造财富。

Server 要完成的任务很简单,就是要能够提供一个接口让其它程序能够访问到这个 Service 集合、同时要维护它所包含的所有 Service 的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的 Service。还有其它的一些次要的任务,如您住在这个地方要向当地政府去登记啊、可能还有要配合当地公安机关日常的安全检查什么的。

Server 的类结构图如下:

图 4. Server 的类结构图
图 4. Server 的类结构图

它的标准实现类 StandardServer 实现了上面这些方法,同时也实现了 Lifecycle、MbeanRegistration 两个接口的所有方法,下面主要看一下 StandardServer 重要的一个方法 addService 的实现:

清单 3. StandardServer.addService
public void addService(Service service) {    service.setServer(this);    synchronized (services) {        Service results[] = new Service[services.length + 1];        System.arraycopy(services, 0, results, 0, services.length);        results[services.length] = service;        services = results;        if (initialized) {            try {                service.initialize();            } catch (LifecycleException e) {                e.printStackTrace(System.err);            }        }        if (started && (service instanceof Lifecycle)) {            try {                ((Lifecycle) service).start();            } catch (LifecycleException e) {                ;            }        }        support.firePropertyChange("service", null, service);    }}

从上面第一句就知道了 Service 和 Server 是相互关联的,Server 也是和 Service 管理 Connector 一样管理它,也是将 Service 放在一个数组中,后面部分的代码也是管理这个新加进来的 Service 的生命周期。Tomcat6 中也是没有什么变化的。

组件的生命线“Lifecycle”

前面一直在说 Service 和 Server 管理它下面组件的生命周期,那它们是如何管理的呢?

Tomcat 中组件的生命周期是通过 Lifecycle 接口来控制的,组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了,这样一层一层的直到一个最高级的组件就可以控制 Tomcat 中所有组件的生命周期,这个最高的组件就是 Server,而控制 Server 的是 Startup,也就是您启动和关闭 Tomcat。

下面是 Lifecycle 接口的类结构图:

图 5. Lifecycle 类结构图
图 5. Lifecycle 类结构图

除了控制生命周期的 Start 和 Stop 方法外还有一个监听机制,在生命周期开始和结束的时候做一些额外的操作。这个机制在其它的框架中也被使用,如在 Spring 中。关于这个设计模式会在后面介绍。

Lifecycle 接口的方法的实现都在其它组件中,就像前面中说的,组件的生命周期由包含它的父组件控制,所以它的 Start 方法自然就是调用它下面的组件的 Start 方法,Stop 方法也是一样。如在 Server 中 Start 方法就会调用 Service 组件的 Start 方法,Server 的 Start 方法代码如下:

清单 4. StandardServer.Start
public void start() throws LifecycleException {    if (started) {        log.debug(sm.getString("standardServer.start.started"));        return;    }    lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);    lifecycle.fireLifecycleEvent(START_EVENT, null);    started = true;    synchronized (services) {        for (int i = 0; i < services.length; i++) {            if (services[i] instanceof Lifecycle)                ((Lifecycle) services[i]).start();        }    }    lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);}

监听的代码会包围 Service 组件的启动过程,就是简单的循环启动所有 Service 组件的 Start 方法,但是所有 Service 必须要实现 Lifecycle 接口,这样做会更加灵活。

Server 的 Stop 方法代码如下:

清单 5. StandardServer.Stop
public void stop() throws LifecycleException {    if (!started)        return;    lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);    lifecycle.fireLifecycleEvent(STOP_EVENT, null);    started = false;    for (int i = 0; i < services.length; i++) {        if (services[i] instanceof Lifecycle)            ((Lifecycle) services[i]).stop();    }    lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);}

它所要做的事情也和 Start 方法差不多。

Connector 组件

Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。

由于这个过程比较复杂,大体的流程可以用下面的顺序图来解释:

图 6. Connector 处理一次请求顺序图
图 6. Connector 处理一次请求顺序图

(查看清晰大图)

Tomcat5 中默认的 Connector 是 Coyote,这个 Connector 是可以选择替换的。Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心。Tomcat5 将这个过程更加细化,它将 Connector 划分成 Connector、Processor、Protocol, 另外 Coyote 也定义自己的 Request 和 Response 对象。

下面主要看一下 Tomcat 中如何处理多线程的连接请求,先看一下 Connector 的主要类图:

图 7. Connector 的主要类图
图 7. Connector 的主要类图

(查看清晰大图)

看一下 HttpConnector 的 Start 方法:

清单 6. HttpConnector.Start
public void start() throws LifecycleException {    if (started)        throw new LifecycleException            (sm.getString("httpConnector.alreadyStarted"));    threadName = "HttpConnector[" + port + "]";    lifecycle.fireLifecycleEvent(START_EVENT, null);    started = true;    threadStart();    while (curProcessors < minProcessors) {        if ((maxProcessors > 0) && (curProcessors >= maxProcessors))            break;        HttpProcessor processor = newProcessor();        recycle(processor);    }}

threadStart() 执行就会进入等待请求的状态,直到一个新的请求到来才会激活它继续执行,这个激活是在 HttpProcessor 的 assign 方法中,这个方法是代码如下 

清单 7. HttpProcessor.assign
synchronized void assign(Socket socket) {    while (available) {        try {            wait();        } catch (InterruptedException e) {        }    }    this.socket = socket;    available = true;    notifyAll();    if ((debug >= 1) && (socket != null))        log(" An incoming request is being assigned");}

创建 HttpProcessor 对象是会把 available 设为 false,所以当请求到来时不会进入 while 循环,将请求的 socket 赋给当期处理的 socket,并将 available 设为 true,当 available 设为 true 是 HttpProcessor 的 run 方法将被激活,接下去将会处理这次请求。

Run 方法代码如下:

清单 8. HttpProcessor.Run
public void run() {     while (!stopped) {         Socket socket = await();         if (socket == null)             continue;         try {             process(socket);         } catch (Throwable t) {             log("process.invoke", t);         }         connector.recycle(this);     }     synchronized (threadSync) {         threadSync.notifyAll();     } }

解析 socket 的过程在 process 方法中,process 方法的代码片段如下:

清单 9. HttpProcessor.process
 private void process(Socket socket) {    boolean ok = true;    boolean finishResponse = true;    SocketInputStream input = null;    OutputStream output = null;    try {        input = new SocketInputStream(socket.getInputStream(),connector.getBufferSize());    } catch (Exception e) {        log("process.create", e);        ok = false;    }    keepAlive = true;    while (!stopped && ok && keepAlive) {        finishResponse = true;        try {            request.setStream(input);            request.setResponse(response);            output = socket.getOutputStream();            response.setStream(output);            response.setRequest(request);            ((HttpServletResponse) response.getResponse()).setHeader("Server", SERVER_INFO);        } catch (Exception e) {            log("process.create", e);            ok = false;        }        try {            if (ok) {                parseConnection(socket);                parseRequest(input, output);                if (!request.getRequest().getProtocol().startsWith("HTTP/0"))                    parseHeaders(input);                if (http11) {                    ackRequest(output);                    if (connector.isChunkingAllowed())                        response.setAllowChunking(true);                }            }        。。。。。。        try {            ((HttpServletResponse) response).setHeader                ("Date", FastHttpDateFormat.getCurrentDate());            if (ok) {                connector.getContainer().invoke(request, response);            }            。。。。。。        }        try {            shutdownInput(input);            socket.close();        } catch (IOException e) {            ;        } catch (Throwable e) {            log("process.invoke", e);        }    socket = null;}

当 Connector 将 socket 连接封装成 request 和 response 对象后接下来的事情就交给 Container 来处理了。

Servlet 容器“Container”

Container 是容器的父接口,所有子容器都必须实现这个接口,Container 容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是:Engine、Host、Context、Wrapper,这四个组件不是平行的,而是父子关系,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。通常一个 Servlet class 对应一个 Wrapper,如果有多个 Servlet 就可以定义多个 Wrapper,如果有多个 Wrapper 就要定义一个更高的 Container 了,如 Context,Context 通常就是对应下面这个配置:

清单 10. Server.xml
<Context     path="/library"    docBase="D:\projects\library\deploy\target\library.war"     reloadable="true"/>

容器的总体设计

Context 还可以定义在父容器 Host 中,Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。

那么这些容器是如何协同工作的呢?先看一下它们之间的关系图:

图 8. 四个容器的关系图
图 8. 四个容器的关系图

(查看清晰大图)

当 Connector 接受到一个连接请求时,将请求交给 Container,Container 是如何处理这个请求的?这四个组件是怎么分工的,怎么把请求传给特定的子容器的呢?又是如何将最终的请求交给 Servlet 处理。下面是这个过程的时序图:

图 9. Engine 和 Host 处理请求的时序图
图 9. Engine 和 Host 处理请求的时序图

(查看清晰大图)

这里看到了 Valve 是不是很熟悉,没错 Valve 的设计在其他框架中也有用的,同样 Pipeline 的原理也基本是相似的,它是一个管道,Engine 和 Host 都会执行这个 Pipeline,您可以在这个管道上增加任意的 Valve,Tomcat 会挨个执行这些 Valve,而且四个组件都会有自己的一套 Valve 集合。您怎么才能定义自己的 Valve 呢?在 server.xml 文件中可以添加,如给 Engine 和 Host 增加一个 Valve 如下:

清单 11. Server.xml
<Engine defaultHost="localhost" name="Catalina">    <Valve className="org.apache.catalina.valves.RequestDumperValve"/>    ………    <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true"     xmlNamespaceAware="false" xmlValidation="false">        <Valve className="org.apache.catalina.valves.FastCommonAccessLogValve"            directory="logs"  prefix="localhost_access_log." suffix=".txt"            pattern="common" resolveHosts="false"/>       …………    </Host></Engine>

StandardEngineValve 和 StandardHostValve 是 Engine 和 Host 的默认的 Valve,它们是最后一个 Valve 负责将请求传给它们的子容器,以继续往下执行。

前面是 Engine 和 Host 容器的请求过程,下面看 Context 和 Wrapper 容器时如何处理请求的。下面是处理请求的时序图:

图 10. Context 和 wrapper 的处理请求时序图
图 10. Context 和 wrapper 的处理请求时序图

(查看清晰大图)

从 Tomcat5 开始,子容器的路由放在了 request 中,request 中保存了当前请求正在处理的 Host、Context 和 wrapper。

Engine 容器

Engine 容器比较简单,它只定义了一些基本的关联关系,接口类图如下:

图 11. Engine 接口的类结构
图 11. Engine 接口的类结构

它的标准实现类是 StandardEngine,这个类注意一点就是 Engine 没有父容器了,如果调用 setParent 方法时将会报错。添加子容器也只能是 Host 类型的,代码如下:

清单 12. StandardEngine. addChild
public void addChild(Container child) {    if (!(child instanceof Host))        throw new IllegalArgumentException            (sm.getString("standardEngine.notHost"));    super.addChild(child);}public void setParent(Container container) {    throw new IllegalArgumentException        (sm.getString("standardEngine.notParent"));}

它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。

Host 容器

Host 是 Engine 的字容器,一个 Host 在 Engine 中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。

下面是和 Host 相关的类关联图:

图 12. Host 相关的类图
图 12. Host 相关的类图

(查看清晰大图)

从上图中可以看出除了所有容器都继承的 ContainerBase 外,StandardHost 还实现了 Deployer 接口,上图清楚的列出了这个接口的主要方法,这些方法都是安装、展开、启动和结束每个 web application。

Deployer 接口的实现是 StandardHostDeployer,这个类实现了的最要的几个方法,Host 可以调用这些方法完成应用的部署等。

Context 容器

Context 代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。

Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢? Tomcat5 以前是通过一个 Mapper 类来管理的,Tomcat5 以后这个功能被移到了 request 中,在前面的时序图中就可以发现获取子容器都是通过 request 来分配的。

Context 准备 Servlet 的运行环境是在 Start 方法开始的,这个方法的代码片段如下:

清单 13. StandardContext.start
public synchronized void start() throws LifecycleException {    ………    if( !initialized ) {         try {            init();        } catch( Exception ex ) {            throw new LifecycleException("Error initializaing ", ex);        }    }    ………    lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);    setAvailable(false);    setConfigured(false);    boolean ok = true;    File configBase = getConfigBase();    if (configBase != null) {        if (getConfigFile() == null) {            File file = new File(configBase, getDefaultConfigFile());            setConfigFile(file.getPath());            try {                File appBaseFile = new File(getAppBase());                if (!appBaseFile.isAbsolute()) {                    appBaseFile = new File(engineBase(), getAppBase());                }                String appBase = appBaseFile.getCanonicalPath();                String basePath =                     (new File(getBasePath())).getCanonicalPath();                if (!basePath.startsWith(appBase)) {                    Server server = ServerFactory.getServer();                    ((StandardServer) server).storeContext(this);                }            } catch (Exception e) {                log.warn("Error storing config file", e);            }        } else {            try {                String canConfigFile =  (new File(getConfigFile())).getCanonicalPath();                if (!canConfigFile.startsWith (configBase.getCanonicalPath())) {                    File file = new File(configBase, getDefaultConfigFile());                    if (copy(new File(canConfigFile), file)) {                        setConfigFile(file.getPath());                    }                }            } catch (Exception e) {                log.warn("Error setting config file", e);            }        }    }    ………    Container children[] = findChildren();    for (int i = 0; i < children.length; i++) {        if (children[i] instanceof Lifecycle)            ((Lifecycle) children[i]).start();    }    if (pipeline instanceof Lifecycle)        ((Lifecycle) pipeline).start();    ………}

它主要是设置各种资源属性和管理组件,还有非常重要的就是启动子容器和 Pipeline。

我们知道 Context 的配置文件中有个 reloadable 属性,如下面配置:

清单 14. Server.xml
<Context     path="/library"     docBase="D:\projects\library\deploy\target\library.war"     reloadable="true" />

当这个 reloadable 设为 true 时,war 被修改后 Tomcat 会自动的重新加载这个应用。如何做到这点的呢 ? 这个功能是在 StandardContext 的 backgroundProcess 方法中实现的,这个方法的代码如下:

清单 15. StandardContext. backgroundProcess
public void backgroundProcess() {    if (!started) return;    count = (count + 1) % managerChecksFrequency;    if ((getManager() != null) && (count == 0)) {        try {            getManager().backgroundProcess();        } catch ( Exception x ) {            log.warn("Unable to perform background process on manager",x);        }    }    if (getLoader() != null) {        if (reloadable && (getLoader().modified())) {            try {                Thread.currentThread().setContextClassLoader                    (StandardContext.class.getClassLoader());                reload();            } finally {                if (getLoader() != null) {                    Thread.currentThread().setContextClassLoader                        (getLoader().getClassLoader());                }            }        }        if (getLoader() instanceof WebappLoader) {            ((WebappLoader) getLoader()).closeJARs(false);        }    }}

它会调用 reload 方法,而 reload 方法会先调用 stop 方法然后再调用 Start 方法,完成 Context 的一次重新加载。可以看出执行 reload 方法的条件是 reloadable 为 true 和应用被修改,那么这个 backgroundProcess 方法是怎么被调用的呢?

这个方法是在 ContainerBase 类中定义的内部类 ContainerBackgroundProcessor 被周期调用的,这个类是运行在一个后台线程中,它会周期的执行 run 方法,它的 run 方法会周期调用所有容器的 backgroundProcess 方法,因为所有容器都会继承 ContainerBase 类,所以所有容器都能够在 backgroundProcess 方法中定义周期执行的事件。

Wrapper 容器

Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。

下面看一下非常重要的一个方法 loadServlet,代码片段如下:

清单 16. StandardWrapper.loadServlet
public synchronized Servlet loadServlet() throws ServletException {    ………    Servlet servlet;    try {        ………        ClassLoader classLoader = loader.getClassLoader();        ………        Class classClass = null;        ………        servlet = (Servlet) classClass.newInstance();        if ((servlet instanceof ContainerServlet) &&            (isContainerProvidedServlet(actualClass) ||            ((Context)getParent()).getPrivileged() )) {                ((ContainerServlet) servlet).setWrapper(this);        }        classLoadTime=(int) (System.currentTimeMillis() -t1);        try {            instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet);            if( System.getSecurityManager() != null) {                Class[] classType = new Class[]{ServletConfig.class};                Object[] args = new Object[]{((ServletConfig)facade)};                SecurityUtil.doAsPrivilege("init",servlet,classType,args);            } else {                servlet.init(facade);            }            if ((loadOnStartup >= 0) && (jspFile != null)) {                ………                if( System.getSecurityManager() != null) {                    Class[] classType = new Class[]{ServletRequest.class,                        ServletResponse.class};                    Object[] args = new Object[]{req, res};                    SecurityUtil.doAsPrivilege("service",servlet,classType,args);                } else {                    servlet.service(req, res);                }            }            instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet);            ………        return servlet;}

它基本上描述了对 Servlet 的操作,当装载了 Servlet 后就会调用 Servlet 的 init 方法,同时会传一个 StandardWrapperFacade 对象给 Servlet,这个对象包装了 StandardWrapper,ServletConfig 与它们的关系图如下:

图 13. ServletConfig 与 StandardWrapperFacade、StandardWrapper 的关系
图 13. ServletConfig 与 StandardWrapperFacade、StandardWrapper 的关系

Servlet 可以获得的信息都在 StandardWrapperFacade 封装,这些信息又是在 StandardWrapper 对象中拿到的。所以 Servlet 可以通过 ServletConfig 拿到有限的容器的信息。

当 Servlet 被初始化完成后,就等着 StandardWrapperValve 去调用它的 service 方法了,调用 service 方法之前要调用 Servlet 所有的 filter。

Tomcat 中其它组件

Tomcat 还有其它重要的组件,如安全组件 security、logger 日志组件、session、mbeans、naming 等其它组件。这些组件共同为 Connector 和 Container 提供必要的服务。





面向初级 Web 开发人员的 Tomcat

Apache Tomcat 应用服务器不再是高级 Web 系统开发人员的专用领域。在本教程中,Sing Li 将向初级 Web 开发人员展示如何利用他们当前的 Java™ 开发技能,使用 Tomcat 编写服务器端 JSP、servlet 和 Web 服务。



开始之前

关于本教程

本教程向 Java Web 开发人员介绍使用 Tomcat 对 JavaServer Pages (JSP)、servlet 和 Web 服务进行编程,Tomcat 是来自 Apache Foundation 的开源应用服务器。本教程引导您完成以下任务:

  • 下载和安装您自己的 Tomcat 服务器。
  • 在 Tomcat 上编码和部署 JSP。
  • 在 Tomcat 上编码和部署 servlet。
  • 使用 Tomcat 和 Apache Axis 编码和部署 Web 服务。

本教程概述 JSP、servlet 和 Web 服务,但不会深入讨论这些技术。

必要条件

要最大限度地利用本教程,您需要熟悉 Java 编程语言、面向对象设计原理和基本 TCP/IP 网络概念。最好理解 JDK 中的网络 API,但并非必须如此。

要运行本教程中的示例,需要:

  • 有效的 JDK 1.5.0 或更高版本的安装程序。
  • 有效的 Tomcat 5.5 或更高版本的安装程序,可从 http://jakarta.apache.org/tomcat/ 获得。本教程包含详细的 Tomcat 下载、安装和设置说明。

要运行 Web 服务示例,还需要安装:

  • Apache Ant 1.5.2 或更高版本,可从 http://ant.apache.org/ 获得
  • Apache Axis 1.2.1 或更高版本,可从 http://ws.apache.org/axis/ 获得。本教程包含详细的 Axis 安装说明。

运行本教程推荐的系统配置:

  • 支持 JDK 1.5.0 且内存至少为 512MB 的系统。本教程中的说明基于运行 Microsoft Windows 的系统。
  • 安装软件和示例至少需要 50MB 的磁盘空间。

Tomcat 是什么?

Tomcat 是来自 Apache Software Foundation 的开源服务器。它是一个 Web 应用服务器,这说明它支持 JavaServer Pages (JSP) 和 servlet 进行编程。

自 2000 年初期,Tomcat 一直担当着最新的 Java Servlet 和 JSP 规范的参考实现。Tomcat 5.5 是编写本教程时的最新版本,支持最新的 Java Servlet 2.4 和 JavaServer Pages 2.0 标准(参见 参考资料)。Tomcat 还包括一个受限的 Web 服务器,该服务器以独立模式(默认情况下)执行时能服务于静态网页。

由于有多种开源库和扩展,因此 Tomcat 支持:

  • 使用 Apache Axis servlet 的 Web 服务
  • 开发框架,如 Apache Struts
  • 模板引擎,如 Apache Jakarta Velocity
  • 对象关系映射技术,如 Hibernate

本教程展示如何使用 Tomcat 学习 JSP、servlet 和 Web 服务编程。与 Tomcat 结合使用 Struts、Velocity 和 Hibernate 不属于本教程的范围。

过去,因为配置和管理 Tomcat 需要高级专业知识,主要的 Tomcat 用户都是高级的服务器端应用程序开发人员。现在,由于 Tomcat 的 GUI 安装程序的出现、将服务器安装为系统服务的能力以及服务器功能的稳定性,甚至初级 Web 开发人员都能使用这个通用的服务器。

Tomcat 安装与设置

下载 Tomcat

要下载最新版本的 Tomcat,请访问 Apache Tomcat 主页(参见 必要条件),如图 1 所示,并单击 Download 标题下的 Tomcat 5.x 链接(图 1 中红色边框内的区域):

图1. Apache Tomcat 项目主页
Apache Tomcat 主页

第3页,共11页 文档选项 打印此页 PDF - A4 475 KB PDF - Letter 479 KB 获取 Adobe® Reader® 样例代码 对本教程的评价 帮助我们改进此内容 Tomcat 安装与设置 下载 Tomcat 要下载最新版本的 Tomcat,请访问 Apache Tomcat 主页(参见必要条件),如图 1 所示,并单击 Download 标题下的 Tomcat 5.x 链接(图 1 中红色边框内的区域): 图1. Apache Tomcat 项目主页 可以在最新的 5.5.x 发行版中进行选择。选择最新的稳定(非 Beta 和非 Alpha)发行版的二进制分发版。对于 Windows 系统,下载 EXE 二进制文件进行简单安装。

安装 Tomcat

EXE 二进制安装程序完成以下事项:

  • 解压并安装 Tomcat 服务器组件。
  • 要求您指定侦听传入请求时,服务器将使用的 TCP 端口。(TCP 端口是一个网络端点,用数字表示,客户机应用程序可以在连接到服务器时指定该数字。)
  • 将服务器配置为作为系统服务运行。

启动 EXE 安装程序。您将看到最初的欢迎屏幕,如图 2 所示:

图 2. Tomcat 设置向导欢迎屏幕
Tomcat 设置向导欢迎屏幕

EXE 安装程序使用运行一个带有逐步说明的向导。必须拥有对机器的管理权限,因为 Tomcat 是作为系统服务安装的。如果使用自己的 PC,作为默认用户并成功安装了其他软件,可能就已经有了管理权限。

表 1 描述了设置向导的各个屏幕所提示的项以及您应该做出的响应。

表1. Tomcat 设置向导提示设置向导屏幕描述许可协议这是 Apache License 2.0,是现有的开源软件许可中的一种。仔细阅读许可条款。如果同意条款,请单击 I Agree 按钮继续。选择组件选择 Tomcat 组件进行安装。默认情况下会选中必需的组件。如果有足够的磁盘空间,可以考虑安装示例。它们对于 Web 应用程序编程很有用。选择安装位置选择在计算机上安装 Tomcat 服务器的目录。如果是初次安装,使用向导选择的默认设置即可。该屏幕还显示了 Tomcat 安装将占用的磁盘空间以及磁盘上可用的空间。配置该屏幕用于执行基本 Tomcat 服务器配置。可以选择安装服务器的 TCP 端口,以及管理员用户名和密码。建议将 TCP 端口设置为 8080。保留管理员用户名为 admin 并输入自己的管理员密码。不要忘记密码;稍后将需要使用它来部署本教程中的示例。Java 虚拟机该屏幕用于选择运行 Tomcat 的 JVM。除非机器上安装了多个 JDK,否则使用默认设置即可。对于最新的 Tomcat 5.5 发行版,应该选择 JVM 1.5.0 版或更高版本。完成 Apache Tomcat 设置向导这是安装的最后一步。选择 Run Apache Tomcat 复选框。这将在安装后立即启动系统服务。

注意,在某些带有防火墙的 Windows 版本中,可能需要赋予 Tomcat 明确的权限,以侦听用于请求的 TCP 端口。

安装后,Tomcat 服务器将运行,同时,Apache 服务监控程序图标将显示在 Windows 任务栏(屏幕底部的长条)的右下角,如图 3 所示:

图 3. 服务监控程序显示 Tomcat 正在运行
服务监控程序显示 Tomcat 正在运行

在图 3 中,监控程序图标上的绿色箭头指示 Tomcat 服务正在运行。

验证服务器操作

访问运行的 Tomcat 服务器和验证安装是否成功很简单。启动浏览器并指向地址 http://localhost:8080/ 即可。

Tomcat 服务器在端口 8080 上侦听。(在安装期间配置该端口。)图 4 展示了 Tomcat 显示的欢迎屏幕:

图 4. Tomcat 的欢迎屏幕
Tomcat 的欢迎屏幕

通过让 Tomcat 服务器与浏览器在同一机器上运行,可以模拟网络环境。图 5 显示了这个回环网络配置:

图 5. 用于单机服务器端开发的回环配置
用于单机服务器端开发的回环配置

在图 5 中,客户机(浏览器)与服务器 (Tomcat) 在同一台机器上运行。客户机与服务器之间的连接以回环模式运行。这是 Web 开发中的一个常用实践,允许使用单机执行服务器端开发。在实际生产中,可以将 URL 的主机名从 localhost 更改为联网的生产 Tomcat 服务器的 IP 地址(如图 5 中的虚线内所显示的)。

Tomcat 上的第一个 JSP 应用程序

JSP 编程简介

JSP 是一种流行的脚本和模板语言,用于创建服务器端 Java 应用程序的表示层。通常,JSP 与动态用户界面一起用于网页。它可以动态地生成 HTML、XML、级联样式表(Cascading Style Sheets,CSS)、JavaScript 以及任何客户端表示内容。最新的广泛实现的 JSP 版本是 2.0,基于 Java 规范要求(Java Specification Request,JSR)152(参见 参考资料)。

JSP 语言的基本元素如下:

  • 指令
  • 标准动作
  • 表达式语言(Expression Language,EL)
  • 自定义标记库
  • JavaBean

JSP 内建了访问 JavaBean 的能力。在生产应用程序中,JavaBean 通常用于在应用程序逻辑(使用 servlet 和其他组件实现)和 JSP 中传递数据值。JSP 代码的主要职责是显示 JavaBean 中包含的值。

JSP 频繁使用的一个标记库是 JSP 标准标记库(JSP Standard Tag Library,JSTL)。JSTL 在 JSR 52 中定义(参见 参考资料),它包含一个大型的标记库,可与 JSP 中的 EL 联合使用。JSTL 的最新版本(到 2005 年 10 月为止)是 1.1。

与 Java 程序不同,JSP 程序不需要进行预编译。Tomcat 在 JSP 首次执行时对它进行编译,并保留一份编译的二进制文件副本,用于后续执行。这能够加快开发和测试周期。

对于早期的 JSP 版本(2.0 之前),如果不求助于嵌入式 Java 编码,则很难编写一般的应用程序逻辑。事实上,2.0 之前的 JSP 版本允许并鼓励使用混合 Java/JSP 编码。该实践通常会创建混乱和难以维护的代码。

从 JSP 2.0 开始,因为支持 EL 和 JSTL,所以 JSP 程序中不再需要 Java 代码。建议所有新的 JSP 开发人员不要将嵌入式 Java 代码与 JSP 混用。此方法通常称为无脚本 JSP

学完本教程后,可以了解更多关于 JSP 编程的内容(参见 参考资料 了解更多关于 JSP 的信息)。本部分其余内容说明如何使用 Tomcat 创建和运行 JSP 应用程序,以便立即开始编写自己的 JSP 程序。

一个简单 JSP 程序

本教程中的 JSP 程序示例演示 JSP 的动态 HTML 生成能力。该程序输出一条包含当前服务器端时间的信息和一个乘法表。每次访问该页时,数据信息都会更改。该乘法表使用一个编程算法生成。

可以在代码分发版的 step1 子目录中找到该 JSP 程序示例(参见 下载)。清单 1 展示了 index.jsp:

清单 1. 示例 JSP 程序:index.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%><jsp:useBean id="timeNow" class="java.util.Date" /><html><head><title>developerWorks Tomcat Tutorial</title><link rel=stylesheet type="text/css" href="specials.css"></head><body><table width="600">  <tr>    <td class="mainHead" colspan="9">      <h1>Today is          <fmt:formatDate value="${timeNow}"         type="date" dateStyle="long" /></h1>    </td>  </tr>  <tr>    <c:forEach var="i" begin="1" end="9" step="1">      <th>${i}x</th>    </c:forEach>  </tr>    <c:forEach var="row" begin="1" end="9" step="1">      <tr>      <c:forEach var="col" begin="1" end="9" step="1">        <td><c:out value="${row * col}" /></td>      </c:forEach>      </tr>    </c:forEach></table></body></html>

该程序中一些要注意的 JSP 技术包括:

  • <%@taglib> 指令,用于包含 JSTL 的核心和格式化组件,并将它们的标记分别与名称空间前缀 c: 和 fmt: 关联。
  • <jsp:useBean> 标准动作将 java.util.Date 类的一个实例实例化为一个 JavaBean,表示当前时间。
  • 在 ${timeNow} 表达式中使用 EL,表示 java.util.Date 的 JavaBean 实例,以输出当前的月和日。
  • 使用 JSTL 中的日期格式化库标记来格式化日期值。
  • 使用 JSTL 中的 <c:forEach> 标记创建输出乘法表的循环。
  • 将静态 HTML 内容与 JSP 生成动态内容混合。

准备在 Tomcat 上运行 JSP 应用程序

完成一些包装工作后,才能在 Tomcat 上运行 index.jsp 程序。通常需要按照以下步骤操作:

  1. 创建 JSP 应用程序。如果只使用一个页面,则称它为 index.jsp,就像在示例程序中所做的 一样。
  2. 创建一个部署描述符(一个 web.xml 文件)并将它放在 WEB-INF 目录中。
  3. 将 JSTL 库复制到 WEB-INF/lib 目录。
  4. 使用 JDK 中的 JAR 工具将所有代码捆绑成一个 Web 应用程序归档 (WAR) 文件,用于部署。
  5. 使用 Tomcat Web 应用程序管理器部署和运行 WAR 文件。

WAR 文件是一个标准 Java 企业版(Java Enterprise Edition,Java EE)部署单元。它是一种格式非常特殊的 JAR 文件,文件扩展名为 .war。 在此 WAR 文件中,必须已经部署了一个名为 web.xml 的部署描述符文件,其中包含一些指令告诉服务器如何部署 WAR 内容。

对于该示例程序,web.xml 文件(参见清单 2)的作用不大,因为应用程序只包含一个 JSP 页面:

清单 2. web.xml 部署描述符
<?xml version="1.0" encoding="ISO-8859-1"?><web-app xmlns="http://java.sun.com/xml/ns/j2ee"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"    version="2.4">    <description>   developerWorks Beginning Tomcat Tutorial    </description>    <display-name>IBM developerWorks Beginning Tomcat Tutorial    </display-name></web-app>

清单 2 中的 web.xml 文件只为 Tomcat 提供描述和显示名称,这些内容稍后将被 Tomcat 应用程序管理器使用。

要创建 WAR 文件,运行代码分发版(参见下载)中的 makewar.bat 文件。该批处理文件仅使用 JAR 实用工具来创建 JAR 文件。清单 3 显示了 makewar.bat 的内容:

清单 3. makewar.bat
jar cvf step1.war .

如果是在 Linux 系统上操作,可以在控制台上输入 jar cvf step1.war . 来手动创建 WAR 文件。

使用 Tomact Web 应用程序管理器部署应用程序

要在 Tomcat 上运行应用程序,首先需要部署 WAR 文件。使用 Tomcat Web 应用程序管理器(简称为管理器)实用工具完成该操作。转到 http://localhost:8080/manager/html 可以访问管理器。

Tomcat 会询问用户名和密码。输入在设置期间提供的管理员用户名和密码。登录服务器后,将看到管理器的显示。它显示了当前加载的并在 Tomcat 服务器上运行的所有应用程序,如图 6 所示:

图 6. Tomcat Web 应用程序管理器实用工具
Tomcat Web 应用程序管理器实用工具

要部署 step1.war,向下滚动到管理器页面底部。单击 WAR file to deploy 区域旁的 Browse 按钮。使用浏览器选择 step1.war 文件,然后单击 Deploy 按钮。该动作将 WAR 文件发送到 Tomcat 服务器并启动它。

现在应该能看到 step1.war 应用程序在管理器的运行应用程序列表中运行。另请注意,该列表使用在 web.xml 中设置的显示名称来标识应用程序。

最后,转到 http://localhost:8080/step1/ 可以看到 JSP 应用程序正在运行。

默认情况下,Tomcat 使用 WAR 文件的名称为应用程序提供上下文。该上下文被指定为用于访问应用程序的地址的一部分。在本例中, 上下文是 step1。Tomcat 在应用程序的根目录中查找名为 index.jsp 的文件并执行(如果存在)。图 7 显示了运行的 JSP 应用程序:

图 7. 在 Tomcat 上运行的一个 JSP 应用程序
在 Tomcat 上运行的一个 JSP 应用程序

如果对 JSP 代码进行任何修改(比如在学习 JSP 时),可以执行以下步骤来运行新代码:

  1. 使用 makewar 创建新的 WAR 文件。
  2. 单击管理器中的 undeploy 取消旧 step1.WAR 的部署。
  3. 使用管理器部署新的 WAR 文件。

使用 Tomcat 进行 servlet 部署

servlet 简介

Servlet 是服务器端 Java 代码,在 servlet 容器(如 Tomcat 服务器)的控制下执行。Servlet 与 JSP 一样,接受传入请求,进行处理或转换,然后生成传出响应。因为 servlet 实际上是 Java 代码,所以可以任意使用 Java 编程语言的功能和灵活性来创建服务器端逻辑。

所有 servlet 都会直接或间接通过实现 javax.servlet.Servlet 接口的辅助类来实现该接口。Servlet 还可以使用容器提供的 API 公开容器服务。例如,servlet 可以从容器获得数据库连接来访问关系数据库。

Servlet 通常用于实现 Web 应用程序逻辑。Servlet 可以获取和处理数据,然后将数据传递给 JSP 进行显示(例如,动态生成用户界面)。Servlet 还可用于频繁地处理通过基于 Web 形式提交的数据。

Tomcat 5.5 实现 Servlet 2.4 —— JSR 154 中指定的最新完成的 servlet 标准(参见 参考资料)。

本节说明如何使用 Tomcat 学习 servlet 编程。参见 参考资料 了解关于 servlet 的更多信息。

生成菜单特价项的 Servlet 示例

本节中的示例显示某虚构餐馆当日的特价菜单项。Servlet 负责获取数据,JSP 负责动态生成 HTML 来显示事项。本例中的 JSP 称为 showspecials.jsp,Servlet 位于 com.ibm.dw.tutorial.tomcat.SpecialsServlet Java 类中。

本例演示 Web 应用程序中的一种典型模式:

  1. 一个 servlet 接受来自用户的传入请求。
  2. 该 servlet 根据传入请求进行处理。
  3. 该 servlet 使用作为属性附加的数据将请求分派给一个 JSP。
  4. JSP 生成一个动态响应来显示数据。

清单 4 显示 SpecialsServlet 的代码:

清单 4. SpecialsServlet servlet
package com.ibm.dw.tutorial.tomcat;import javax.servlet.*;import javax.servlet.http.*;import java.util.*;import java.io.IOException;public class SpecialsServlet extends HttpServlet {    protected void doGet(HttpServletRequest request,         HttpServletResponse response)        throws ServletException, IOException {         ServletContext context = getServletContext();         request.setAttribute("specials", getSpecials());        context.getRequestDispatcher("/showspecials.jsp")            .forward(request, response);        }       protected void doPost(HttpServletRequest request,        HttpServletResponse response)              throws ServletException, IOException {              doGet(request, response);        }        private List getSpecials() {         List retval = new Vector();         retval.add(new Special("Coq au Vin", 15));         retval.add(new Special("Pad Thai", 10));         retval.add(new Special("Lobster Thermador", 10));         retval.add(new Special("Baked Alaska", 8));         return retval;    }    public class Special {         int price;         String menuItem;         public Special(String item, int inPrice)  {              menuItem = item;              price = inPrice;         }         public int getPrice() {              return price;         }         public String getMenuItem() {              return menuItem;         }          }    }

清单 4 中的 servlet 代码:

  1. 声明一个名为 Special 的内部类来保存菜单特价项。
  2. 在名为 getSpecials() 的方法中创建特价项项列表。
  3. 在 doGet() 方法(Tomcat 调用该方法来处理传入 HTTP 请求)中,将特价项列表作为名为 specials 的属性附加到 request
  4. 将请求转发给 showspecials.jsp 进行显示。

显示当日特价项的 JSP

清单 5 显示 showspecials.jsp 代码:

清单 5. showspecials.jsp 代码
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%><jsp:useBean id="timeNow" class="java.util.Date" /><%@ page session="true"%><html><head><title>developerWorks Tomcat Tutorial</title><link rel=stylesheet type="text/css" href="specials.css"></head><body><table width="600">     <tr>          <td class="mainHead" colspan="2">          Today's specials for           <fmt:formatDate value="${timeNow}" type="date"               dateStyle="long" />          </td>     </tr>     <tr>      <th>Specialty</th>      <th>Price</th>     </tr>     <c:forEach var="special" items="${specials}">          <tr>            <td>${special.menuItem}</td>            <td>\$${special.price}</td>          </tr>     </c:forEach></table></body></html>

可以发现,这里使用了前一节(Tomcat 上的第一个 JSP 应用程序)中 step1 示例使用 清单 1 的技术:

  • JSTL 日期格式化标记格式化一个 java.util.Date JavaBean 实例。
  • <c:forEach> JSTL 循环标记迭代 specials 的 List 属性(通过 SpecialServlet 附加到 request 对象)。
  • EL 表达式显示特价项的值。

将 servlet 部署到 Tomcat

Servlet 存在于 Web 应用程序中,方式与 JSP 的相同。在将 JSP 和 servlet 部署到 Tomcat 之前,需要将应用程序包装成一个 WAR 文件。将应用程序中 servlet 的类放在 WEB-INF/类子目录下。

本例的 Web 描述符与前一节(Tomcat 上的第一个 JSP 应用程序)中的 清单 2 中的 Web 描述符稍有不同。前者必须告诉 Tomcat 有关 servlet 的信息以及如何将它映射到传入请求。清单 6 显示了本例的 web.xml 文件:

清单 6. web.xml 文件
<?xml version="1.0" encoding="ISO-8859-1"?><web-app xmlns="http://java.sun.com/xml/ns/j2ee"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee       http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"     version="2.4">     <description>developerWorks Beginning Tomcat Tutorial      </description>     <display-name>          IBM developerWorks Beginning Tomcat Tutorial Step 2     </display-name>     <servlet>          <servlet-name>Specials</servlet-name>          <servlet-class>               com.ibm.dw.tutorial.tomcat.SpecialsServlet          </servlet-class>     </servlet>     <servlet-mapping>          <servlet-name>Specials</servlet-name>          <url-pattern>/showspecials.cgi</url-pattern>     </servlet-mapping></web-app>

该部署描述符包含一个 <servlet> 元素,告诉 Tomcat 有关 servlet 实现类的信息。<servlet-mapping> 元素告诉服务器应该将发往 showspecials.cgi 的请求传递给 servlet。

编译 servlet 后,才能创建 WAR 文件。因为 servlet 是 Java 代码,所以在部署 servlet 之前必须对它们进行编译。为此,可以使用代码分发版(参见 下载)中的 compile.bat 批处理文件。但是,需要修改它来指向您自己的 Tomcat 目录,因为来自 Tomcat common/lib 目录的 servlet-api.jar 包含编译 servlet 所需的一些接口和辅助类。

成功编辑应用程序后,即可通过执行 makewar.bat 来制作 WAR 文件。这次,WAR 文件称为 step2.war。创建 WAR 文件后,可以使用 Tomcat 管理器来部署它。

可以通过转到 http://localhost:8080/step2/showspecials.cgi 来访问 Web 应用程序。

Tomcat 现在将 showspecials.cgi 的 URL 路径映射到 servlet 代码。这通过 <servlet-mapping> 元素在 web.xml 中指定。图 8 显示了应用程序的输出,即显示菜单特价项:

图 8. 在 Tomcat 上运行的 SpecialsServlet
在 Tomcat 上运行的 SpecialsServlet

如果对 servlet 代码进行更改。可以按照以下步骤运行新的代码:

  1. 使用 compile.bat 重新编译代码
  2. 使用 makewar.bat 创建 step2.war
  3. 使用 Tomcat 管理器取消所有旧 step2.war 的部署。
  4. 部署新的 step2.war。

使用 Tomcat 探索 Web 服务开发

Web 服务简介

Web 服务是服务器端代码组件,可以公开其功能以使用标准 HTTP 协议访问 TCP/IP 网络。这种公开允许 Web 服务用户(称为消费者)使用大多数网络连接上的 Web 服务 -- 甚至能穿越防火墙。

Web 服务处理传入请求并生成响应。这正是 servlet 所做的工作,所以自然要使用 servlet 实现 Web 服务。

Web 服务正日益流行,因为它们可以有效地用于 B2B 或 B2C 接口。它们允许通过 Internet 发送请求和接收响应。任何能访问网站的用户都可以访问 Web 服务。例如,eBay 和 Amazon.com 都为其合作伙伴和用户提供了 Web 服务。

Web 服务依赖于在消费者和服务之间传递基于 XML 的消息。此消息被包装并按照简单对象访问协议(Simple Object Access Protocol,SOAP)发送。

Apache Axis 是一个 Web 服务开发工具包,可用作 Tomcat 的一个增件。下一节说明如何使用 Apache Axis 创建简单 Web 服务,并在 Tomcat 服务器上部署它。参见 参考资料 中有助于了解 Web 服务编程更多信息的文章和教程。

向 Tomcat 添加 Axis

Axis 可以作为一个 servlet 在 Tomcat 上运行。如果还未这样做,请下载最新版本的 Axis(参见 必要条件)。取消 Axis 分发版的部署。

将 Axis 分发版 webapps/axis 目录下的所有文件复制到本文章代码分发版的 step3/axis 目录(参见 下载)。

可以使用 makewar.bat 批处理文件(位于 step3/axis 目录中)来创建可以作为 Web 应用程序部署到 Tomcat 的 axis.war 文件。

在 Internet 上下载一些附加 JAR 文件并将它们放入 step3 应用程序的 WEB-INF/lib 目录后,才能在 Tomcat 上正确运行 Axis。如果使用 Axis 1.2.1,则需要下载以下文件:

  • activation.jar 来自 http://java.sun.com/products/javabeans/glasgow/jaf.html
  • xmlsec-1.2.1.jar 来自 http://xml.apache.org/security/
  • mail.jar 来自 http://java.sun.com/products/javamail/

如果使用的不是 Axis 1.2.1 版,则上述列表会稍有不同。参见 Axis 分发版的随附文档了解更多信息。

一个发布当日事项的简单 Web 服务

继续餐馆菜单特价项示例,在本节示例中创建的 Web 服务为消费者提供当日特价销售的菜单项列表。

Web 服务的代码包含在 ShowSpecials.jws 文件中,如清单 7 所示:

清单 7. ShowSpecials.jws
public class ShowSpecials {    public String [] getMenuItems() {         return new String []{         "Coq au Vin", "Pad Thai",         "Lobster Thermador", "Baked Alaska" };      }    public int  [] getPrices() {     return  new int [] { 15, 10, 10, 8 };    }        }

清单 7 中的代码有两个公共方法。getMenuItems() 方法返回特价出售的菜单项,getPrices() 方法检索它们的价格。

Axis 使用 .jws(Java Web 服务)文件支持立即部署模式。在该模式下,所需做的只是将带有 .jws 扩展名的 Java 源文件放入 axis 目录。此类的任何公共方法都将通过 Web 服务公开。本例使用立即部署模式。getMenuItems() 和 getPrices() 方法在解析和编译 .jws 文件时通过 Axis 动态公开为 Web 服务方法。解析和编译只在初次访问 Web 服务时发生。

将 Axis servlet 部署到 Tomcat

因为 Axis 作为 servlet 运行,所以将 Web 服务部署到 Tomcat的过程与之前 Web 应用程序部署示例中的相同。

在 step3/axis 目录中,运行 run makewar.bat 来创建 axis.jar。使用 Tomcat 管理器将 axis.war 文件部署到 Tomcat 服务器。

部署后,即可在 URL http://localhost:8080/axis/ShowSpecials.jws 上获得 Web 服务。不过,如果使用浏览器访问该 URL,将看不到很多信息。必须通过 Web 服务使用者应用程序来使用 Web 服务。

使用使用者应用程序测试 Web 服务

编写 Web 服务使用者应用程序代码超出了本教程的范围。不过,可以使用 Apache Ant(参见 必要条件)根据代码分发版(参见 下载)中提供的现有代码编译客户机应用程序。

因为 Web 服务通过标准 Web 协议传输,所以客户机应用程序可以在任何使用编程语言的操作系统上创建。许多 Web 服务工具箱自动生成 Web 服务使用程序。这是可能的,因为工具可以自动发现访问 Web 服务的方法。

有关如何访问 Web 服务的说明以 XML 文件的形式提供,用 Web 服务定义语言(Web Service Definition Language,WSDL)进行编码。大多数 Web 服务容器(如 Axis)可以根据某个 Java 类或接口来生成这个 WSDL 文件。例如,要查看为 Web 服务生成的 WSDL,可以试着使用浏览器访问 URL http://localhost:8080/axis/ShowSpecials.jws?wsdl。看似复杂的 XML 文档是使用 .jws 代码生成的。此 WSDL 文档可由某个工具轻松处理,而且能自动生成调用 Web 服务的客户机使用程序。

本例的客户机不使用 WSDL 或自动代码生成。它通过硬编码来调用 ShowSpecials.jws Web 服务。这使编写客户机代码相对简单。

需要安装 Ant 来编译和运行 Web 服务客户机(参见 必要条件),因为代码需要许多 Axis 库 JAR 文件才能成功编译和运行。

编译客户机之前,请找到 step3/client 目录中的 build.xml 文件,并编辑该文件,以指向 Axis 安装目录。

要编译客户机,请使用以下命令运行 Ant:

ant compile

要启动客户机,使用 Web 服务,请使用以下命令运行 Ant:

ant run

成功运行 Web 服务客户机的典型输出(显示特价菜单项和价格)如下所示:

Buildfile: build.xmlrun:     [java] Specials today:     [java]     Coq au Vin                        $15     [java]     Pad Thai                          $10     [java]     Lobster Thermador                 $10     [java]     Baked Alaska                      $8BUILD SUCCESSFULTotal time: 7 seconds

结束语

Tomcat 服务器是一个学习 JSP、servlet 和 Web 服务的的优秀平台。在本教程中学习了以下方法:

  • 下载和安装您自己的 Tomcat 服务器。
  • 使用 Tomcat 管理器部署和取消部署应用程序。
  • 创建基于 JSP 的应用程序并在 Tomcat 服务器上执行它。
  • 使用 servlet 创建 Web 应用程序并在 Tomcat 上执行它。
  • 创建 Web 服务并在 Tomcat 上执行它。

现在,您已经能教熟练地使用这些工具来开发和运行 Web 应用程序,即可进一步研究 JSP、servlet 和 Web 服务编程了。



0 0
原创粉丝点击