本文是Tomcat源码阅读系列的第五篇文章,本系列前四篇文章如下:
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码
Tomcat源码阅读系列(二)Tomcat总体架构
Tomcat源码阅读系列(三)启动和关闭过程
Tomcat源码阅读系列(四)Connector连接器
本文首先介绍Catalina容器中的关键类,然后会绘制出Catalina容器的时序图。时序图分为两部分,第一部分为Catalina容器的初始化的过程,第二部分是Catalina容器处理请求的过程。根据两个时序图对其中比较关键的点进行分析和介绍。
第一次使用Markdown,效果不错!不过,竟然把之前保存的文章重新发布了一遍,导致我六月份的文章,重新编辑修改时变成了九月份的文章,罪过罪过!
其中的关键类主要包括Server、Service、Engine、Host、Context、Wrapper和Valve七个接口以及对应的实现类StandardServer、StandardService、StandardEngine、StandardHost、StandardContext、StandardWrapper和一些列的Valve实现。
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。 |
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 "%r" %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 "%r" %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配置多个虚拟目录。即:
上面的配置,可以用下面链接访问:
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
需要注意点如下:
对工程的部署一般是将工程的压缩文件放在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,在里面加入如下代码:
以上便是Catalina容器的初始化过程的时序图,主要涉及到Server.xml和web.xml两大关键的xml配置文件,在解析Server.xml文件时,创建StandardEngine、StandardHost和StandardContext对象,在解析web.xml结点时创建StandardWrapper对象,对于每个Servlet结点都有一个StandardWrapper对象与其对应,保存其原始信息。需要注意的几点是,
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 //对host的map信息 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分析,在此不多说。
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方法。
文章比较长,内容是在太多,也就说这些吧,有兴趣的可以读一下源码