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.xml中可以看出,作为顶层标签,下面的子标签有三个,我们猜测Tomcat中必定有一种类对应标签,同时也会存在三种类 (为什么不说三个类,因为可能存在一对多的关系) 对应下面的子标签,而父子之间的关系可能通过组合的关系联系在一起。同样的,也可以推理出存在这几个标签对应的类,他们之间的关系也可以根据标签之间的“父子”关系推断出来

2. Tomcat整体架构图

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

Tomcat架构中各个组件及组件间关系_第1张图片
图1. Tomcat架构

从图中可以看到,大部分的组件都与 server.xml中标签有着对应关系,比如 对应图中的Server组件,该组件是Tomcat的顶层组件,其中包含一个或者多个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[])方法

Tomcat架构中各个组件及组件间关系_第2张图片
图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解析模块,分别用于文档的开始结束、元素的开始结束。为了突出重点,该流程我们采用一个为标签设置解析规则的例子说明

Tomcat架构中各个组件及组件间关系_第3张图片
图3. createStartDigester方法节选

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


      
      

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

Tomcat架构中各个组件及组件间关系_第4张图片
图4. Digester的startElment方法

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

Tomcat架构中各个组件及组件间关系_第5张图片
图5. ObjectCreateRule的begin方法

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

Tomcat架构中各个组件及组件间关系_第6张图片
图6. 解析标签时SetNextRule的end方法

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

图7. StandardServer类中的Service数组

图8. StandardService类中Server对象

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

你可能感兴趣的:(Tomcat架构中各个组件及组件间关系)