Tomcat的Catalina篇4-Web应用加载StandardContext和ContextConfig

505 阅读23分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

Catalina对Web应用的加载主要由StandardHost、HostConfig、StandardContext、ContextConfig、StandardWrapper这5个类完成。如果以一张时序图来展示Catalina对Web应用的加载过程,对应的流程图为: image.png

本文我们重点分析StandardContext、ContextConfig、StandardWrapper。

1. StandardContext

对于StandardHost和HostConfig来说,只是根据不同情况(部署描述文件、部署目录、WAR包)创建并启动Context对象,并不包含具体的Web应用初始化及启动工作,该部分工作由组件Context完成(当然这也是由各个组件定位所决定的)。

Web应用初始化时,会频繁涉及Servlet规范以及Tomcat对应的实现类,因此,为了便于理解,我们先给出Tomcat针对此部分的静态设计。Web容器相关的静态结构如图所示: image.png

从图中可知,Tomcat提供的ServletContext实现类为ApplicationContext。但是,该类仅供Tomcat服务器使用,Web应用使用的是其门面类ApplicationContextFacade。FilterConfig实现类为ApplicationFilterConfig,同时该类也负责Filter的实例化。FilterMap用于存储filter-mapping配置。NamingResources,用于存储Web应用声明的命名服务(JNDI)。StandardContext通过servletMappings 属性存储servlet-mapping配置

Context启动在StandardContext.startInternal()如下:

protected synchronized void startInternal() throws LifecycleException {

    if(log.isDebugEnabled()) {
        log.debug("Starting " + getBaseName());
    }

    // Send j2ee.state.starting notification
    // 1.发布正在启动的JMX通知,可以通过添加NotificationListener监听Web应用的启动
    if (this.getObjectName() != null) {
        Notification notification = new Notification("j2ee.state.starting",
                this.getObjectName(), sequenceNumber.getAndIncrement());
        broadcaster.sendNotification(notification);
    }

    setConfigured(false);
    boolean ok = true;

    // Currently this is effectively a NO-OP but needs to be called to
    // ensure the NamingResources follows the correct lifecycle
    // 2.启动当前Context维护的JNDI资源
    if (namingResources != null) {
        namingResources.start();
    }

    // Post work directory
    // 3.处理工作目录
    // 这个目录用于存放编译后的jsp文件,一般生成的目录格式为tomcat安装目录work+engine名+host名+context的baseName
    postWorkDirectory();

    // Add missing components as necessary
    if (getResources() == null) {   // (1) Required by Loader
        if (log.isDebugEnabled()) {
            log.debug("Configuring default Resources");
        }

        try {
            setResources(new StandardRoot(this));
        } catch (IllegalArgumentException e) {
            log.error(sm.getString("standardContext.resourcesInit"), e);
            ok = false;
        }
    }
    // 3.初始化当前Context使用的WebResourceRoot并启动,
    // WebResourceRoot维护了Web应用所有的资源集合(Class,Jar包以及其他资源文件),用于类加载和按照路径查找资源文件
    if (ok) {
        resourcesStart();
    }

    // 4.创建Web应用类加载器WebappLoader
    if (getLoader() == null) {
        // 加载web应用
        WebappLoader webappLoader = new WebappLoader();
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }

    // An explicit cookie processor hasn't been specified; use the default
    // 5.如果没有设置Cookie处理器,则创建默认的Rfc6265CookieProcessor
    if (cookieProcessor == null) {
        cookieProcessor = new Rfc6265CookieProcessor();
    }

    // Initialize character set mapper
    // 6.设置字符集映射CharsetMapper,该映射主要用于根据Locale获取字符集编码
    getCharsetMapper();

    // Validate required extensions
    // 8.Web应用的依赖检测,主要检测依赖扩展点完整性
    boolean dependencyCheck = true;
    try {
        dependencyCheck = ExtensionValidator.validateApplication
            (getResources(), this);
    } catch (IOException ioe) {
        log.error(sm.getString("standardContext.extensionValidationError"), ioe);
        dependencyCheck = false;
    }

    if (!dependencyCheck) {
        // do not make application available if dependency check fails
        ok = false;
    }

    // Reading the "catalina.useNaming" environment variable
    // 9.如果当前Context使用JNDI,则为其添加NamingContextListener
    String useNamingProperty = System.getProperty("catalina.useNaming");
    if ((useNamingProperty != null)
        && (useNamingProperty.equals("false"))) {
        useNaming = false;
    }

    if (ok && isUseNaming()) {
        if (getNamingContextListener() == null) {
            NamingContextListener ncl = new NamingContextListener();
            ncl.setName(getNamingContextName());
            ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite());
            addLifecycleListener(ncl);
            setNamingContextListener(ncl);
        }
    }

    // Standard container startup
    if (log.isDebugEnabled()) {
        log.debug("Processing standard container startup");
    }


    // Binding thread
    ClassLoader oldCCL = bindThread();

    try {
        if (ok) {
            // Start our subordinate components, if any
            // 10.启动Web应用类加载器,WebappLoader.start,此时才真正创建WebappClassLoader实例
            Loader loader = getLoader();
            if (loader instanceof Lifecycle) {
                ((Lifecycle) loader).start();
            }

            // since the loader just started, the webapp classloader is now
            // created.
            if (loader.getClassLoader() instanceof WebappClassLoaderBase) {
                WebappClassLoaderBase cl = (WebappClassLoaderBase) loader.getClassLoader();
                cl.setClearReferencesRmiTargets(getClearReferencesRmiTargets());
                cl.setClearReferencesStopThreads(getClearReferencesStopThreads());
                cl.setClearReferencesStopTimerThreads(getClearReferencesStopTimerThreads());
                cl.setClearReferencesHttpClientKeepAliveThread(getClearReferencesHttpClientKeepAliveThread());
                cl.setClearReferencesObjectStreamClassCaches(getClearReferencesObjectStreamClassCaches());
                cl.setClearReferencesThreadLocals(getClearReferencesThreadLocals());
            }

            // By calling unbindThread and bindThread in a row, we setup the
            // current Thread CCL to be the webapp classloader
            unbindThread(oldCCL);
            oldCCL = bindThread();

            // Initialize logger again. Other components might have used it
            // too early, so it should be reset.
            logger = null;
            getLogger();

            // 11.启动安全组件Realm
            Realm realm = getRealmInternal();
            if(null != realm) {
                if (realm instanceof Lifecycle) {
                    ((Lifecycle) realm).start();
                }

                // Place the CredentialHandler into the ServletContext so
                // applications can have access to it. Wrap it in a "safe"
                // handler so application's can't modify it.
                CredentialHandler safeHandler = new CredentialHandler() {
                    @Override
                    public boolean matches(String inputCredentials, String storedCredentials) {
                        return getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials);
                    }

                    @Override
                    public String mutate(String inputCredentials) {
                        return getRealmInternal().getCredentialHandler().mutate(inputCredentials);
                    }
                };
                context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler);
            }

            // Notify our interested LifecycleListeners
            // 12.发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件完成Servlet创建
            fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

            // Start our child containers, if not already started
            // 13.启动Context子节点Wrapper(在server.xml中配置的)
            for (Container child : findChildren()) {
                if (!child.getState().isAvailable()) {
                    child.start();
                }
            }

            // Start the Valves in our pipeline (including the basic),
            // if any
            // 14.启动Context维护的Pipeline
            if (pipeline instanceof Lifecycle) {
                ((Lifecycle) pipeline).start();
            }

            // Acquire clustered manager
            // 15.创建会话管理器,如果配置了集群组件,则由集群组件创建,否则使用标准的会话管理
            //器(StandardManager)。在集群环境下,需要将会话管理器注册到集群组件。
            Manager contextManager = null;
            Manager manager = getManager();
            if (manager == null) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("standardContext.cluster.noManager",
                            Boolean.valueOf((getCluster() != null)),
                            Boolean.valueOf(distributable)));
                }
                if ((getCluster() != null) && distributable) {
                    try {
                        contextManager = getCluster().createManager(getName());
                    } catch (Exception ex) {
                        log.error(sm.getString("standardContext.cluster.managerError"), ex);
                        ok = false;
                    }
                } else {
                    contextManager = new StandardManager();
                }
            }

            // Configure default manager if none was specified
            if (contextManager != null) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("standardContext.manager",
                            contextManager.getClass().getName()));
                }
                setManager(contextManager);
            }

            if (manager!=null && (getCluster() != null) && distributable) {
                //let the cluster know that there is a context that is distributable
                //and that it has its own manager
                // 集群环境下,将会话管理器注册到集群组件
                getCluster().registerManager(manager);
            }
        }

        if (!getConfigured()) {
            log.error(sm.getString("standardContext.configurationFail"));
            ok = false;
        }

        // We put the resources into the servlet context
        if (ok) {
            // 16.将Context的Web资源集合(org.apache.catalina.WebResourceRoot)添加到ServletContext
            //属性,属性名为org,apache.catalina.resources。
            getServletContext().setAttribute
                (Globals.RESOURCES_ATTR, getResources());

            if (getInstanceManager() == null) {
                setInstanceManager(createInstanceManager());
            }
            // 17.创建实例管理器(InstanceManager),用于创建对象实例,如Servlet、Filter等。
            getServletContext().setAttribute(
                    InstanceManager.class.getName(), getInstanceManager());
            InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());

            // Create context attributes that will be required
            // 18.将Jar包扫描器(JarScanner)添加到ServletContext)属性,属性名为org.apache.tomcat.
            //JarScanner.
            getServletContext().setAttribute(
                    JarScanner.class.getName(), getJarScanner());

            // Make the version info available
            getServletContext().setAttribute(Globals.WEBAPP_VERSION, getWebappVersion());
        }

        // Set up the context init params 设置初始化参数
        // 19.合并ServletContext初始化参数和Context组件中的ApplicationParameter
        mergeParameters();

        // Call ServletContainerInitializers
        // 20.启动添加到当前Context的ServletContainerInitializer。该类的实例具体由ContextConfig
        //查找并添加,具体过程见下一节讲解。该类主要用于以可编程的方式添加Wb应用的配置,如
        //Servlet、Filter等。
        for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
            initializers.entrySet()) {
            try {
                entry.getKey().onStartup(entry.getValue(),
                        getServletContext());
            } catch (ServletException e) {
                log.error(sm.getString("standardContext.sciFail"), e);
                ok = false;
                break;
            }
        }

        // Configure and call application event listeners
        // 21.实例化应用监听器(ApplicationListener),分为事件监听器(ServletContextAttribute-
        //Listener ServletRequestAttributeListener ServletRequestListener,HttpSessionldListener,HttpSession-
        //AttributeListener)以及生命周期监听器(HttpSessionListener、ServletContextListener)。这些监听
        //器可以通过Contexti部署描述文件、可编程的方式(ServletContainerInitializer)或者Web.xml添加,
        //并且触发ServletContextListener..contextInitialized。
        if (ok) {
            if (!listenerStart()) {
                log.error(sm.getString("standardContext.listenerFail"));
                ok = false;
            }
        }

        // Check constraints for uncovered HTTP methods
        // Needs to be after SCIs and listeners as they may programmatically
        // change constraints
        if (ok) {
            checkConstraintsForUncoveredMethods(findConstraints());
        }

        try {
            // Start manager
            // 23.启动会话管理器
            Manager manager = getManager();
            if (manager instanceof Lifecycle) {
                ((Lifecycle) manager).start();
            }
        } catch(Exception e) {
            log.error(sm.getString("standardContext.managerFail"), e);
            ok = false;
        }

        // Configure and call application filters
        // 24.实例化filter,并且调用其初始化方法
        if (ok) {
            if (!filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }
        }

        // Load and initialize all "load on startup" servlets
        // 25.对于loadOnStartup≥0的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并
        //调用Servlet..init进行初始化。
        if (ok) {
            if (!loadOnStartup(findChildren())){
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }
        }

        // Start ContainerBackgroundProcessor thread
        // 26.启动后台定时处理线程。只有当backgroundProcessorDelay>0时启动,用于监控守护文件
        //的变更等。当backgroundProcessorDelay≤O时,表示Context的后台任务由上级容器(Host)调度。
        super.threadStart();
    } finally {
        // Unbinding thread
        unbindThread(oldCCL);
    }

    // Set available status depending upon startup success
    if (ok) {
        if (log.isDebugEnabled()) {
            log.debug("Starting completed");
        }
    } else {
        log.error(sm.getString("standardContext.startFailed", getName()));
    }

    startTime=System.currentTimeMillis();

    // Send j2ee.state.running notification
    // 27.发布正在运行的JMX通知。
    if (ok && (this.getObjectName() != null)) {
        Notification notification =
            new Notification("j2ee.state.running", this.getObjectName(),
                             sequenceNumber.getAndIncrement());
        broadcaster.sendNotification(notification);
    }

    // The WebResources implementation caches references to JAR files. On
    // some platforms these references may lock the JAR files. Since web
    // application start is likely to have read from lots of JARs, trigger
    // a clean-up now.
    // 28.调用WebResourceRoot,gc()释放资源(WebResourceRoot加载资源时,为了提高性能会缓
    //存某些信息,该方法用于清理这些资源,如关闭JAR文件)。
    getResources().gc();

    // Reinitializing if something went wrong
    // 29.设置Contextl的状态,如果启动成功,设置为STARTING(其父类LifecycleBase会自动将
    //状态转换为STARTED),否则设置为FAILED。
    if (!ok) {
        setState(LifecycleState.FAILED);
        // Send j2ee.object.failed notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.object.failed",
                    this.getObjectName(), sequenceNumber.getAndIncrement());
            broadcaster.sendNotification(notification);
        }
    } else {
        setState(LifecycleState.STARTING);
    }
}

接下来看一下StandardContext的启动过程:

  1. 发布正在启动的JMX通知,这样可以通过添加NotificationListener来监听Web应用的启动。

  2. 启动当前Context维护的NDI资源。

  3. 初始化当前Context使用的WebResourceRoot并启动。WebResourceRoot维护了Web应用所有的资源集合(Class文件、Jar包以及其他资源文件),主要用于类加载和按照路径查找资源文件。

  4. 创建Web应用类加载器(WebappLoader)。WebappLoader继承自LifecycleMBeanBase,在其启动时创建Web应用类加载器(WebappClassLoader)。此外,该类还提供了background-Process,用于Context后台处理。当检测到Web应用的类文件、Jar包发生变更时,重新加载Context。

  5. 如果没有设置Cookie处理器,则创建默认的Rfc6265CookieProcessor

  6. 设置字符集映射(CharsetMapper),该映射主要用于根据Locale获取字符集编码。

  7. 初始化临时目录,默认为$CATALINA_BASE/work/<Engine名称><Host名称>/<Context名称>

  8. Wb应用的依赖检测,主要检测依赖扩展,点完整性。

  9. 如果当前Context使用JNDI,则为其添加NamingContextListener。

  10. 启动Web应用类加载器(WebappLoader.start),此时才真正创建WebappClassLoader实例

  11. 启动安全组件(Realm)。

  12. 发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件以完成Servlet的创建.

  13. 启动Context子节点(Wrapper),在server.xml中配置的Wrapper。

  14. 启动Context维护的Pipeline。

  15. 创建会话管理器。如果配置了集群组件,则由集群组件创建,否则使用标准的会话管理器(StandardManager)。在集群环境下,需要将会话管理器注册到集群组件。

  16. 将Context的Web资源集合(org.apache.catalina.WebResourceRoot)添加到Servlet(ontext属性,属性名为org.apache,catalina.resources

  17. 创建实例管理器(InstanceManager),用于创建对象实例,如Servlet、Filter等。

  18. 将Jar包扫描器(JarScanner)添加到ServletContext)属性,属性名为org.apache.tomcat.JarScanner

  19. 合并ServletContext初始化参数和Context组件中的ApplicationParameter。合并原则:ApplicationParameter配置为可以覆盖,那么只有当ServletContext没有相关参数或者相关参数为空时添加;如果配置为不可覆盖,则强制添加,此时即使ServletContext配置了同名参数也不会生效。

  20. 启动添加到当前Context的ServletContainerInitializer。该类的实例具体由ContextConfig查找并添加。该类主要用于以可编程的方式添加Wb应用的配置,如Servlet、Filter等。

  21. 实例化应用监听器(ApplicationListener),分为事件监听器(ServletContextAttributeListener,ServletRequestAttributeListener,ServletRequestListener HttpSessionldListener,HttpSessionAttributeListener)以及生命周期监听(HttpSessionListener、ServletContextListener)。这些监听器可以通过Context部署描述文件、可编程的方式(ServletContainerInitializer)或者Web.xml添加,并且触发ServletContextListener.contextInitialized。

  22. 检测未覆盖的HTTP方法的安全约束。

  23. 启动会话管理器。

  24. 实例化FilterConfig(ApplicationFilterConfig)、Filter,并调用Filter.init初始化e

  25. 对于loadOnStartup≥O的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init进行初始化。

  26. 启动后台定时处理线程。只有当backgroundProcessorDelay>O时启动,用于监控守护文件的变更等。当backgroundProcessorDelay≤O时,表示Context的后台任务由上级容器(Host)调度。

  27. 发布正在运行的MX通知。

  28. 调用WebResourceRoot.gc()释放资源(WebResourceRoot加载资源时,为了提高性能会缓存某些信息,该方法用于清理这些资源,如关闭JAR文件)。

  29. 设置Context的状态,如果启动成功,设置为STARTING(其父类LifecycleBase会自动将状态转换为STARTED),否则设置为FAILED

通过上面的讲述,我们已经知道了StandardContext的整个启动过程,但是这部分工作并不包含如何解析Web.xml中的Servlet、请求映射、Filter等相关配置。这部分工作具体是由ContextConfig完成的。

2. ContextConfig

Context创建时会默认添加一个生命周期监听器一ContextConfig。该监听器一共处理6类事件,此处我们仅讲解其中与Context启动关系重大的3类:AFTER_INIT_EVENT、BEFORE_START_EVENT、CONFIGURE_START_EVENT,以便读者可以了解该类在Context启动中扮演的角色。

2.1 AFTER_INIT_EVENT事件

严格意义上讲,该事件属于Context初始化阶段,它主要用于Context属性的配置工作。通过前面讲解我们可以知道,Context的创建可以有如下几个来源:

  • 在实例化Server时,解析server.xml文件中的Context元素创建

  • 在HostConfig部署Web应用时,解析Web应用(目录或者WAR包)根目录下的META-NF/context.xml文件创建。如果不存在该文件,则自动创建一个Context对象,仅设置path、docBase等少数几个属性

  • 在Host部署Web应用时,解析$CATALINA_BASE/conf/<Engine名称><Host名称>下的Context部署描述文件创建。

除了Context创建时的属性配置,将Tomcat提供的默认配置也一并添加到Context实例(如果Context没有显式地配置这些属性)。这部分工作即由该事件完成。

路径为ContextConfig.lifecycleEvent()->init()->contextConfig():

protected synchronized void init() {
    // Called from StandardContext.init()
    //创建一个Digester用于解析context.xml
    Digester contextDigester = null;

    if (!getUseGeneratedCode()) {
        contextDigester = createContextDigester();
        contextDigester.getParser();
    }

    if (log.isDebugEnabled()) {
        log.debug(sm.getString("contextConfig.init"));
    }
    //设置配置状态,默认设置为失败,以免被误任务成功
    context.setConfigured(false);
    ok = true;

    // todo
    contextConfig(contextDigester);
}


protected void contextConfig(Digester digester) {
    代码太多,请自行看github上代码注释
}

具体过程如下:

  1. 如果Context的override属性为false(即使用默认配置):
    • 如果存在conf/context.xml文件(Catalina容器级默认配置),那么解析该文件,更新当前Context实例属性;
    • 如果存在conf/<Engine名称>/<Host名称>/context.xml.default文件(Host级默认配置,那么解析该文件,更新当前Context实例属性。
  2. 如果Context的configFile属性不为空,那么解析该文件,更新当前Context实例的属性。

疑问,为什么最后一步还要解析configFile呢?因为在服务器独立运行时,该文件和创建Context时解析的文件是相同的。这是由于Digester解析时会将原有属性覆盖。试想一下,如果在创建Context时,我们指定了crossContext属性,而这个属性恰好在默认配置中也存在,此时我们希望的效果当然是忽略默认属性。而如果不在最后一步解析configFile,此时的结果将会是默认属性覆盖指定属性。除此之外,在嵌入式启动Tomcat时,Context为手动创建,即使存在META-INF/context.xml文件。此时,也需要解析configFile文件(即META-INF/context.xml文件),以更新其属性。

通过上面的执行顺序我们可以知道,Tomcat中Context属性的优先级为:configFile、conf/<Engine名称>/<Host名称>/context.xml.default、conf/context.xml,即Web应用配置优先级最高,其 次为Host配置,Catalina容器配置优先级最低。

2.2 BEFORE_START_EVENT事件

该事件在Context启动之前触发,用于更新Context的docBase属性和解决Web目录锁的问题。源码为ContextConfig.lifecycleEvent()->ContextConfig.beforeStart():

// BEFORE_START_EVENT事件,用于更新Context的docBase)属性和解决Web目录锁的问题
protected synchronized void beforeStart() {

    try {
        // 更新Context的docBase属性
        fixDocBase();
    } catch (IOException e) {
        log.error(sm.getString(
                "contextConfig.fixDocBase", context.getName()), e);
    }
    // 解决Web目录锁的问题
    antiLocking();
}

2.2.1 更新Context的docBase属性

更新Context的docBase属性主要是为了满足WAR部署的情况。当Web应用为一个WAR压缩包且需要解压部署(Host的unpackWAR为true,且Context的unpackWAR为true)时,docBase属性指向的是解压后的文件夹目录,而非WAR包的路径。ContextConfig.fixDocBase()源码如下:

//对docBase做调整
protected void fixDocBase() throws IOException {

    /**
     * 1. 根据Host的appBase以及Context的docBase计算docBase的绝对路径。
     */
    Host host = (Host) context.getParent();
    File appBase = host.getAppBaseFile();

    // This could be blank, relative, absolute or canonical
    String docBaseConfigured = context.getDocBase();
    // If there is no explicit docBase, derive it from the path and version
    //如果没有设置docBase,那么就根据应用路径重新设置路径
    if (docBaseConfigured == null) {
        // Trying to guess the docBase according to the path
        String path = context.getPath();
        if (path == null) {
            return;
        }
        //通过路径和版本去获取docBase
        ContextName cn = new ContextName(path, context.getWebappVersion());
        docBaseConfigured = cn.getBaseName();
    }

    // Obtain the absolute docBase in String and File form
    String docBaseAbsolute;
    File docBaseConfiguredFile = new File(docBaseConfigured);
    if (!docBaseConfiguredFile.isAbsolute()) {
        docBaseAbsolute = (new File(appBase, docBaseConfigured)).getAbsolutePath();
    } else {
        docBaseAbsolute = docBaseConfiguredFile.getAbsolutePath();
    }
    File docBaseAbsoluteFile = new File(docBaseAbsolute);
    String originalDocBase = docBaseAbsolute;

    ContextName cn = new ContextName(context.getPath(), context.getWebappVersion());
    String pathName = cn.getBaseName();

    boolean unpackWARs = true;
    if (host instanceof StandardHost) {
        unpackWARs = ((StandardHost) host).isUnpackWARs();
        if (unpackWARs && context instanceof StandardContext) {
            unpackWARs =  ((StandardContext) context).getUnpackWAR();
        }
    }

    // At this point we need to determine if we have a WAR file in the
    // appBase that needs to be expanded. Therefore we consider the absolute
    // docBase NOT the canonical docBase. This is because some users symlink
    // WAR files into the appBase and we want this to work correctly.
    //判断这个docBase路径是否是webapps下的
    boolean docBaseAbsoluteInAppBase = docBaseAbsolute.startsWith(appBase.getPath() + File.separatorChar);
    /**
     * 2. 如果这个docBase指定的是一个war包,那么就将其解压
     */
    if (docBaseAbsolute.toLowerCase(Locale.ENGLISH).endsWith(".war") && !docBaseAbsoluteFile.isDirectory()) {
        URL war = UriUtil.buildJarUrl(docBaseAbsoluteFile);
        if (unpackWARs) {
            //解压war包,返回解压后的目录路径,war解析源码请看war包解析笔记
            docBaseAbsolute = ExpandWar.expand(host, war, pathName);
            // 将Context的docBase更新为解压后的路径(基于appBasel的相对路径)
            docBaseAbsoluteFile = new File(docBaseAbsolute);
            if (context instanceof StandardContext) {
                //设置未解压时指定的位置,因为解压时会将解压后的内容放到host指定的appBase目录下
                ((StandardContext) context).setOriginalDocBase(originalDocBase);
            }
        } else {
            //如果不需要解压,那么就校验对应的docbase是由已经存在了,如果不存在,直接报错
            ExpandWar.validate(host, war, pathName);
        }
    } else {
        File docBaseAbsoluteFileWar = new File(docBaseAbsolute + ".war");
        URL war = null;
        //如果war包存在,并且是在APPBase中的,那么直接获取其war的url
        if (docBaseAbsoluteFileWar.exists() && docBaseAbsoluteInAppBase) {
            war = UriUtil.buildJarUrl(docBaseAbsoluteFileWar);
        }
        //如果目录存在,并且是war包,允许解压
        if (docBaseAbsoluteFile.exists()) {
            if (war != null && unpackWARs) {
                // Check if WAR needs to be re-expanded (e.g. if it has
                // changed). Note: HostConfig.deployWar() takes care of
                // ensuring that the correct XML file is used.
                // This will be a NO-OP if the WAR is unchanged.
                ExpandWar.expand(host, war, pathName);
            }
        } else {
            if (war != null) {
                if (unpackWARs) {
                    docBaseAbsolute = ExpandWar.expand(host, war, pathName);
                    docBaseAbsoluteFile = new File(docBaseAbsolute);
                } else {
                    docBaseAbsoluteFile = docBaseAbsoluteFileWar;
                    ExpandWar.validate(host, war, pathName);
                }
            }
            if (context instanceof StandardContext) {
                ((StandardContext) context).setOriginalDocBase(originalDocBase);
            }
        }
    }

    String docBaseCanonical = docBaseAbsoluteFile.getCanonicalPath();

    // Re-calculate now docBase is a canonical path
    boolean docBaseCanonicalInAppBase =
            docBaseAbsoluteFile.getCanonicalFile().toPath().startsWith(appBase.toPath());
    String docBase;
    //如果目录为appBase下的,那么直接截取到appBase后面一截
    if (docBaseCanonicalInAppBase) {
        docBase = docBaseCanonical.substring(appBase.getPath().length());
        docBase = docBase.replace(File.separatorChar, '/');
        if (docBase.startsWith("/")) {
            docBase = docBase.substring(1);
        }
    } else {
        docBase = docBaseCanonical.replace(File.separatorChar, '/');
    }

    context.setDocBase(docBase);
}

具体的处理过程如下:

  1. 根据Host的appBase以及Context的docBase计算docBase的绝对路径。
  2. 如果docBase为一个WAR文件,且需要解压部署:
    • 解压WAR文件;
    • 将Context的docBase更新为解压后的路径(基于appBase的相对路径)。 如果不需要解压部署,只检测WAR包,不更新docBase。
  3. 如果docBase为一个有效目录,而且存在与该目录同名的WAR包,同时需要解压部署,则重新解压WAR包。
  4. 如果docBase为一个不存在的目录,但是存在与该目录同名的WAR包,同时需要解压部署:
    • 解压WAR文件;
    • 将Context的docBase更新为解压后的路径(基于appBasel的相对路径)。 如果不需要解压部署,只检测WAR包,docBase为WAR包路径。

2.2.2 解决Web目录锁的问题

当Context的antiResourceLocking属性为true时,Tomcat会将当前的Web应用目录复制到临时文件夹下,以避免对原目录的资源加锁。ContextConfig.antiLocking()源码如下:

protected void antiLocking() {

    // 1.根据Host的appBase以及Context的docBase计算docBase的绝对路径。
    if ((context instanceof StandardContext)
            && ((StandardContext) context).getAntiResourceLocking()) {

        Host host = (Host) context.getParent();
        String docBase = context.getDocBase();
        if (docBase == null) {
            return;
        }
        originalDocBase = docBase;

        File docBaseFile = new File(docBase);
        if (!docBaseFile.isAbsolute()) {
            docBaseFile = new File(host.getAppBaseFile(), docBase);
        }

        String path = context.getPath();
        if (path == null) {
            return;
        }
        ContextName cn = new ContextName(path, context.getWebappVersion());
        docBase = cn.getBaseName();

        String tmp = System.getProperty("java.io.tmpdir");
        File tmpFile = new File(tmp);
        if (!tmpFile.isDirectory()) {
            log.error(sm.getString("contextConfig.noAntiLocking", tmp, context.getName()));
            return;
        }
        // 2.计算临时文件夹中的Wb应用根目录或WAR包名。
        if (originalDocBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
            // WAR包:${Context生命周期内的部署次数}-$WAR包名}。
            antiLockingDocBase = new File(tmpFile, deploymentCount++ + "-" + docBase + ".war");
        } else {
            // Web目录:${Context生命周期内的部署次数}-${目录名}。
            antiLockingDocBase = new File(tmpFile, deploymentCount++ + "-" + docBase);
        }
        antiLockingDocBase = antiLockingDocBase.getAbsoluteFile();

        if (log.isDebugEnabled()) {
            log.debug("Anti locking context[" + context.getName()
                    + "] setting docBase to " +
                    antiLockingDocBase.getPath());
        }

        // Cleanup just in case an old deployment is lying around
        ExpandWar.delete(antiLockingDocBase);
        // 3. 复制web目录或者WAR包到临时目录
        if (ExpandWar.copy(docBaseFile, antiLockingDocBase)) {
            // 4.将Context的docBase更新为临时目录下的Web应用目录或者WAR包路径。
            context.setDocBase(antiLockingDocBase.getPath());
        }
    }
}

具体过程如下:

  1. 根据Host的appBase以及Context的docBase计算docBase的绝对路径。
  2. 计算临时文件夹中的Web应用根目录或WAR包名。
    • Web目录:{Context生命周期内的部署次数;-{目录名}。
    • WAR包:SContext生命周期内的部署次数}-S{WAR包名}。
  3. 复制Web目录或者WAR包到临时目录。
  4. 将Context的docBase更新为临时日录下的Web应用目录或者WAR包路径。

通过上面的讲解我们知道,无论是AFTER_INIT_EVENT还是BEFORE_START_EVENTI的处理,仍属于启动前的准备工作,以确保Context相关属性的准确性。而真正创建Wrapper的则是CONFIGURE_START_EVENT事件

2.3 CONFIGURE_START_EVENT事件

Context在启动子节点之前,触发了CONFIGURE_START_EVENT事件(上面第1小节的第12个步骤)。ContextConfig正是通过该事件解析web.xml,创建Wrapper(Servlet、Filter、ServletContextListener等一系列Web容器相关的对象,完成Web容器的初始化的。

我们先从整体上看一下ContextConfig在处理CONFIGURE_START_EVENT事件时做了哪些工作,然后再具体介绍web.xml的解析过程。ContextConfig.lifecycleEvent()->ContextConfig.configStart():

protected synchronized void configureStart() {
    ...
    // todo 1. 开始web.xml的配置
    webConfig();

    //2. 如果未配置忽略应用注解配置,那么对filter,servlet,listener进行Resource注解的搜索
    // Resource配置在类,字段,方法上,会根据资源的类型进行划分资源类型,添加到不同资源集合中,比如环境变量,JNDI等等资源
    if (!context.getIgnoreAnnotations()) {
        applicationAnnotationsConfig();
    }
    if (ok) {
        // 3. 将context约束的角色和wrapper中注解RunAs或配置中设置的角色添加到context容器中,重复的不会被再次添加
        validateSecurityRoles();
    }

    // Configure an authenticator if we need one
    //4. 配置验证器,如果没有Ralm或者实现了 Authenticator的管道阀,那么就会添加一个默认的NonLoginAuthenticator验证器
    if (ok) {
        authenticatorConfig();
    }
    ...
    // Make our application available if no problems were encountered
    //如果配置context时没有遇到任何问题,那么就表示配置成功
    if (ok) {
        context.setConfigured(true);
    } else {
        log.error(sm.getString("contextConfig.unavailable"));
        context.setConfigured(false);
    }
}

该事件的主要工作内容如下:

  • 根据配置创建Wrapper(Servlet)、Filter、ServletContextListener等,完成Web容器的初始化。除了解析Web应用目录下的web.xml外,还包括Tomcat默认配置、web-fragment.xml、ServletContainerInitializer,以及相关XML文件的排序和合并。

  • 如果StandardContext的ignoreAnnotations为false,则解析应用程序注解配置,添加相关的JNDI资源引用

  • 基于解析完的Web容器,检测Web应用部署描述中使用的安全角色名称,当发现使用了未定义的角色时,提示警告同时将未定义的角色添加到Context安全角色列表中。

  • 当Context需要进行安全认证但是没有指定具体的Authenticatorl时,根据服务器配置自动创建默认实例。

我们重点看一下web容器初始化。

2.3.1 Web容器初始化

根据Servlet规范,Web应用部署描述可来源于WEB-INF/web.xml、Web应用JAR包中的META-INF/web-fragment.xml 和 META-INF/services/javax.servlet.ServletContainerInitializer

web-fragment.xml:可以看作web.xml的片段,其绝大部分元素均与web.xml相同,通过将其置于JAR包的 META-INF目录下,可以将Web应用的配置拆解到各个模块中,而不必统一在web.xml中配置。这有利于Web应用 的可插拔和模块化。

其中META-INF/services/javax.servlet..ServletContainerInitializer文件中配置了所属JAR中该接 口的实现类,用于动态注册Servlet,这是Servlet规范基于SPI机制的可编程实现。

除了Servlet规范中提到的部署描述方式,Tomcat还支持默认配置,以简化Web应用的配置工 作。这些默认配置包括容器级别(conf/web.xml)和Host级别(conf/<Engine名称><Host名 称>/web.xml.default)。Tomcat解析时确保Web应用中的配置优先级最高,其次为Host级,最后为 容器级

Tomcat初始化Web容器的过程如下(ContextConfig.webConfig)。

  1. 解析默认配置,生成WebXml对象(Tomcat使用该对象表示web.xml的解析结果)。先解析容器级配置,然后再解析Host级配置。这样对于同名配置,Host级将覆盖容器级。为了便于后续过程描述,我们暂且称之为“默认WebXml”。为了提升性能,ContextConfig对默认WebXml进行了缓存,以避免重复解析。

  2. 解析Web应用的web.xml文件。如果StandardContext的altDDName不为空,则将该属性指向的文件作为web,xml,否则使用默认路径,即WEB-NF/web.xml。解析结果同样为WebXml对象(此时创建的对象为主WebXml,其他解析结果均需要合并到该对象上)。暂时将其称为“主WebXml"。

  3. 扫描Web应用所有JAR包,如果包含META-INF/web-fragment.xml,则解析文件并创建WebXml对象。暂时将其称为“片段WebXml”

  4. 将web-fragment.xml创建的WebXml对象按照Servlet规范进行排序,同时将排序结果对应的JAR文件名列表设置到ServletContext属性中,属性名为javax.servlet.context.orderedLibs。该排序非常重要,因为这决定了Filter等的执行顺序。

注意:尽管Servlet规范定义了web-fragment.xml的排序(绝对排序和相对排序),但是为了降低各个模块的耦合度,Web应用在定义web-fragment.xml时,应尽量保证相对独立性,减少相互间的依赖,将产生依赖过多的配置尝试放到web.xml中。

  1. 查找ServletContainerInitializer实现,并创建实例,查找范围分为两部分。

    • Web应用下的包:如果javax.servlet.context..orderedLibs不为空,仅搜索该属性包含的包,否则搜索WEB-NF/lib下所有包。
    • 容器包:搜索所有包。 Tomcat返回查找结果列表时,确保Web应用的顺序在容器之后,因此容器中的实现将先加载。
  2. 根据ServletContainerInitializer查询结果以及javax.servlet.annotation.HandlesTypes注解配置,初始化typeInitializerMap和initializerClassMap两个映射(主要用于后续的注解检测),前者表示类对应的ServletContainerInitializer集合,而后者表示每个ServletContainerInitializer对应的类的集合,具体类由javax.servlet.annotation.HandlesTypes注解指定。

  3. 当“主WebXml'”的metadataComplete为false或者typeInitializerMap不为空时。

    • ① 处理WEB-INF/classes下的注解,对于该目录下的每个类应做如下处理。
      • 检测javax.servlet.annotation.HandlesTypes注解。
      • 当WebXml的metadataComplete为false,查找javax.servlet.annotation.WebServlet,javax.servlet.annotation.WebFilter、javax.servlet..annotation.WebListener注解配置,将其合并到“主WebXml”。
    • ② 处理JAR包内的注解,只处理包含web-fragment.xml的JAR,对于JAR包中的每个类做如下处理。
      • 检测javax.servlet.annotation,HandlesTypes注解;
      • 当“主WebXml”和“片段WebXml”的metadataComplete均为false,查找javax.servlet.annotation.WebServlet,javax.servlet.annotation.WebFilter,javax.servlet.annotation.WebListener注解配置,将其合并到“片段WebXml”。
  4. 如果“主WebXml'”的metadataComplete为false,将所有的“片段WebXml”按照排序顺序合并到“主WebXml”。

  5. 将“默认WebXml”合并到“主WebXml”。

  6. 配置JspServlet。对于当前Web应用中JspFile属性不为空的Servlet,将其servletClass设置为org.apache.jasper.servlet.JspServlet(Tomcat提供的SP引擎),将JspFile设置为Servlet的初始化参数,同时将名称为“jsp”的Servlet(见conf/web.xml)的初始化参数也复制到该Servlet中。

  7. 使用“主WebXml”配置当前StandardContext,包括Servlet、Filter、Listener等Servlet规范中支持的组件。对于ServletContext层级的对象,直接由StandardContext维护,对于Servlet,则创建StandardWrapper子对象,并添加到StandardContext实例

  8. 将合并后的WebXml保存到ServletContext属性中,便于后续处理复用,属性名为org.apache.tomcat.util.scan.MergedwebXml

  9. 查找JAR包"META-INF/resources/”下的静态资源,并添加到StandardContext。

  10. 将ServletContainerInitializer扫描结果添加到StandardContext,以便StandardContext启动 时使用。

至此,StandardContext在正式启动StandardWrapper子对象之前,完成了Web应用容器的初始化,包括Servlet规范中涉及的各类组件、注解以及可编程方式的支持。

2.3.2 应用程序注解配置

当StandardContext的ignoreAnnotations为false时,Tomcat支持读取如下接口的Java命名服务注解配置,添加相关的JNDI资源引用,以便在实例化相关接口时,进行JNDI资源依赖注入。

支持读取的接口如下:

  • Web应用程序监听器
    • javax.servlet.ServletContextAttributeListener
    • javax.servlet.ServletRequestListener
    • javax.servlet.ServletRequestAttributelistener
    • javax.servlet.http.HttpSessionAttributeListener
    • javax.servlet.http.HttpSessionListener
    • javax.servlet.ServletContextListener
  • javax.servlet.Filter
  • javax.servlet.Servlet

支持读取的注解包括类注解、属性注解、方法注解,具体注解如下:

  • 类:javax.annotation.Resource、javax.annotation.Resources
  • 属性和方法:javax.annotation.Resource

3. StandardWrapper

StandardWrapper具体维护了Servlet实例,而在StandardContext启动过程中,StandardWrapper的处理分为两部分:

  • 首先,当通过ContextConfig完成Web容器初始化后,先调用StandardWrapper.start,此时StandardWrapper组件的状态将变为STARTED(除广播启动通知外,不进行其他处理)。
    • 路径,上面2.3.1的第12步骤->ContextConfig.configureContext()->context.addChild()->ContainerBase.addChild()->child.satrt()这块大家自行研究
  • 其次,对于启动时加载的Servlet(load-on-startup≥0),调用StandardWrapper.load,完成Servlet的加载。
    • 上面第1小节第25步骤StandardContext.loadOnStartup()->StandardWrapper.load()。

我们分析一下StandardWrapper.load()源码:

public synchronized void load() throws ServletException {

    // todo
    instance = loadServlet();
    ...
}
public synchronized Servlet loadServlet() throws ServletException {
        ...
        
        try {
            /**
             * 1.  创建Servlet对象,如果添加了JNDI资源注解,将进行依赖注入。
             */
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } 
        ...
        
        /** 2. 读取javax.servlet.annotation.MultipartConfig注解配置,以用于multipart/form-data请求处理,
         * 包括临时文件存储路径、上传文件最大字节数、请求最大字节数、文件大小阈值。
        */
        if (multipartConfigElement == null) {
            MultipartConfig annotation =
                    servlet.getClass().getAnnotation(MultipartConfig.class);
            if (annotation != null) {
                multipartConfigElement =
                        new MultipartConfigElement(annotation);
            }
        }

       ...

        /** 3. 初始化Servlet对象
         */
        initServlet(servlet);

        fireContainerEvent("load", this);

        loadTime=System.currentTimeMillis() -t1;
    } finally {
        if (swallowOutput) {
            String log = SystemLogHandler.stopCapture();
            if (log != null && log.length() > 0) {
                if (getServletContext() != null) {
                    getServletContext().log(log);
                } else {
                    out.println(log);
                }
            }
        }
    }
    return servlet;
}

StandardWrapper的load过程具体如下:

  1. 创建Servlet实例,如果添加了JNDI资源注解,将进行依赖注入。

  2. 读取javax.servlet.annotation.MultipartConfig注解配置,以用于multipart/form-data请求处理,包括临时文件存储路径、上传文件最大字节数、请求最大字节数、文件大小阈值。

  3. 调用javax.servlet.Servlet.init()方法进行Servlet初始化

至此,整个Wb应用的加载过程便已完成,可以结合流程图再回顾一下,以便加深理解。 image.png

4. Context命名规则

简单说一下Context的命名规则。尽管在大多数情况下,Context的名称与部署目录名称或者WAR包名称(去除扩展名,下文称为“基础文件名称”)相同,但是Tomcat支持的命名规则要复杂得多。在部署较简单的情况下,我们基本可以忽略Tomcat对Context命名规则的处理,但是在复杂部署的情况下,这可能会给我们的应用部署管理带来极大便利。

实际上,Context的name、path和version这3个属性与基础文件名称有非常紧密的关系:

  • 当未指定version时,name与path相同。如果path为空字符串,基础文件名称为“ROOT”;
  • 否则,将path起始的“/”删除,并将其余“/”替换成“#”即为基础文件名称。
  • 如果指定了version,则path不变,name和基础文件名称将追加“##”和具体版本号。

尽管以上描述以name、path、version推导基础文件名称,但是在自动部署的情况下,则是由基础文件名称生成name、path、version信息,具体规则实现参见org.apache.catalina.util.ContextName。

Tomcat部署文件与请求路径转换规则:

基础文件名称NamePathVersion部署文件名称
foo/foo/foofoo.xml、foo.war、foo
foo#bar/foo/bar/foo/barfoo#bar.xml、foo#bar.war、foo#bar
f00#2/f00#2/foo2foo#2.xml、foo#2.war、foo#2
foo#bar##2/foo/bar##2/foo/bar2foo#bar##2.xml、foo#bar##2.war、foo#bar##2
ROOTROOT.xml、ROOT.war、ROOT
ROOT##2##22ROOT##2.xml、ROOT##2.war、ROOT##2

那么问题来了,以“foo”和“foo#2”为例,既然当版本号不同时,Tomeat的基础文件名称 不同,那么同一个Tomcat实例下是否可以同时部署多个版本的Web应用呢?答案是肯定的。

Tomcat支持同时以相同的Context路径部署多个版本的Web应用,此时Tomcat将按照如下规则将请求匹配到对应版本的Context。

  • 如果请求中不包含session信息,将使用最新版本。
  • 如果请求中包含session信息,检查每个版木中的会话管理器,如果会话管理器包含当前会话,则使用该版本。
  • 如果请求中包含session信息,但是并未找到匹配的版本,则使用最新版本。

通过Context的命名规则,我们可以更合理地划分请求目录,尤其是当我们面临的是数个Web应用统一部署时。例如我们对外提供的是一个CM产品,包括销售、市场营销、客户服务3个独 立的应用。对于CRM,企业提供的统一根请求地址是http:/ip:port/crm,3个应用的子地址分别为 http:://ip:port/crm/sale、htp:/ip:port/crm/market、http:lip:port/cm/customer。 这时,我们只需要将3个应用的部署目录命名为:crm#sale、crm#market、crm#customer即可。通过这种方式,我们在保 证请求目录统一的情况下,实现了对Wb应用的分解

通过在部署目录名称中增加版本号信息,在请求路径不变的情况下,实现了Wb应用的多版本管理,便于系统的升级和降级。

参考文章

tomcat-9.0.60-src源码解析 
Tomcat架构解析
Tomcat剖析之源码篇