Tomcat架构中各个组件及组件间关系

前言
借着上次对Tomcat类加载机制的分析,就想着看都看了,何不再看看Tomcat内部的实现原理和架构设计,向优秀的源码学习。Tomcat相较于其他的web容器,比如Jetty,要更加的复杂,内部应用了很多优秀的设计模式和思想,一上来就一头扎进源码进行分析并不是特别好的学习方式,因此,本文在借鉴其他文章、书籍的基础上,从大家都熟悉的server.xml配置文件入手,循序渐进的分析。文章主要的篇章布局是:

  • 分析server.xml配置文件中的常用标签,引出Tomcat中的对应组件
  • 给出比较全面的Tomcat架构图,和上面的分析相互印证
  • 从源码的角度分析组件是如何被Tomcat所加载的
    由于Tomcat相关的内容比较繁杂,很难在一篇文章内讲清楚所有重要的内容,因此,本文重点在于核心组件的“静态”分析,在代码层面类与类之间是如何组合的。本文依然依赖于Tomcat7版本的源码,读者需要先搭建对应的测试环境

1. Tomcat配置文件server.xml标签解析

相信大部分Javaer都用过Tomcat作为web容器,那么对于其中的server.xml肯定也不陌生,其中配置了Tomcat启动要加载的各种组件

<Server port="8005" shutdown="SHUTDOWN">
    <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
    <!-- Security listener. Documentation at /docs/config/listeners.html
    <Listener className="org.apache.catalina.security.SecurityListener" />
    -->
    <!--APR library loader. Documentation at /docs/apr.html -->
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
    <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->
    <Listener className="org.apache.catalina.core.JasperListener"/>
    <!-- Prevent memory leaks due to use of particular java/javax APIs-->
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>

    <!-- Global JNDI resources
         Documentation at /docs/jndi-resources-howto.html
    -->
    <GlobalNamingResources>
        <!-- Editable user database that can also be used by
             UserDatabaseRealm to authenticate users
        -->
        <Resource name="UserDatabase" auth="Container"
                  type="org.apache.catalina.UserDatabase"
                  description="User database that can be updated and saved"
                  factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
                  pathname="conf/tomcat-users.xml"/>
    </GlobalNamingResources>

    <!-- A "Service" is a collection of one or more "Connectors" that share
         a single "Container" Note:  A "Service" is not itself a "Container",
         so you may not define subcomponents such as "Valves" at this level.
         Documentation at /docs/config/service.html
     -->
    <Service name="Catalina">

        <!--The connectors can use a shared executor, you can define one or more named thread pools-->
        <!--
        <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
            maxThreads="150" minSpareThreads="4"/>
        -->


        <!-- A "Connector" represents an endpoint by which requests are received
             and responses are returned. Documentation at :
             Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
             Java AJP  Connector: /docs/config/ajp.html
             APR (HTTP/AJP) Connector: /docs/apr.html
             Define a non-SSL HTTP/1.1 Connector on port 8080
        -->
        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443"/>
        <!-- A "Connector" using the shared thread pool-->
        <!--
        <Connector executor="tomcatThreadPool"
                   port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443" />
        -->
        <!-- Define a SSL HTTP/1.1 Connector on port 8443
             This connector uses the BIO implementation that requires the JSSE
             style configuration. When using the APR/native implementation, the
             OpenSSL style configuration is required as described in the APR/native
             documentation -->
        <!--
        <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
                   maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
                   clientAuth="false" sslProtocol="TLS" />
        -->

        <!-- Define an AJP 1.3 Connector on port 8009 -->
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>


        <!-- An Engine represents the entry point (within Catalina) that processes
             every request.  The Engine implementation for Tomcat stand alone
             analyzes the HTTP headers included with the request, and passes them
             on to the appropriate Host (virtual host).
             Documentation at /docs/config/engine.html -->

        <!-- You should set jvmRoute to support load-balancing via AJP ie :
        <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
        -->
        <Engine name="Catalina" defaultHost="localhost">

            <!--For clustering, please take a look at documentation at:
                /docs/cluster-howto.html  (simple how to)
                /docs/config/cluster.html (reference documentation) -->
            <!--
            <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
            -->

            <!-- Use the LockOutRealm to prevent attempts to guess user passwords
                 via a brute-force attack -->
            <Realm className="org.apache.catalina.realm.LockOutRealm">
                <!-- This Realm uses the UserDatabase configured in the global JNDI
                     resources under the key "UserDatabase".  Any edits
                     that are performed against this UserDatabase are immediately
                     available for use by the Realm.  -->
                <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                       resourceName="UserDatabase"/>
            </Realm>

            <Host name="localhost" appBase="webapps"
                  unpackWARs="true" autoDeploy="true">

                <!-- SingleSignOn valve, share authentication between web applications
                     Documentation at: /docs/config/valve.html -->
                <!--
                <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
                -->
                <Context path="" docBase="www/" reloadable="true" /> 

                <!-- Access log processes all example.
                     Documentation at: /docs/config/valve.html
                     Note: The pattern used is equivalent to using pattern="common" -->
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                       prefix="localhost_access_log." suffix=".txt"
                       pattern="%h %l %u %t "%r" %s %b"/>

            </Host>
        </Engine>
    </Service>
</Server>

从上面标准的server.xml中可以看出,<Server>作为顶层标签,下面的子标签有<Listener><GlobalNamingResources><Service>三个,我们猜测Tomcat中必定有一种类对应<Server>标签,同时也会存在三种类 (为什么不说三个类,因为可能存在一对多的关系) 对应下面的子标签,而父子之间的关系可能通过组合的关系联系在一起。同样的,也可以推理出存在<Resource><Executor><Connector><Cluster><Realm><Host><Context><Valve>这几个标签对应的类,他们之间的关系也可以根据标签之间的“父子”关系推断出来

2. Tomcat整体架构图

我找了一张比较完整的Tomcat架构图,通过真正的抽象化架构来评判上面我们推断的合理性,有什么遗漏的地方,或错误的地方

图1. Tomcat架构

从图中可以看到,大部分的组件都与server.xml中标签有着对应关系,比如<server>对应图中的Server组件,该组件是Tomcat的顶层组件,其中包含一个或者多个Service组件,正如<Server>中包含一个或多个<Service>子标签一样,但即便如此,我们仍需要着重看一下代码层面的实现,毕竟这才是验证理论最可靠的途径

3. 代码层面实现

首先我们要看一下server.xml是如何被加载进Tomcat容器中,在违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制中,我们知道了Tomcat的是通过Bootstrap.javamain(String args[])启动的,代码清单1

public static void main(String args[]) {

        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {

                //     (1)
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            //      (2)
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to prevent
            // a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }

        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } 
            //      (3)
            else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null==daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }

注释1处,main方法内首先调用了init()方法,在该方法中使用反射创建了org.apache.catalina.startup.Catalina类的实例,并将该实例赋值给了Bootstrap类中的catalinaDaemon实例,默认启动Tomcat容器流程会走到注释3处,调用daemon.load(args)方法,这里的daemon实例其实就是注释2处的Bootstrap自己的实例,我们接着看load(String[])方法

图2. Bootstrap类的load(String[])方法

该方法的主要逻辑就是通过反射调用了成员变量catalinaDaemonload(String args[])方法,上面说过,catalinaDaemon实际上就是Catalina.class的实例对象,因此,最终调用了Catalina类的load()方法,代码清单2

public void load() {

        long t1 = System.nanoTime();

        initDirs();

        // Before digester - it may be needed

        initNaming();

        //         (1)
        // Create and execute our Digester
        Digester digester = createStartDigester();

        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        try {
            try {
                file = configFile();
                inputStream = new FileInputStream(file);
                inputSource = new InputSource(file.toURI().toURL().toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail", file), e);
                }
            }
            //    省略其他代码.....
            try {
                inputSource.setByteStream(inputStream);
                 //         (2)
                digester.push(this);
                //          (3)
                digester.parse(inputSource);

            } catch (SAXParseException spe) {
                log.warn("Catalina.start using " + getConfigFile() + ": " +
                        spe.getMessage());
                return;
            } catch (Exception e) {
                log.warn("Catalina.start using " + getConfigFile() + ": " , e);
                return;
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }

        getServer().setCatalina(this);

        // Stream redirection
        initStreams();

        // Start the new server
        try {

            //             (4)
            getServer().init();
        } catch (LifecycleException e) {
            if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
                throw new java.lang.Error(e);
            } else {
                log.error("Catalina.start", e);
            }

        }

        long t2 = System.nanoTime();
        if(log.isInfoEnabled()) {
            log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
        }

    }

Tomcat底层使用SAX来对xml文件进行解析,具体来说,注释1处createStartDigester()方法的目的是为解析server.xml创建特定的“摘要”,Tomcat采用Digester.java来封装对server.xml文件中所有标签的解析规则,每一种规则都是Rule接口的实现,Digester.java中的startDocument()startElement(String,String,String,Attributes)endDocument()endElment(String,String,String)等方法都是标准的SAX解析模块,分别用于文档的开始结束、元素的开始结束。为了突出重点,该流程我们采用一个为<Server>标签设置解析规则的例子说明

图3. createStartDigester方法节选

红框内的代码实际上为解析<Server>标签创建了三个规则ObjectCreateRuleSetPropertiesRuleSetNextRule,并指明了<Server>对应对象的实例为org.apache.catalina.core.StandardServer,这三个规则最终会被放在规则父类RuleBase类的缓存HashMap<String,List<Rule>> cache中,而Digester又持有该类的实例,也就是说Digester最终会装载解析xml文件所需的所有规则
我们回到代码清单2中的注释2,Digester做了一个类似压栈的操作,将当前的Catalina对象压入Catalina类中的ArrayStack<Object> stack中,根据栈先进后出的特性可知该Catalina对象必定会最后一个弹栈,而栈中存放的其他对象实际上就是上面对应标签的java类实例,举个例子,如果server.xml中标签的结构为

<Server>
      <Service>
      </Service>
</Server>

那么最后栈中的结构必然是先入栈Catalina实例,然后是<Server>标签对应类的实例,栈顶的是<Service>标签的实例。为什么要用这种设计思路存放标签对应的类实例,我理解可以想一想SAX方式解析xml文件的特点,SAX对xml文件边扫描边解析,自顶向下依次解析,可以看成是深度优先遍历的一种变体,该特性在数据结构的层面上正好用栈完美诠释,这里又为什么要将“自己”压入栈底,答案随着分析的深入自会揭晓,现在只需要记住
代码清单2中的注释3,此处通过摘要类的实例对已经加载为输入流形式的server.xml进行了解析,上面说过Digester作为SAX的解析类,当解析到Docuemt开始会调用startDocument()方法,解析到Element开始会调用startElement()方法,我们来看一下

图4. Digester的startElment方法

因为SAX解析会将每一个标签映射成一个Element,红框内的代码主要是在标签解析的时候筛选出之前为对应标签配置的规则,比如当解析到<Server>标签时,会从上面所说的标签cache中得到为其所配置的ObjectCreateRuleSetPropertiesRuleSetNextRule三个规则,然后依次调用对应规则的begin方法,同样的Digester在解析到标签的结尾时会调用endElment()方法,在该方法中也会有遍历所有规则的流程,与处理标签开始不同的是,结束时会依次调用规则的end方法,这里我们仅以ObjectCreateRulebegin方法举例

图5. ObjectCreateRule的begin方法

图中classNamerealClassName实际上就是图3中的org.apache.catalina.core.StandardServer,所以<Server>标签实际上就生成了StandardServer.java的实例,从而建立了标签和类的对应关系,同时将StandardServer实例压栈
代码清单2中的注释4代码主要进行各个容器的初始化工作,具体的初始化流程在下一篇讲述容器生命周期的文章中详述,这里一笔带过。但是有一个问题,就是这里的getServer()方法返回了Catalina类中的protected Server server = null;,这个Server实际上就是上面创建的<Server>标签对应的实例StandardServer,问题是Tomcat是何时将这个初始值为null的Server赋值的呢?
有人肯定会说肯定会调用该变量的setServer(Server)方法啊,在Catalina类中确实存在setServer(Server)方法,但查询其调用链时发现该方法并没有被直接调用过,那这个Server是如何被赋值的呢?我们要重新看看在解析<Server>标签时Rule起了什么作用,ObjectCreateRule主要生成标签对应的类的实例,并将其压栈;SetPropertiesRule主要用于标签参数的解析;SetNextRule处理父子标签对应类方法的调用,建立标签实体之间的关联

图6. 解析</Server>标签时SetNextRule的end方法

为了调试方便,我们对server.xml中的内容进行了修改,只保留了顶层的<Server>标签,从调试截图可见,此时栈顶元素为StandardServer,栈底元素为Catalina,待调用的方法名称为setServer,最后通过内省工具类Object callMethod1(Object, String, Object, String, ClassLoader) throws Exception完成了层级关联关系的映射,图中就是用Catalina实例调用了他的setServer(Server)方法,其传入的Server就是StandardServer的实例。至此完成了server.xml文件中组件的解析,最后我们以<Server>标签和<Service>标签为例看一看代码层面的表现形式

图7. StandardServer类中的Service数组

图8. StandardService类中Server对象

Tomcat中各组件在类层面上的关系基本如图7、图8所示,层级关系表现为双向的关联关系,数量关系表现为数组对象的引用,其主要的思想还是内含在对server.xml解析的过程中

推荐阅读更多精彩内容