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解析的过程中

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 前言由于换工作的原因,需要融入新的开发团队,开展新的业务征途,因此,距离上一次更新博客已有一段时间,现在稍微稳定下...
    宝之家阅读 1,282评论 0 2
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,137评论 11 349
  • 文/白乌鸦 第欧根尼抬起头 揉揉惺忪的睡眼 太阳要翻过皇宫了 他嘴里不停咒骂着凯撒 上午凯撒又来打断他的好梦 他差...
    南倚闲坐阅读 174评论 0 0