掘金 后端 ( ) • 2024-04-25 09:37

theme: smartblue

从上面那篇文章里面我们知道每个应用(我们打包的war)对应一个Context,每个Servlet对应一个Wrapper。但是应用、Servlet是怎么被装载进Tomcat?怎么被统一管理的?是如何对应到Context、Warpper的?下面我们一起来分析下。

Context创建过程

按照正常思路,当前容器一般应该是由其父容器进行加载跟启动,于是可以看下Context的父容器StandardHost类,发现StandardHost类非常简单,并没有想象中的Context的发现、解析、装载等。于是我们只能回头看Catalina的启动过程,由于Tomcat是使用Digester来解析server.xml文件的,所有看Tomcat的启动过程主要分析Digester的生成即可(在Catalina#createStartDigester方法),我们看到HostRuleSet中会对添加一个LifecycleListenerRule的Rule

digester.addRule(prefix + "Host",
                         new LifecycleListenerRule
                         ("org.apache.catalina.startup.HostConfig",
                          "hostConfigClass"));

而LifecycleListenerRule做的事情其实就是初始化了HostConfig这个Listener,并且为StandardHost增加了这个Listener。

Container c = (Container) digester.peek();
......
// Instantiate a new LifecycleListener implementation object
Class<?> clazz = Class.forName(className);
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();

// Add this LifecycleListener to our associated component
c.addLifecycleListener(listener);

接着我们详细分析下HostConfig这个类。由于这个类是一个LifecycleListener接口的实现类,所以很容易找他的对外方法就是lifecycleEvent方法。

public void lifecycleEvent(LifecycleEvent event) {

    // Identify the host we are associated with
    try {
        host = (Host) event.getLifecycle();
        if (host instanceof StandardHost) {
            setCopyXML(((StandardHost) host).isCopyXML());
            setDeployXML(((StandardHost) host).isDeployXML());
            setUnpackWARs(((StandardHost) host).isUnpackWARs());
            setContextClass(((StandardHost) host).getContextClass());
        }
    } catch (ClassCastException e) {
        log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
        check();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.START_EVENT)) {
        start();
    } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
        stop();
    }
}

可以看到这里做了一些属性的初始化,因为这些参数基本都是配置在Host节点上的,这里将属性做了copy便于后续使用。

我们都知道整个容器的生命周期都是遵循Lifecycle接口规范的,在Host启动时候就会触发Lifecycle.START_EVENT事件,所以我们主要看启动过程的start方法,而start方法里面主要就是调用的deployApps方法。

deployApps

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);
    }
tomcat应用的三种部署方式
  • deployDescriptors:按照描述文件方式部署,描述文件位置是$CATALINA_BASE/conf/[enginename]/[hostname]/[webappname].xml,会按照配置文件中的描述去创建Context,并添加资源信息。
  • deployWARs:会首先尝试看是否已经有解压的app目录,如果过有用已解压的部署,否则会使用war包部署,会先尝试使用META-INF/context.xml来创建Context,如果没有这个文件则使用反射创建一个默认的Context。
  • deployDirectories:可以看成deployWARs的简化版本,去除了检查使用war包部署的过程,其他流程一致。
应用的部署流程

细看下这三种部署方式的代码,各有差异,但整体的流程是一致的

  1. 创建Context。deployDescriptors这种方式只会根据描述文件去创建,否则会创建失败。而其他两种方式会首先尝试根据描述文件去创建,如果找不到则会创建一个默认的Context。在Tomcat的Document中说“Any Context Descriptors will be deployed first.”,可以看出跟我们之前分析一致,而Tomcat应该也是推荐我们用描述文件的方式去创建。实际使用的时候我们大多不会去特意定义Context.xml,更多的是使用现在比较流行的“约定大于配置”思想去创建默认的,其实这样也带来了统一性的好处。使用统一的配置无论是在后续的问题排查还是后续的维护都带来明显的好处。
  2. 设置基本属性。包括创建LifecycleListener,这个LifecycleListener的作用跟HostConfig类似,设置Tomcat的名字、版本等信息。
  3. 添加到父容器。通过host.addChild(context)添加到父容器中去。
  4. 创建DeployedApplication对象。包括当前app的名称,是否有描述文件,需要检测的资源信息(用与检测变化重新部署),以及已经部署标识,防止同一个应用多次部署。

Context启动

到这里应用也就是Context的创建就告一段落了,接下来看下Context的启动以及Servlet的创建过程,主要看下StandardContext#startInternal方法。

 	// Post work directory
    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;
        }
    }
    if (ok) {
        resourcesStart();
    }

首先设置了工作目录,一般是:$CATALINA_BASE/work/[enginename]/[hostname]/[webappname]。用来存储一些运行过程中的一些数据,比如JSP运行时候创建的Java文件以及对应的Class文件等。

接着创建了Resource对象,并且启动,在这里会根据当前Context的类型创建不同的WebResourceSet并且扫描应用/WEB-INF/lib下的jar文件,创建相应的ResourceSet。

	if (getLoader() == null) {
        WebappLoader webappLoader = new WebappLoader();
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }
	... ...
 		// Start our subordinate components, if any
        Loader loader = getLoader();
        if (loader instanceof Lifecycle) {
            ((Lifecycle) loader).start();
        }

        // since the loader just started, the webapp classloader is now
        // created.
        setClassLoaderProperty("clearReferencesRmiTargets",
                getClearReferencesRmiTargets());
        setClassLoaderProperty("clearReferencesStopThreads",
                getClearReferencesStopThreads());
        setClassLoaderProperty("clearReferencesStopTimerThreads",
                getClearReferencesStopTimerThreads());
        setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread",
                getClearReferencesHttpClientKeepAliveThread());
        setClassLoaderProperty("clearReferencesObjectStreamClassCaches",
                getClearReferencesObjectStreamClassCaches());
        setClassLoaderProperty("clearReferencesThreadLocals",
                getClearReferencesThreadLocals());

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

这里主要创建了一个ClassLoader(ParallelWebappClassLoader),并且启动、绑定到线程上,Tomcat为了应用的隔离性,每个应用都会创建一个独有的ClassLoader,后续会专门讲下这个ClassLocader。

Wrapper加载过程

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

在往下我们就看到这个代码块了,触发了一个CONFIGURE_START_EVENT时间,还记得上面创建Context时候添加的LifecycleListener吗?现在到了看org.apache.catalina.startup.ContextConfig的时候了。

ContextConfig

我们到ContextConfig中找到lifecycleEvent方法,发现触发这个事件的时候调用了configureStart方法,而这个方法里面最核心的调用方法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.
     */

这是这个方法的注释,简单的来说就是对web.xml的扫描、解析、合并。

	WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
                context.getXmlValidation(), context.getXmlBlockExternal());

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

    WebXml webXml = createWebXml();

    // Parse context level web.xml
    InputSource contextWebXml = getContextWebXmlSource();
    if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
        ok = false;
    }

这段比较简单,就是把web.xml解析成了WebXml对象。接下来是对整个Servlet的加载过程,我也按照源码中标记的步骤逐步说明下。

  1. processJarsForWebFragments。 扫描/WEB-INF/lib 目录下的每个jar文件中是否包含 /META-INF/web-fragment.xml文件,如果存在则解析并返回。或许大家对/META-INF/web-fragment.xml比较陌生,原本一个web应用的任何配置都需要放在web.xml中,当项目比较大的时候会使得web.xml变得比较混乱。于是从Servlet 3.0开始就可以将每个Servlet、Filter、Listener打成jar包,放在WEB-INF\lib目录下,每个模块都有各自的配置文件,这个配置文件的名子就是 web-fragment.xml。但其实现在大家进行web应用开发的时候基本都是使用Spring MVC所以基本也不大会用到这个特性。
  2. WebXml.orderWebFragments。按照Servlet规范,在处理之前需要先对这些片段进行排序。
  3. processServletContainerInitializers。扫描ServletContainerInitializer的实现类。 前提:当配置不完全的时候(metadata-complete=false),执行如下步骤
  4. processClasses(webXml, orderedFragments)。这里分为两步处理,扫描①/WEB-INF/classes下②每个fragment下,Servlet相关的注解。如WebServlet、WebFilter、WebListener,将其初始化为ServletDef对象,并加入到fragment中。
  5. webXml.merge(orderedFragments)。将每个单独的fragment合并到主的webXml中。
  6. webXml.merge(defaults)。合并默认的webXml,一般包含了JSP Servlet的定义,默认的web.xml一般在$CATALINA_BASE/conf/web.xml。
  7. convertJsps(webXml)。将显示指定的JSP转换为Servlet,这里会用到JSP Servlet的定义,如果没有也没关系,会创建一个默认的。
  8. configureContext(webXml)。这个是最重要的一个方法,看名字就是配置Context,方法内基本上也是将web.xml解析完的数据赋值给Context对象,包括显示名称、介绍、版本等信息。也会将例如Filter、Lintener加入到Context中,当然最重要的是将ServletDef对象转化为Wrapper对象,并且添加为Context的子容器。有兴趣的可以细看下这块代码,还有包括Session配置,欢迎页面等的处理都在这块代码。
  9. processResourceJARs(resourceJars)。这里主要是加载fragment中的静态资源,META-INF/resources/,主程序中的资源在前面已经加载过了。
  10. context.addServletContainerInitializer(entry.getKey(), entry.getValue())。将ServletContainerInitializer的配置信息赋予Context。

Tomcat Web Application Deployment