coding……
但行好事 莫问前程

Tomcat源码解读『web.xml解析』

上篇文章我们介绍了Tomcat Context是如何构建的,了解了Context构建的两种方式:

  • Host启动,触发HostConfig的Lifecycle.START_EVENT事件监听,构建Context
  • 后台线程,定期去执行Host的backgroundProcess方法,触发HostConfig的Lifecycle.PERIODIC_EVENT事件监听,构建Context

这里的构建Context,指的是构建一个Context对象,并将构建好的Context对象与Host组件关联起来(调用host.addChild(context)。

但如果容器只是构建了Context容器,是无法响应一个浏览器的一次请求。就web服务器的实现来看,一次请求过来除了需要确定这次请求访问的web应用具体所对应的Context对象(根据访问的path确定),还需要包含web应用中具体的哪个Servlet来处理这次请求,中间是否还需要执行相应的过滤器(filter)、监听器(listener)等。我们知道,这些servlet、filter和listener信息,是配置在一个web应用的WEB-INF\web.xml文件中的。

所以可以推测,Tomcat启动过程伴随着Context构建之后,必然存在一个web.xml的解析过程。本篇文章我们就来介绍一下web.xml的解析过程。

1. Context启动

上篇文章在介绍Context构建的时候提到,在HostConfig的deployApps方法中,可以通过三种方式构建Context,如下:

protected void deployApps() {

    File appBase = host.getAppBaseFile();
    File configBase = host.getConfigBaseFile();
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // Deploy XML descriptors from configBase
    deployDescriptors(configBase, configBase.list());
    // Deploy WARs
    deployWARs(appBase, filteredAppPaths);
    // Deploy expanded folders
    deployDirectories(appBase, filteredAppPaths);

}

在这三种方式中,基本思想都是一样的,构建Context对象,然后调用Host的addChild方法,将构建的Context对象添加到Host。在上述三种构建方式中,在构建Context时,都会调用如下一段代码:

Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);
/**
 * The Java class name of the default context configuration class
 * for deployed web applications.
 */
private String configClass =
    "org.apache.catalina.startup.ContextConfig";

同时我们也提到,Context的启动(start方法调用)就是在host的addChild方法中调用的。所以可以得出以下两点结论:

  • Context构建过程中,会为Context添加一个生命周期监听器ContextConfig
  • Context会在构建成功后,添加到Host组件过程中,启动(start方法调用)

而web.xml的解析就是通过ContextConfig生命周期监听器完成的。

2. ContextConfig

关于Context的启动过程,我们这里不做详细介绍,我们只需要知道最终会调用到standardContext的startInternal方法,在startInternal方法中,会触发生命周期监听器的Lifecycle.CONFIGURE_START_EVENT事件监听,如下:

// Notify our interested LifecycleListeners
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

关于fireLifecycleEvent的触发机制我们上篇文章介绍HostConfig时,已经介绍过,这里不重复介绍了。这里肯定会触发ContextConfig的事件监听如下:

public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }

}

所以会调用ContextConfig的configureStart()方法。

2.1 configureStart()

/**
 * Process a "contextConfig" event for this Context.
 */
protected synchronized void configureStart() {
    // Called from StandardContext.start()

    if (log.isDebugEnabled()) {
        log.debug(sm.getString("contextConfig.start"));
    }

    if (log.isDebugEnabled()) {
        log.debug(sm.getString("contextConfig.xmlSettings",
                context.getName(),
                Boolean.valueOf(context.getXmlValidation()),
                Boolean.valueOf(context.getXmlNamespaceAware())));
    }

    webConfig();

    if (!context.getIgnoreAnnotations()) {
        applicationAnnotationsConfig();
    }
    if (ok) {
        validateSecurityRoles();
    }

    // Configure an authenticator if we need one
    if (ok) {
        authenticatorConfig();
    }

    // Dump the contents of this pipeline if requested
    if (log.isDebugEnabled()) {
        log.debug("Pipeline Configuration:");
        Pipeline pipeline = context.getPipeline();
        Valve valves[] = null;
        if (pipeline != null) {
            valves = pipeline.getValves();
        }
        if (valves != null) {
            for (Valve valve : valves) {
                log.debug("  " + valve.getClass().getName());
            }
        }
        log.debug("======================");
    }

    // Make our application available if no problems were encountered
    if (ok) {
        context.setConfigured(true);
    } else {
        log.error(sm.getString("contextConfig.unavailable"));
        context.setConfigured(false);
    }

}

不难发现,核心逻辑肯定在webConfig中。

2.2 webConfig()

/**
 * Scan the web.xml files that apply to the web application and merge them
 * using the rules defined in the spec. For the global web.xml files,
 * where there is duplicate configuration, the most specific level wins. ie
 * an application's web.xml takes precedence over the host level or global
 * web.xml file.
 */
protected void webConfig() {
    /*
     * Anything and everything can override the global and host defaults.
     * This is implemented in two parts
     * - Handle as a web fragment that gets added after everything else so
     *   everything else takes priority
     * - Mark Servlets as overridable so SCI configuration can replace
     *   configuration from the defaults
     */

    /*
     * The rules for annotation scanning are not as clear-cut as one might
     * think. Tomcat implements the following process:
     * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of
     *   which Servlet spec version is declared in web.xml. The EG has
     *   confirmed this is the expected behaviour.
     * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main
     *   web.xml is marked as metadata-complete, JARs are still processed
     *   for SCIs.
     * - If metadata-complete=true and an absolute ordering is specified,
     *   JARs excluded from the ordering are also excluded from the SCI
     *   processing.
     * - If an SCI has a @HandlesType annotation then all classes (except
     *   those in JARs excluded from an absolute ordering) need to be
     *   scanned to check if they match.
     */
    WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
            context.getXmlValidation(), context.getXmlBlockExternal());

    Set<WebXml> defaults = new HashSet<>();
    defaults.add(getDefaultWebXmlFragment(webXmlParser));

    Set<WebXml> tomcatWebXml = new HashSet<>();
    tomcatWebXml.add(getTomcat