Tomcat源码阅读系列(五)Catalina容器

本文是Tomcat源码阅读系列的第五篇文章,本系列前四篇文章如下:
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码
Tomcat源码阅读系列(二)Tomcat总体架构
Tomcat源码阅读系列(三)启动和关闭过程
Tomcat源码阅读系列(四)Connector连接器
本文首先介绍Catalina容器中的关键类,然后会绘制出Catalina容器的时序图。时序图分为两部分,第一部分为Catalina容器的初始化的过程,第二部分是Catalina容器处理请求的过程。根据两个时序图对其中比较关键的点进行分析和介绍。

第一次使用Markdown,效果不错!不过,竟然把之前保存的文章重新发布了一遍,导致我六月份的文章,重新编辑修改时变成了九月份的文章,罪过罪过!

1 Catalina中关键类及配置说明

1.1 Catalina中关键类

其中的关键类主要包括Server、Service、Engine、Host、Context、Wrapper和Valve七个接口以及对应的实现类StandardServer、StandardService、StandardEngine、StandardHost、StandardContext、StandardWrapper和一些列的Valve实现。

  • org.apache.catalina.Server接口表示了整个catalina的servlet引擎,囊括了所有的组件。server使用一种优雅的方法来启动/停止整个系统,不需要对connector和container分别启动/关闭。当启动server时,server会负责启动其所有的组件,然后就等待关闭命令。server使用service来获取组件,如connector和container。
    org.apache.catalina.core.StandardServer类是Server接口的标准实现。其中的shutdown方法是最重要的,用于关闭server。该类中的许多方法用于将server配置信息保存为一个新的server.xml文件。server中可以有0个或多个service。StandardServer提供了addService,removeService和findServices方法用于对service进行操作。与StandardServer的生命周期有关的方法有:initialize,start,stop和await。与其他组件类似,initialize用于初始化,start用于启动,然后调用await等待关闭命令,最后,调用stop关闭server。调用await方法后,server会被阻塞,直到总8085端口(或其他端口,自定)收到了关闭命令。当await命令返回后,stop方法会关闭所有的子组件。
  • org.apache.catalina.Service接口表示了一个service。一个service可以持有一个container和多个connector,所有的connector都会与这个container相关联。
    org.apache.catalina.core.StandardService类是Service接口的标准实现。StandardService的initialize方法用于初始化添加到其中的connector。StandardService类还实现Lifecycle接口,因此,它也可以启动connector和container。其中container只有一个,connector可以有多个。多个connector使用tomcat可以多种不同的请求协议。例如,一个处理http请求,一个处理https请求。
  • Engine:表示整个Catalina的servlet引擎。
  • Host:表示一个拥有数个上下文的虚拟主机。
  • Context:表示一个Web应用,一个context包含一个或多个wrapper。
  • Wrapper:表示一个独立的servlet 每一个概念之上是用org.apache.catalina包来表示的。

Engine、Host、Context和Wrapper接口都实现了Container接口,所以通常意义上的Catalina容器是不包括Server和Service的,本文在介绍时也主要介绍Engine、Host、Context和Wrapper接口。综上可知Engine、Host、Context和Wrapper接口的关系如下,

一个Server可以包含多个Service,而一个Service可以包含多个Connector和一个Engine,一个Engine可以包含多个Host,一个Host可以包含多个Context,一个Context可以包含多个Wapper。

1.2 Catalina中关键配置说明

Engine、Host、Context和Wrapper分别起着什么样的作用呢,基本上我们在使用Tomcat时所遇到的虚拟目录、多域名、多个Http监听端口问题,都可以从Server.xml的配置上修改得到。

<Server port="8005" shutdown="SHUTDOWN">
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
    <Listener className="org.apache.catalina.core.JasperListener" />
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
    <GlobalNamingResources>
        <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>

    <Service name="Catalina">
        <Connector port="8088" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
        <Engine name="Catalina" defaultHost="localhost">
            <Realm className="org.apache.catalina.realm.LockOutRealm">
                <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
            </Realm>
            <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
                <Alias>www.test3.com</Alias> 
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
                <Context path="" docBase="D:\TomCat7.0\testapp\TestWebliu" />  
            </Host>
            <Host name="www.test1.com"  appBase="testapp"
            unpackWARs="true" autoDeploy="true">
            </Host>
        </Engine>
    </Service>

    <Service name="Catalina2">
        <Connector port="880" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
        <Connector port="8010" protocol="AJP/1.3" redirectPort="8443" />
        <Engine name="Catalina2" defaultHost="localhost">
            <Realm className="org.apache.catalina.realm.LockOutRealm">
                <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
            </Realm>
            <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
                <Context path="" docBase="D:\TomCat7.0\testapp\TestWebliu" />  
            </Host>
            <Host name="www.test2.com"  appBase="testapp"
            unpackWARs="true" autoDeploy="true">
                <Context path="/TestWeb2" docBase="D:\TomCat7.0\testapp\TestWeb2" />  
                <Context path="" docBase="D:\TomCat7.0\testapp\TestWebliu" />  
            </Host>
        </Engine>
    </Service>

</Server>

该server下面两个service节点,每个service节点可以用Connector配置一个监听端口,service里有只能有一个Engine节点,它接受同级目录Connector端口的请求,可以通过defaultHost属性默认指向一个Host,Host就是一个该Connector端口下的域名,下面可以用Context配置多个虚拟目录。即:

  • Server下面可以有多个Service,用于配置不同监听端口
  • Service下面可以有多个Host,用于配置该端口下的不同域名
  • Host里可以包含多个Context,用于配置该端口该域名下的不同虚拟目录

上面的配置,可以用下面链接访问:

http://127.0.0.1:8088/
http://www.test3.com:8088/
http://www.test1.com:8088/TestWeb2/
http://www.test2.com:880/
http://127.0.0.1:880/

本地配置Hosts信息如下,

127.0.0.1 www.test1.com
127.0.0.1 www.test2.com
127.0.0.1 www.test3.com

需要注意点如下:

  1. Host下的Alias是一个域名别称,可以配置多个域名。
  2. Host里如果没有Context节点,则非webapps文件夹下,直接不用虚拟目录访问,会抛404找不到文件的错误,比如上面配置中www.test1.com域名下必须用虚拟目录比如这里的TestWeb2来访问。webapps为默认Context目录
    Host下面的context节点配置为:<Context path=”” 后,则这个目录为默认的访问目录,比如http://www.test2.com:880/,访问的D:\TomCat7.0\testapp\TestWebliu下面的文件。
  3. 这里因为我有默认首页,因此直接输入域名和端口可以直接访问到默认的index.jsp页面,如果没有默认页,可能会抛错,需要配置可以/为列目录,在conf/web.xml里,把这个的param-value改为true即为可以列目录了。
    <init-param>
        <param-name>listings</param-name>
        <param-value>true</param-value>
    </init-param>
  4. 如果不配置server.xml文件,直接把网站放到webapps,默认会自动加载该网站,因此大多数情况,我们都不用对server.xml文件做任何修改。

对工程的部署一般是将工程的压缩文件放在tomcat安装目录的webapps下,访问时通过键入:http://localhost:8080/xx(假定为本机访问,xx是部署时的应用工程的访问名字)。 而如果直接键入:http://localhost:8080出来的将是tomcat自带的欢迎页面,如何让键入http://localhost:8080出来的是自己的应用工程的页面呢?
在Tomcat默认安装后,tomcat的主目录是webapps/root目录,所以如果想改变tomcat的主目录的话可以如下所做:
第一种:(假设tomcat安装在C盘下,项目名为bidding)打开C:/Tomcat/conf/server.xml,在之间加入代码:这样重新启动tomcat,我们的主目录就被设置为bidding这个项目了。
第二种:将tomcat安装目录下的ROOT下的所有文件全部删除,然后将工程的解压后的文件全部拷进去。
第三种:Tomcat5.0以下版本在C:/Tomcat/conf/Catalina/localhost目录下会自动生成了一个ROOT.Xml,但是5.0以上版本不再生成此文件,所以可以新建个ROOT.xml,在里面加入如下代码:

2 Catalina容器的初始化过程

2.1 Catalina容器初始化时序图

Catalina容器的初始化过程
以上便是Catalina容器的初始化过程的时序图,主要涉及到Server.xml和web.xml两大关键的xml配置文件,在解析Server.xml文件时,创建StandardEngine、StandardHost和StandardContext对象,在解析web.xml结点时创建StandardWrapper对象,对于每个Servlet结点都有一个StandardWrapper对象与其对应,保存其原始信息。需要注意的几点是,

  1. 在初始化过程中大量使用了观察者模式,如在StandartHost.start()方法中调用了lifecycle.fireLifecycleEvent(START_EVENT, null)方法,改方法实现为观察者模式的典型实现。在观察启动详情时,需要注意其中观察者实例的调用,如StandardHost.start()方法便通过该方式调用了HostConfig.deployDirectory()方法,从而创建了StandardContext实例。
  2. 创建过程中大量使用了反射,如context = (Context) Class.forName(contextClass).newInstance();等,会给代码的追踪造成一定的影响。
  3. 要理解初始化,首先要理解我们请求的URL信息,如http://www.test1.com:8088/TestWeb2/这样的请求,主要包含了Connector端口信息8088,主机名host信息www.test1.com,Context信息TestWeb2,所以在初始化时,一定要保证Tomcat在得到这个请求时能够处理的了。Connector不必多说,在Tomcat源码阅读系列(四)Connector连接器一文当中已经进行了说明。在这里主要注意Tomcat是如何根据URL来确定某个Servlet的,这就引出了下一节主题Mapper。

2.2 Mapper的初始化

Mapper的初始化
Mapper中关键内部类如下:

    protected abstract static class MapElement<T> {
        public String name = null;  //名字
        public T object = null;  //对应的对象,例如是host对象,context对象或者wrapper对象啥的
    }
    // ------------------------------------------------------- Host Inner Class
    protected static final class MappedHost  //对hostmap信息
        extends MapElement<Host> {
        public ContextList contextList = null;   //有一个contextlist
    }
    // ------------------------------------------------ ContextList Inner Class
    protected static final class ContextList {  //在mappedhost里面将会用其来存拥有的context的信息
        public MappedContext[] contexts = new MappedContext[0];  //mappedcontext对象的数组
        public int nesting = 0;   //所有的context的path中,最多的斜线数目
    }
    // ---------------------------------------------------- Context Inner Class
    protected static final class MappedContext extends MapElement<Context> {  //对context的map的信息
        public ContextVersion[] versions = new ContextVersion[0];  //版本的数组
    }
    protected static final class ContextVersion extends MapElement<Context> {  //某个context的某个版本的具体信息
        public String path = null;   //path
        public String[] welcomeResources = new String[0];   //welcome的数据
        public WebResourceRoot resources = null;   //操作当前web应用程序的资源
        public MappedWrapper defaultWrapper = null;   //默认的wrapper
        public MappedWrapper[] exactWrappers = new MappedWrapper[0];    //对wrapper的精确的map
        public MappedWrapper[] wildcardWrappers = new MappedWrapper[0];   //基于通配符的map
        public MappedWrapper[] extensionWrappers = new MappedWrapper[0];   //基于扩展名的map
        public int nesting = 0;   // 属于这个context的所有servlet的path里面最大斜线数目
    }
    // ---------------------------------------------------- Wrapper Inner Class
    protected static class MappedWrapper  //对wrapper对象的map信息
        extends MapElement<Wrapper> {
        public boolean jspWildCard = false;
        public boolean resourceOnly = false;
    }

其内部使用字典排序对Host名字、Context路径和Servlet路径,进行排序。因为是有序的序列,所以在查找某个Servlet时,直接使用O(logn)的二分查找法。具体实现的解析可以参考Tomcat源码阅读之Mapper分析,在此不多说。

3 Catalina容器请求处理过程

3.1 Catalina容器请求处理过程时序图

Catalina容器请求处理过程时序图
CoyoteAdapter是适配器模式的典型应用。其起到了承上启下的作用,将Connector的请求交给Catalina处理。其中

connector.getContainer().getPipeline().getFirst().invoke(request, response);

便是Connector的请求交给Catalina处理的交界处,因此Catalina容器的对HTTP请求的处理过程也就从这里开始,
通过pipeline链式调用机制最终调用了Servlet对象,而对于pipeline其实是运用了责任链模式,它将各个阀门链接起来,然后一步步的调用,而至于有多少个阀门(Valve)对象,主要来源于两个地方,一个是conf/server.xml中配置的valve,我们知道所有的容器都是支持pipeline机制的,另外一个就是每一个容器的构造其中自己初始化的阀门对象。对于StandardEngine来说有一个与之对应的StandardEngineValve,对于StandardHost有一个StandardHostValve与之对应,StandardContext有一个StandardContextValve与之对应,StandardWrapper与StandardWrapperValve对应,通过分析代码,我们可以得到如下的一个调用链。

->org.apache.catalina.core.StandardEngineValve#invoke
-->org.apache.catalina.valves.ErrorReportValve#invoke
--->org.apache.catalina.core.StandardHostValve#invoke
---->org.apache.catalina.authenticator.AuthenticatorBase#invoke
----->org.apache.catalina.core.StandardContextValve#invoke
------>org.apache.catalina.core.StandardWrapperValve#invoke
------->org.apache.catalina.core.ApplicationFilterChain#doFilter
-------->javax.servlet.http.HttpServlet#service

最后Valve调用org.apache.catalina.core.ApplicationFilterChain#doFilter,doFilter方法中会根据filterConfig中取的web.xml配置的过滤器,然后一个个调用,等每个过滤器执行完了以后,最终就会调用到Servlet的Service方法。

文章比较长,内容是在太多,也就说这些吧,有兴趣的可以读一下源码

猜你喜欢

转载自blog.csdn.net/yangzl2008/article/details/46686765