http://blog.163.com/haizai219@126/blog/static/4441255520097162481811/
Tomcat6.0源码学习-构建Eclipse源码工程
摘要
对于学习j2ee,且想提高自己Java编程设计水平的每个人来说,研究Tomcat的源码是一件很向往的事,这其中的获益不仅仅是Java编程水平的提高,还有很多其他方面,如j2ee规范的了解,设计模式的运用,产品构建及版本控制等等,这些个方面无一不是我们学习和模拟的对象。但是,当我们开始这段旅程时,往往会迷失于浩瀚的代码海洋之中,这个时候,我们多么希望有这么一盏明灯,指引我们向前进。
在Eclipse中构建Tomcat6.0源码工程
毫不夸张的说,Tomcat 6项目是最友好的项目,原因是它的源码工程构建起来很是方便,它没有使用很先进的maven工具来管理工程,也没有使用通用的IDE来构建工程,而它其实就是一个Eclipse的Java工程,构建时只使用ant。从这一点来看,好像特定了IDE,对贡献者的开发环境要求限制了,但是它却吸引了更多的贡献者。毕竟,像我这样的程序员还是很多,一没钱机子烂,通常只用Eclipse开发,看见Eclipse工程很亲切
好了,如前文所说,Tomcat 6项目就是一个Eclipse的Java工程,那么我们直接用Eclipse中的SVN插件上
http://svn.apache.org/repos/asf/tomcat/tc6.0.x/tags/TOMCAT_6_0_20/导出工程到本地的workspace中。
如果有错,那就是因为classpath中找不到ANT_HOME变量和TOMCAT_LIBS_BASE,重新设置一下ANT_HOME和TOMCAT_LIBS_BASE,
如ANT_HOME=D:\JavaTeam\apache-ant-1.7.1,TOMCAT_LIBS_BASE=D:\JavaTeam\eclipse。
如果你没有SVN插件,也没关系,在http://tomcat.apache.org/download-60.cgi下载Tomcat 6.0.20的源码包,然后解压到workspace。
由于源码包没有.project文件和.classpath文件,所以我们要制作一个,什么?制作太麻烦,好吧,那下载一个,在http://svn.apache.org/repos/asf/tomcat/tc6.0.x/tags/TOMCAT_6_0_20/目录下,把.project文件和.classpath文件保存到解开的源码目录(apache-tomcat-6.0.20-src)下,然后,在Eclipse中导入该工程。ok,这下跟刚才的过程一样了。
Tomcat6.0源码学习--启动框架
1.摘要
对于独立运行的应用程序来说,都有一个入口,以便启动应用程序。Java应用程序的入口是类的main方法,在这里你可以初始化应用的上下文环境,然后创建应用组件并提供服务。对于简单的应用程序,可以直接将启动代码放在main方法中,但是,对于复杂的,或者可扩展的应用来说,这样做是不负责任的,也是不优雅的。那么,怎么做才是负责任的,优雅的?下面我们看看Tomcat是如何启动的,分析一下它的启动框架。希望从中能找到答案。
2.Tomcat启动框架
默认的情况下,在命令行下启动Tomcat,程序的入口是org.apache.catalina.startup.Bootstrap类的main方法,Bootstrap是一个final类,不允许扩展。下面看看main方法,
publicstaticvoid main(String args[]) { if (daemon ==null) daemon =new Bootstrap(); try { daemon.init(); } catch (Throwable t) { t.printStackTrace(); return; } }
try { String command = if (args.length > 0) { command = args[args.length - 1]; }
if (command.equals("startd")) { args[0] = daemon.load(args); daemon.start(); } elseif (command.equals("stopd")) args[0] = daemon.stop(); } elseif (command.equals("start")) daemon.setAwait(true); daemon.load(args); daemon.start(); } elseif (command.equals("stop")) daemon.stopServer(args); } else { log.warn("Bootstrap: command \"" + command +"\" } } catch (Throwable t) { t.printStackTrace(); } } |
由代码可知,main方法很简单,就做2件事,一.实例化和初始化Bootstrap,并缓存起来,二.处理命令行参数,根据参数调用不同的操作,此之谓引导。看一下序列图更加清晰:
2.1引导过程
一般来说,在使用一个对象之前,要先初始化好这个对象的状态(就是属性的值),而Bootstrap的初始化也无非干了这些事。
首先初始化属性commonLoader,catalinaLoader,sharedLoader的值,在初始化的过程中先设置了环境变量${catalina.home}和${catalina.base},然后读取配置文件,创建ClassLoader并赋值给属性字段。
接着使用catalinaLoader加载org.apache.catalina.startup.Catalina类并实例化一个对象,并将该对象设置到属性catalinaDaemon中。
当Bootstrap对象初始化完了,就可以接受命令并引导服务了。不同的命令对应不同的Bootstrap动作,而Bootstrap动作委托给Catalina对象来处理。
2.2Catalina
我们知道,Tomcat是面向组件设计的典范,而且组件的装配是可配置的,那么在Tomcat实例启动之前,必须有一个地方来装配这些个组件。Catalina类正好担此重任。Catalina主要负责Tomcat的装配,并在Bootstrap的引导下操作Tomcat实例。
当Bootstrap接到“start”命令后,会引导Catalina对象的load动作装配一个新的Server实例,接着Bootstrap引导Catalina对象启动Server实例。由此可以看出,Catalina是Tomcat实例的装配工厂和引线。
Tomcat6.0源码学习--架构概述
Tomcat6是最新版本的web容器,其支持最新版本的servlet2.5和jsp2.1。而且Tomcat6架构也是经过重新设计优化过的,所以我们有必要分析一下它的架构过程。显然,这是一个通过阅读Tomcat的源代码及相关文档,演绎架构的过程。或许有人会说,这不是放马后炮吗?!!但我觉得这是自我进步的一个必经步骤,先模仿之,然后才能超越之,毕竟我本凡人。(模仿并超越)
Tomcat的架构总的来说是分层次的、可插拔的组件架构。分层次是指构成Tomcat的组件不是同一级别的,上层组件可以包含子组件,各个组件有其功能范围,当一个组件停止服务时,不会影响上层组件的服务。可插拔是指对于组件的添加和删除并不影响服务器的运行。那么为了达到可插拔的组件架构,分层次的组件架构必成为基础。
对于任何服务器,即使最简单的实现,从面向对象设计(OOD)的角度来说,我们都有必要将“服务器”这个概念抽象出来,为什么呢?因为只有有了这个概念,才能谈服务器的实例,服务器的功能等等其它概念,此之谓“皮之不存,毛将焉附”。赶巧(其实是我的想法恰好撞上人家的想法),Tomcat也将“服务器”抽象为java接口org.apache.catalina.Server,显然Server应该就是最最顶层的组件了。
有了Server这个抽象,很自然的,我们希望它能够提供对servlet和jsp支持的功能。但是我们发现这个概念太大了,我们还需再细化。所以别急,我们还有一些事情要解决。服务器要提供服务就必须能够启动,当然也应该能够停止吧,也就是说服务器应该是有生命的,在启动时初始化必要的资源,而在停止时将其销毁掉。好吧,我们把这个也抽象出来,叫做生命周期接口,tomcat实现为org.apache.catalina.Lifecycle.如上所述我们知道Lifecycle需要完成的工作了。
publicvoidstart() publicvoid stop()throws LifecycleException; |
接下来我们分析服务器如何来处理客户端的请求,一般的我们会在浏览器中输入如下格式的请求,http://192.168.8.221:8080/explorer/loginInit.do。对于服务器来说,要想处理这个请求,就必须监听指定的端口8080,当有TCP的请求包来时,建立Socket连接,分析并解析之,然后给客户端返回响应。在这个过程中,我们发现,其实包含了俩个功能点,即监听并接受请求和处理请求。那么我们能否将这俩个功能给抽象出来呢?Tomcat告诉我们,可以。是的,Tomcat将“监听并接收请求”抽象为org.apache.catalina.connector.Connector类,负责接受请求;将“处理请求”抽象为“容器”
org.apache.catalina.Container,负责处理Connector传递过来的请求。
Ok,到此,我们分析构建的简单服务器模型出来了,Server由Connector组件和Container组件结合提供web服务。
图2
有了这个模型后,要实现一个简单的Server已经很简单了,但是在实现Container时,我们还是要做很多事情,如当来请求,我们怎么知道该请求对应得虚拟主机,以及请求的那个应用,应该交给那个servlet对象来处理?这样看来,Container还是太大了,需要细化。根据Servlet规范,我们知道,servlet属于某个应用,且有上下文环境,Container要根据应用上下文环境初始化servlet,然后根据servlet映射调用servlet的service方法。在这里“应用上下文环境”的概念很重要,Tomcat将其抽象为org.apache.catalina.Context,Context继承了Container接口。对于虚拟主机,Tomcat将其抽象为org.apache.catalina.Host,Host继承了Container接口。
好了,有了这些概念,我们再回顾一下请求的处理过程:浏览器发出请求,Connector接受请求,将请求交由Container处理,Container查找请求对应的Host并将请求传递给它,Host拿到请求后查找相应的应用上下文环境Context,准备servlet环境并调用service方法。
现在,我们的服务器模型变成了如图3所示了。
图3
但是在Tomcat的实现体系中还有一个Engine的接口,Engine也继承了Container接口,那么这个接口什么用呢?设计Engine的目的有俩个,一,当希望使用拦截器查看(过滤或预处理)每个请求时,Engine是个很好的拦截点。二,当希望多个虚拟Host共享一个Http的Connector时,Engine是个很好的门面。所以,Engine接口是作为顶级Container组件来设计的,其作用相当于一个Container的门面。有了Engine,请求的处理过程变为:浏览器发出请求,Connector接受请求,将请求交由Container(这里是Engine)处理,Container(Engine来担当)查找请求对应的Host并将请求传递给它,Host拿到请求后查找相应的应用上下文环境Context,准备servlet环境并调用service方法。再看看服务器的模型,如图4.
图4
到目前,我们碰到的组件类型有Connector和Container,其实,这也就是Tomcat的核心组件。如图4,一组Connector和一个Container有机的组合在一起构成Server,就可以提供服务了,对于Tomcat来说,主要是提供Servlet服务,那么也就是说Tomcat服务器也可以提供其它服务了?是的,Tomcat将“一组Connector和一个Container有机的组合”抽象为“服务”接口org.apache.catalina.Service,然而,这些服务实例彼此独立,仅仅共享JVM的基础设施,如系统类路径。
进一步的,我们得到了服务器的框架模型,如图5.
图5
由图5,我们知道,对于Tomcat服务器来说,除了Server代表它自己以外,其它组件都是功能组件,都有其职责范围。Service为最顶层的组件,可以添加Connector和Container组件。Engine是Container的最顶层组件,可以添加Host组件,但不能添加父组件。Host组件的父组件是Engine,Host下面包含有Context组件。
接下来看看标准的Tomcat体系结构,如图6.
图6
比较图5和图6.我们发现,还有很多辅助组件没有抽象出来。当然,随着需求的一步步加深,我的分析也会一步步深入,这些个组件也会慢慢浮出水面。
Tomcat6.0源码学习--Connector架构
概述
Tomcat虽然具有传统Web服务器(如Apache)的功能(处理Html页面,css文件等静态资源的能力),但更多的,Tomcat以Servlet容器著称(处理Jsp和Servlet等动态资源的应用服务器)。由Tomcat的总体架构可知(参见:Tomcat6.x架构概述),Servlet容器由2个主要组件构成:Connector(连接器)和Container(容器)。Connector负责接收客户端的请求,而Container处理并响应该请求。
由JavaEE规范可知,Servlet容器内部只处理HTTP协议的请求,但是对于连接器的设计来说,它可以接收任何协议(如HTTP,AJP等)的请求,因此,在连接器接收到客户端的请求(Socket)后,需要将该请求包装成容器可以处理的对象,然后再传递给容器处理,同时,连接器也要创建一个响应对象一并传给容器,好让容器响应请求。如此一来,容器就与具体传输协议解耦了,而这正是Connector架构所要达到的目的。
Connector架构
基于上面的分析,我们可以将连接器和容器分别抽象为Connector和Container接口,将请求和响应抽象为Request和Response接口。
Connector接口中的重要方法有:
public
Request createRequest() ;
public
Response createResponse();
public
Container getContainer();
Tomcat6.x的设计
下面我们看看Tomcat6.x是如何设计的。我们知道,Tomcat的Servlet容器有个好听的名字叫Catalina,在其内部,Catalina将容器抽象为org.apache.catalina.Container接口,将连接器抽象为org.apache.catalina.connector.Connector类,将请求和响应抽象为org.apache.catalina.connector.Request和org.apache.catalina.connector.Response类。
org.apache.catalina.connector.Connector类也有以下方法:
public
Request createRequest() ;
public
Response createResponse();
public
Container getContainer();
Connector接收到客户端的Socket请求,调用createRequest方法创建请求对象(org.apache.catalina.connector.Request),调用createResponse方法创建响应对象(org.apache.catalina.connector.Response),然后调用getContainer方法获得容器,最后传递Request对象和Response对象并调用Container的invoke方法处理请求。
Connector支持的协议
对于Apache(或其它Web服务器)来说,它可以加载用C语言写的Apache模块(mod_jk
, mod_jk2 ,或mod_proxy)作为连接器(Connector),这些连接器(Connector)的工作原理一致,可以根据配置支持不同协议的连接器。对于Tomcat来说,其Servlet容器也有用java语言实现的连接器(Connector)模块,这些模块的工作原理也应该一致并且可以根据配置支持不同的协议。为了实现连接器处理工作的一致性和协议的可配置、可扩展性,在Tomcat6.x中,将一致性的工作抽象到org.apache.catalina.connector.Connector类,而将具体协议的差异抽象到org.apache.coyote.ProtocolHandler接口之中。Tomcat6.x通过在配置文件中设置不同的协议类型来初始化不同的ProtocolHandler实例,从而实现协议的可配置、可扩展性。
在Tomcat6.x中,默认支持2种协议类型的连接器:HTTP/HTTPS协议的HTTP/1.1和AJP协议的AJP/1.3。Tomcat6.x使用Coyote模块来实现Connector框架,因此这些协议处理器(ProtocolHandler)在包org.apache.coyote下可以找到。
注:Coyote是Connector框架实现的名字,在该模块下提供了2种协议类型的ProtocolHandler。
如果想扩展连接器,可以在server.xml文件中指定协议和协议处理器的类名。如下:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
然而,HTTP协议和AJP协议几乎符合所有的需求,因此,我们很少会扩展连接器。
在构造连接器时,连接器根据是否支持Apache Portable Runtime (APR),选择不同的类支持HTTP协议和AJP协议,其对应关系如下:
l 支持APR
HTTP/1.1协议对应org.apache.coyote.http11.Http11AprProtocol类
AJP/1.3协议对应org.apache.coyote.ajp.AjpAprProtocol类
l 不支持APR
HTTP/1.1协议对应org.apache.coyote.http11.Http11Protocol类
AJP/1.3协议对应org.apache.jk.server.JkCoyoteHandler类
注:APR(Apache portable Run-time libraries,Apache可移植运行库)的目的如其名称一样,主要为上层的应用程序提供一个可以跨越多操作系统平台使用的底层支持接口库。在早期的Apache版本中,应用程序本身必须能够处理各种具体操作系统平台的细节,并针对不同的平台调用不同的处理函数。启用APR将大大提升Tomcat对静态文件的处理性能。
下面看看如何构建不同的连接器:
public Connector(String protocol)throws Exception
{
setProtocol(protocol);
// Instantiate protocol handler
try {
Class clazz = Class.forName(protocolHandlerClassName);
this.protocolHandler = (ProtocolHandler)
clazz.newInstance();
}
catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed", e));
}
}
publicvoid setProtocol(String protocol) {
// Test APR support
initializeAPR();
if (aprInitialized) {
if ("HTTP/1.1".equals(protocol))
{
setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
}
else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
}
else if (protocol !=
null) {
setProtocolHandlerClassName(protocol);
}
else {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
}
}
else {
if ("HTTP/1.1".equals(protocol))
{
setProtocolHandlerClassName("org.apache.coyote.http11.Http11Protocol");
}
else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName("org.apache.jk.server.JkCoyoteHandler");
}
else if (protocol !=
null) {
setProtocolHandlerClassName(protocol);
}
}
}
Connector构造函数接收一字符串参数protocol,并根据该字符串加载不同的ProtocolHandler。
ProtocolHandler
每个ProtocolHandler代表着一种协议的支持,如Tomcat6.x默认支持的协议http1.1和ajp,相应的处理器为:Http11AprProtocol,AjpAprProtocol,Http11Protocol,AjpProtocol。ProtocolHandler的职责是,接收客户端Socket请求,处理Socket请求,将请求包装之后传给容器来处理。
Coyote架构下,每个ProtocolHandler使用相应的端点类XXXEndpoint(如org.apache.tomcat.util.net.AprEndpoint,
org.apache.tomcat.util.net.NioEndpoint,org.apache.tomcat.util.net.JIoEndpoint)监听指定端口并接收Socket请求。在请求传给容器之前,我们需要对请求做一些处理(如是否超时,是否保持连接等),而这些工作都是交给具体协议处理器(ProtocolHandler)相的关类来实现,最后由Adapter包装请求并调用容器的invoke方法。
ProtocolHandler相关类
为了实现协议处理器的相关功能,Coyote按功能职责抽象了协议处理器的相关类:
l XXXEndpoint,监听指定端口,接收Socket请求
l Handler,管理具体协议的Socket请求连接
l Processor,真正的具体协议处理器
l Adapter,包装请求,并调用容器的invoke方法(准确的说是Pipeline的第一个Valve)
在ProtocolHandler相关类的配合下,其处理请求的过程如下:
根据支持的协议,通常ProtocolHandler包含一个实现相应协议的Handler实例和XXXEndpoint实例,Handler实例接收XXXEndpoint获得的Socket对象,然后创建相应协议的Processor实例,并将Socket请求交给它来处理,最后Processor调用Adapter的service方法,该方法把请求包装后调用容器的invoke方法。
Adapter的service方法签名如下: