How tomcat works——12 StandardContext

概述

如前面章节看到,一个上下文容器(Context)代表着一个 web 应用,每一个上下文包括一个或多个包装器(Wrapper),每个包装器代表一个 Servlet。然而,上下文还需要其它的一些组件,如加载器和管理器。本章介绍 Catalina 中Context 接口的标准实现类org.apache.catalina.core.StandardContext 。

我们首先介绍 StandardContext 对象的实例化和配置,然后讨论跟其相关的StandardContextMapper(Tomcat 4)和 ContextConfig 类。接下来看,当容器接受到 HTTP 请求时的方法调用顺序。然后,在讨论该类几点重要属性,最后一节讨论 Tomcat5 中的 backgroundProcess()方法。

注意:本章没有相关配套的应用Demo,StandardContext 类已经在第 11 章中用过了。

12.1 StandardContext配置

当一个 StandardContext 实例构造之后,必须调用它的 start()方法,确保这个实例能为收到的 HTTP 请求服务了。由于种种原因,一个 StandardContext 对象可能启动失败,这时候属性 available 被设置为 false。属性 available 表示着 StandardContext 对象的可用性。

为了start()方法能成功启动,StandardContext 对象必须进行适当地属性配置。在一个Tomcat 部署中,StandardContext 的配置过程做了以下事情:读取和解析%CATALINA_HOME%/conf 目录下面的默认web.xml文件,部署所有应用程序。确保StandardContext 实例可以处理应用级别的 web.xml。另外,配置需要添加一个验证器阀门和证书阀门(authenticator和certificate valve)。

注意:有关StandardContext 配置的更多细节将在 15 章中讨论。

StandardContext 的属性之一configured用来表示该StandardContext 是否已经适当配置了。StandardContext 使用一个事件监听器来作为它的配置器。当 StandardContext 实例的start() 方法被调用时,会触发一个生命周期事件。该事件会唤醒一个监听器来配置该 StandardContext 实例。配置成功后,该监听器将configured属性设置为 true。否则,StandardContext对象拒绝启动,这样就不能对 HTTP 请求进行服务。

在第 11 章中已经看到一个生命周期监听器被添加到 StandardContext 实例上。它是 ch11.pyrmont.core.SimpleContextConfig类,它仅仅将StandardContext实例的configured属性直接设置为true,这样就认为配置完成了。在一个 Tomcat 部署中,配置 StandardContext 的生命周期监听器类型为org.apache.catalina.startup.ContextConfig,具体内容将在第 15 章中进行介绍。

现在你应该已经明白了配置对于 StandardContext 的重要性,接下来看更多StandardContext 类的细节,首先是它的构造函数。

12.1.1 StandardContext的构造函数

StandardContext的构造函数如下:

public StandardContext() {
    super();
    pipeline.setBasic(new StandardContextValve());
    namingResources.setContainer(this);
}

在构造函数中,最重要的事情是在 StandardContext 的管道上添加了一个类型为 org.apache.catalina.core.StandardContextValve 的基础阀门,该阀门用于处理每个从连接器中获得的HTTP 请求。

12.1.2 启动StandardContext

start()方法初始化 StandardContext 对象并让生命周期监听器配置该StandardContext 实例。如果配置成功,生命周期监听器会将 configured 属性设置为 true。最后 start()方法,将 available 属性设置为 true 或 false。如果是 true 的话表示该 StandardContext 属性配置完毕并且所有相关子容器和组件已经成功启动,这样就能对 HTTP 请求进行服务,如果是 false 则表示出现了错误。

StandardContext 类将 configured 的值初始化为 false,如果生命周期监听器成功完成配置,则将该值改为 true。在 start ()方法的最后,会检查StandardContext 对象的 configured 属性,如果该值为 true,则表示成功启动该StandardContext实例,否则调用stop()方法停止所有已经启动的相关组件。

Tomcat 4 中 StandardContext 类中的 start()方法如 Listing12.1:

Listing 12.1: The start method of the StandardContext class in Tomcat 4

public synchronized void start() throws LifecycleException {
        if (started)
            throw new LifecycleException
                (sm.getString("containerBase.alreadyStarted", logName()));

        if (debug >= 1)
            log("Starting");

        // Notify our interested LifecycleListeners
        lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);

        if (debug >= 1)
            log("Processing start(), current available=" + getAvailable());
        setAvailable(false);
        setConfigured(false);
        boolean ok = true;

        // Add missing components as necessary
        if (getResources() == null) {   // (1) Required by Loader
            if (debug >= 1)
                log("Configuring default Resources");
            try {
                if ((docBase != null) && (docBase.endsWith(".war")))
                    setResources(new WARDirContext());
                else
                    setResources(new FileDirContext());
            } catch (IllegalArgumentException e) {
                log("Error initializing resources: " + e.getMessage());
                ok = false;
            }
        }
        if (ok && (resources instanceof ProxyDirContext)) {
            DirContext dirContext =
                ((ProxyDirContext) resources).getDirContext();
            if ((dirContext != null)
                && (dirContext instanceof BaseDirContext)) {
                ((BaseDirContext) dirContext).setDocBase(getBasePath());
                ((BaseDirContext) dirContext).allocate();
            }
        }
        if (getLoader() == null) {      // (2) Required by Manager
            if (getPrivileged()) {
                if (debug >= 1)
                    log("Configuring privileged default Loader");
                setLoader(new WebappLoader(this.getClass().getClassLoader()));
            } else {
                if (debug >= 1)
                    log("Configuring non-privileged default Loader");
                setLoader(new WebappLoader(getParentClassLoader()));
            }
        }
        if (getManager() == null) {     // (3) After prerequisites
            if (debug >= 1)
                log("Configuring default Manager");
            setManager(new StandardManager());
        }

        // Initialize character set mapper
        getCharsetMapper();

        // Post work directory
        postWorkDirectory();

        // Reading the "catalina.useNaming" environment variable
        String useNamingProperty = System.getProperty("catalina.useNaming");
        if ((useNamingProperty != null)
            && (useNamingProperty.equals("false"))) {
            useNaming = false;
        }

        if (ok && isUseNaming()) {
            if (namingContextListener == null) {
                namingContextListener = new NamingContextListener();
                namingContextListener.setDebug(getDebug());
                namingContextListener.setName(getNamingContextName());
                addLifecycleListener(namingContextListener);
            }
        }

        // Binding thread
        ClassLoader oldCCL = bindThread();

        // Standard container startup
        if (debug >= 1)
            log("Processing standard container startup");

        if (ok) {

            try {

                addDefaultMapper(this.mapperClass);
                started = true;

                // Start our subordinate components, if any
                if ((loader != null) && (loader instanceof Lifecycle))
                    ((Lifecycle) loader).start();
                if ((logger != null) && (logger instanceof Lifecycle))
                    ((Lifecycle) logger).start();

                // Unbinding thread
                unbindThread(oldCCL);

                // Binding thread
                oldCCL = bindThread();

                if ((cluster != null) && (cluster instanceof Lifecycle))
                    ((Lifecycle) cluster).start();
                if ((realm != null) && (realm instanceof Lifecycle))
                    ((Lifecycle) realm).start();
                if ((resources != null) && (resources instanceof Lifecycle))
                    ((Lifecycle) resources).start();

                // Start our Mappers, if any
                Mapper mappers[] = findMappers();
                for (int i = 0; i < mappers.length; i++) {
                    if (mappers[i] instanceof Lifecycle)
                        ((Lifecycle) mappers[i]).start();
                }

                // Start our child containers, if any
                Container children[] = findChildren();
                for (int i = 0; i < children.length; i++) {
                    if (children[i] instanceof Lifecycle)
                        ((Lifecycle) children[i]).start();
                }

                // Start the Valves in our pipeline (including the basic),
                // if any
                if (pipeline instanceof Lifecycle)
                    ((Lifecycle) pipeline).start();

                // Notify our interested LifecycleListeners
                lifecycle.fireLifecycleEvent(START_EVENT, null);

                if ((manager != null) && (manager instanceof Lifecycle))
                    ((Lifecycle) manager).start();

            } finally {
                // Unbinding thread
                unbindThread(oldCCL);
            }

        }
        if (!getConfigured())
            ok = false;

        // We put the resources into the servlet context
        if (ok)
            getServletContext().setAttribute
                (Globals.RESOURCES_ATTR, getResources());

        // Binding thread
        oldCCL = bindThread();

        // Create context attributes that will be required
        if (ok) {
            if (debug >= 1)
                log("Posting standard context attributes");
            postWelcomeFiles();
        }

        // Configure and call application event listeners and filters
        if (ok) {
            if (!listenerStart())
                ok = false;
        }
        if (ok) {
            if (!filterStart())
                ok = false;
        }

        // Load and initialize all "load on startup" servlets
        if (ok)
            loadOnStartup(findChildren());

        // Unbinding thread
        unbindThread(oldCCL);

        // Set available status depending upon startup success
        if (ok) {
            if (debug >= 1)
                log("Starting completed");
            setAvailable(true);
        } else {
            log(sm.getString("standardContext.startFailed"));
            try {
                stop();
            } catch (Throwable t) {
                log(sm.getString("standardContext.startCleanup"), t);
            }
            setAvailable(false);
        }

        // Notify our interested LifecycleListeners
        lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);

    }

注意:在 Tomcat5 中,start()方法跟 Tomcat4 的相似,只是包括了 JMX 相关的代码,
JMX 的内容将在第 20 章讨论。

如在 Listing12.1 中看到的,下面是start()方法做的事情:
》触发 BEFORE_START 事件
》设置 availability 属性为 false
》设置 configured 属性为 false
》设置源(resources)
》设置加载器(loader)
》设置管理器(manager)
》初始化属性 map
》启动跟该上下文相关的组件
》启动子容器(包装器wrapper)
》启动管道(pipeline)
》启动管理器(manager)
》触发 START 事件。监听器(ContextConfig)会进行一系列配置操作,配置成功后,将 StandardContext 实例的 configured 属性设置为 true(第15章详细讨论)
》检查 configured 属性值,如果为 true:调用postWelcomPages()方法,加载子包装器,并将available属性值设为true。如果 configured 值为 false则调用 stop()方法
》触发 AFTER_START 事件

12.1.3 invoke()方法

在 Tomcat4 中,StandardContext的invoke()方法由相关联的连接器调用,如果该上下文是一个主机(host)的子容器,则有该主机的 invoke()方法调用。StandardContext的 invoke()方法首先检查是否正在重加载该应用程序,如果是的话,则等待直到加载完毕。然后调用它的父类 ContainerBase 的 invoke() 方法。Listing12.2 展示了StandardContext 的 invoke() 方法。

Listing 12.2: The invoke method of the StandardContext Class

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

        // Wait if we are reloading
        while (getPaused()) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                ;
            }
        }

        // Normal request processing
        if (swallowOutput) {
            try {
                SystemLogHandler.startCapture();
                super.invoke(request, response);
            } finally {
                String log = SystemLogHandler.stopCapture();
                if (log != null && log.length() > 0) {
                    log(log);
                }
            }
        } else {
            super.invoke(request, response);
        }

    }

getPaused()方法返回 paused 的值,当应用程序正在加载时该属性值为ture。应用程序重载将在下一节讨论。

在 Tomcat5 中,StandardContext 并没有提供 invoke()方法的实现,因此它会执行 其父类ContainerBase 的 invoke()方法。检查应用程序加载的任务放在了StandardContextValve 类的 invoke()方法中完成。

12.2 StandardContextMapper类

对于每一个请求,invoke()方法都会调用 StandarContext 管道中基础阀门的invoke()方法。StandarContext 的基础阀门用org.apache.catalina.core.StandardContextValve 类表示。
StandardContextValve 的 invoke()方法首先需要得到一个处理请求的包装器。

在 Tomcat4 中,StandardContextValve 实例查找包含它的 StandardContext。StandardContextValve 使用上下文容器的 mapper来查找合适的包装器。一旦它得到一个合适的包装器,则调用该包装器的 invoke()方法。在介绍StandardContextValve 之前,本节先介绍相关的mapper组件。

StandardContext 的父类 ContainerBase 定义了 addDefaultMapper ()方法来添加一个默认的映射(map)如下:

protected void addDefaultMapper(String mapperClass) {

        // Do we need a default Mapper?
        if (mapperClass == null)
            return;
        if (mappers.size() >= 1)
            return;

        // Instantiate and add a default Mapper
        try {
            Class clazz = Class.forName(mapperClass);
            Mapper mapper = (Mapper) clazz.newInstance();
            mapper.setProtocol("http");
            addMapper(mapper);
        } catch (Exception e) {
            log(sm.getString("containerBase.addDefaultMapper", mapperClass),
                e);
        }

    }

StandardContext 在 start()方法中调用addDefaultMapper()方法,传递一个mapperClass 变量:

public synchronized void start() throws LifecycleException {
    ...
    if (ok) {
    try {
        addDefaultMapper(this.mapperClass);
    ...
}

StandardContext 的 mapperClass 变量定义如下:

private String mapperClass = "org.apache.catalina.core.StandardContextMapper";

我们必须使用映射器的 setContainer() 方法来将映射器跟容器相关联。org.apache.catalina.core.StandardContextMapper是org.apache.catalina.Mapper 接口的标准实现。StandardContextMapper只能跟上下文相关联,从 setContainer()方法可见:

public void setContainer(Container container) {
    if (!(container instanceof StandardContext))
        throw new IllegalArgumentException(sm.getstring("httpContextMapper.container"));
    context = (StandardContext) container;
}

在一个映射器(mapper)中最重要的方法是 map()方法,它返回一个子容器来处理HTTP 请求,该方法的签名如下:

public Container map(Request request, boolean update)

在 StandardContextMapper 中 map()方法返回一个包装器来处理请求。如果找不到合适的包装器,方法返回 null。

回到本节开始讨论的内容,StandardContextValve 调用上下文容器的 map()方法来处理 HTTP 请求,传递的参数是 org.apache.catalina.Request 对象。map()方法(ContainerBase 类中)通过调用findMapper()方法来获得返回一个对应相应协议的映射器,然后调用该映射器的 map()方法。

// Select the Mapper we will use
Mapper mapper = findMapper(request.getRequest().getProtocol());
if (mapper == null)
    return (null);
// Use this Mapper to perform this mapping
return (mapper.map(request, update));

StandardContextMapper 的 map()方法首先识别上下文的相关 URL 映射:

// 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 = null;

        // Rule 1 -- Exact Match
        if (wrapper == null) {
            if (debug >= 2)
                context.log("  Trying exact match");
            if (!(relativeURI.equals("/")))
                name = context.findServletMapping(relativeURI);
            if (name != null)
                wrapper = (Wrapper) context.findChild(name);
            if (wrapper != null) {
                servletPath = relativeURI;
                pathInfo = null;
            }
        }

        // Rule 2 -- Prefix Match
        if (wrapper == null) {
            if (debug >= 2)
                context.log("  Trying prefix match");
            servletPath = relativeURI;
            while (true) {
                name = context.findServletMapping(servletPath + "/*");
                if (name != null)
                    wrapper = (Wrapper) context.findChild(name);
                if (wrapper != null) {
                    pathInfo = relativeURI.substring(servletPath.length());
                    if (pathInfo.length() == 0)
                        pathInfo = null;
                    break;
                }
                int slash = servletPath.lastIndexOf('/');
                if (slash < 0)
                    break;
                servletPath = servletPath.substring(0, slash);
            }
        }

        // Rule 3 -- Extension Match
        if (wrapper == null) {
            if (debug >= 2)
                context.log("  Trying extension match");
            int slash = relativeURI.lastIndexOf('/');
            if (slash >= 0) {
                String last = relativeURI.substring(slash);
                int period = last.lastIndexOf('.');
                if (period >= 0) {
                    String pattern = "*" + last.substring(period);
                    name = context.findServletMapping(pattern);
                    if (name != null)
                        wrapper = (Wrapper) context.findChild(name);
                    if (wrapper != null) {
                        servletPath = relativeURI;
                        pathInfo = null;
                    }
                }
            }
        }

        // Rule 4 -- Default Match
        if (wrapper == null) {
            if (debug >= 2)
                context.log("  Trying default match");
            name = context.findServletMapping("/");
            if (name != null)
                wrapper = (Wrapper) context.findChild(name);
            if (wrapper != null) {
                servletPath = relativeURI;
                pathInfo = null;
            }
        }

你可能会问,上下文如何有像servlet映射这样信息的?回头看第11章中的Bootstrap 类,它添加了2个 Servlet 映射到 StandarContext:

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

它还将包装器作为子容器添加到上下文容器中:

context.addChild(wrapper1);
context.addChild(wrapper2);

Tomcat 5 删除了 Mapper 接口及其相关类。实际上 StandardContextValve 的invoke()方法从请求对象中就能获取到合适的包装器:

Wrapper wrapper = request.getWrapper();

这表示映射信息已经封装在请求对象中了。

12.3 重载支持

StandardContext 定义了 reloadable 属性来标识是否支持应用程序的重加载。允许重加载时,当 web.xml 或 WEB-INF/classes 目录下的文件被改变的时候就会重加载。

StandardContext 用它的加载器来加载应用程序。在Tomcat4 中,StandardContext 中 Loader 接口的标准实现 WebappLoader 类,有一单独线程来检查WEB-INF目录下面所有类和JAR文件的时间戳。我们需要做的是启动该线程,并通过setContainer()方法将 WebappLoader与StandardContext关联。下面是 Tomcat4 中 WebappLoader中setContainer()方法的实现:

public void setContainer(Container container) {

        // Deregister from the old Container (if any)
        if ((this.container != null) && (this.container instanceof Context))
            ((Context) this.container).removePropertyChangeListener(this);

        // Process this property change
        Container oldContainer = this.container;
        this.container = container;
        support.firePropertyChange("container", oldContainer, this.container);

        // Register with the new Container (if any)
        if ((this.container != null) && (this.container instanceof Context)) {
            setReloadable( ((Context) this.container).getReloadable() );
            ((Context) this.container).addPropertyChangeListener(this);
        }

    }

注意最后一个 if 语句块中,如果容器是一个上下文容器,则调用 setReloadable()方法。这也就是说 WebappLoader 的 reloadable 属性跟 StandardContext 的reloadable 属性相同。

下面是 WebappLoader 中setReload()方法的实现:

public void setReloadable(boolean reloadable) {

        // Process this property change
        boolean oldReloadable = this.reloadable;
        this.reloadable = reloadable;
        support.firePropertyChange("reloadable",
                                   new Boolean(oldReloadable),
                                   new Boolean(this.reloadable));

        // Start or stop our background thread if required
        if (!started)
            return;
        if (!oldReloadable && this.reloadable)
            threadStart();
        else if (oldReloadable && !this.reloadable)
            threadStop();

    }

如果将 reloadable 属性设置为 true,则调用 threadStart()方法。如果从 true 到false,则调用 threadStop()方法。threadStart()方法启动一个线程用来持续地检查WEB-INF 目录下面的类文件和 JAR 文件的时间戳。threadStop()方法用于停止该线程。

在 Tomcat5 中,检测类的时间戳交由 backgroundProcess()方法调用完成,下一节将会介绍该方法。

12.4 backgroundProcess()方法

一个上下文容器需要其它组件如加载器和管理器的支持。这些组件通常需要一个单独的线程来在后台进行任务处理。例如,加载器通过一个线程检查类文件和 JAR 文件的时间戳来支持自动重载;管理器需要一个线程来检查它管理的Session对象过期时间。在Tomcat4中,这些组件都有自己的线程。

为了节省资源,Tomcat 5使用了一种不同的方式来处理。所有的后台过程都共享同一个线程。如果一个组件或者是容器需要定期的来执行操作,它需要做的是将这些代码写入到 backgroundProcess ()方法中即可。

共享线程在 ContainerBase 对象创建,ContainerBase 在它的 start()方法中调用threadStard() 方法。

protected void threadStart() {
    if (thread != null)
        return;
    if (backgroundProcessorDelay <= 0)
        return;
    threadDone = false;
    String threadName = "ContainerBackgroundProcessor[" + toString() +"]";
    thread = new Thread(new ContainerBackgroundProcessor(), threadName);
    thread.setDaemon(true);
    thread.start();
}

方法 threadStart()传递一个 ContainerBackgroundProcessor 对象创建一个新线程。ContainerBackgroundProcessor 类实现了 java.lang.Runnable 接口,该类如Listing12.3 所示:

Listing 12.3: The ContainerBackgroundProcessor class

protected class ContainerBackgroundProcessor implements Runnable {
    public void run() {
        while (!threadDone) {
            try {
                Thread.sleep(backgroundProcessorDelay * 1000L);
            }catch (InterruptedException e) {
                ;
            }
            if (!threadDone) {
                Container parent = (Container) getMappingObject();
                ClassLoader cl =
                Thread.currentThread().getContextClassLoader();
                if (parent.getLoader() != null) {
                    cl = parent.getLoader().getClassLoader();
                }
                processChildren(parent, cl);
            }
        }
    }
protected void processChildren(Container container, ClassLoader cl) {
        try {
            if (container.getLoader() != null) {
                Thread.currentThread().setContextClassLoader(container.getLoader().getClassLoader());
            }
            container.backgroundProcess();
        }catch (Throwable t) {
            log.error("Exception invoking periodic operation: ", t);
        }finally {
            Thread.currentThread().setContextClassLoader(cl);
        }
        Container[] children = container.findChildren();
        for (int i = 0; i < children.length; i++) {
            if (children[i].getBackgroundProcessorDelay() <= 0) {
                processChildren(children[i], cl);
            }
        }
    }
}

ContainerBackgroundProcessor 是 ContainerBase 的内部类,在它的 run()方法里,有一个 while 循环定期调用它的 processChildren ()方法。processChildren()调用 backgroundProcess() 来处理它的每个孩子的 processChildren()方法。通过实现backgroundProcess() 方法,ContainerBase 的子类就可以有一个线程来周期性地执行任务,例如检查时间戳或者 Session 对象有效时间。Listing12.4 展示了Tomcat5 中 StandardContext 的 backgroundProcess()方法的实现。

Listing 12.4: The backgroudProccss method of the StandardContext class

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);
    }
  }
}

需要明白 StandardContext 怎样帮助与其相关的管理器和加载器来周期性执行任务。

12.5小结

在本章中,我们学习了 StandardContext 及其相关类。另外还看到了 StandardContext对象是如何配置以及如何处理 HTTP 请求,最后一节讨论了 Tomcat5 中backgroundProcess()方法实现。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值