Tomcat源码分析篇(转载)
说明:仅供学习,原⽂出⾃
Tomcat源码分析(⼀)--服务启动
1. Tomcat主要有两个组件,连接器和容器,所谓连接器就是⼀个http请求过来了,连接器负责接收这个请求,然后转发给容器。容器即servlet容器,容器有很多层,分别是Engine,
Host,Context,Wrapper。最⼤的容器Engine,代表⼀个servlet引擎,接下来是Host,代表⼀个虚拟机,然后是Context,代表⼀个应⽤,Wrapper对应⼀个servlet。从连接器
传过来连接后,容器便会顺序经过上⾯的容器,最后到达特定的servlet。要说明的是Engine,Host两种容器在不是必须的。实际上⼀个简单的tomcat只要连接器和容器就可以了,
但tomcat的实现为了统⼀管理连接器和容器等组件,额外添加了服务器组件(server)和服务组件(service),添加这两个东西的原因我个⼈觉得就是为了⽅便统⼀管理连接器和
容器等各种组件。⼀个server可以有多个service,⼀个service包含多个连接器和⼀个容器,当然还有⼀些其他的东西,看下⾯的图就很容易理解Tomcat的架构了:
2. ⼀个⽗组件⼜可以包含多个⼦组件,这些被统⼀管理的组件都实现了Lifecycle接⼝。只要⼀个组件启动了,那么他的所有⼦组件也会跟着启动,⽐如⼀个server启动了,它的所有⼦
service都会跟着启动,service启动了,它的所有连接器和容器等⼦组件也跟着启动了,这样,tomcat要启动,只要启动server就⾏了,其他的组件都会跟随着启动
3. ⼀般启动Tomcat会是运⾏startup.bat或者startup.sh⽂件,实际上这两个⽂件最后会调⽤org.apache.catalina.startup.Bootstrap类的main⽅法,这个main⽅法主要做了两件事情,
1:定义和初始化了tomcat⾃⼰的类加载器,2:通过反射调⽤了org.apache.catalina.startup.Catalina的process⽅法;
4. process⽅法的功能也很简单,1:如果catalina.home和catalina.base两个属性没有设置就设置⼀下,2:参数正确的话就调⽤execute⽅法,execute的⽅法就是简单的调⽤start⽅法,
其中在判断参数正确的⽅法arguments中会设置starting标识为true,这样在execute⽅法中就能调⽤start⽅法,start⽅法是重点,在它⾥⾯启动了我们的Tomcat所有的服务
5. 这⾥最重要的⽅法是createStartDigester();和((Lifecycle) server).start();createStartDigester⽅法主要的作⽤就是帮我们实例化了所有的服务组件包括server,service和connect,⾄于
怎么实例化的等下再看,start⽅法就是启动服务实例了。File file = configFile();是新建l⽂件实例,后⾯的服务组件都是要根据这个⽂件来的
6. Digester是⼀个外部jar包⾥⾯的类,主要的功能就是解析xml⾥⾯的元素并把元素⽣成对象,把元素的属性设置成对象的属性,并形成对象间的⽗⼦兄弟等关系。
Digester.addObjectCreate("Server", "org.StandardServer", "className");//创建⼀个
org.StandardServer对象,实际上这⾥并没有真正
创建出⼀个对象,⽽是添加⼀个模式,只是后⾯创建的对象是根据这些模式和l来的,所以可以暂时这么理解。真正创建对象是在start⽅法⾥⾯的digester.parse(is),is是
HttpConnector,容器StandardHost的关系,所以调⽤digester.parse(is)⽅法后就会根据模式和l⽂件来⽣成对象以及他们之间的相互关系。这样我们便有了服务器组件
StandardServer的对象,也有了它的⼦组件StandardService对象等等
7. 既然有了服务器组件的对象,就初始化然后启动就可以了,到此,tomcat就实现了启动服务器组件StandardServer。启动后做的事情就东西⽐较多,但是还是⽐较清晰的,
StandardServer的start⽅法关键代码是启动它的⼦组件StandardService。StandardService的start⽅法跟StandardServer的start⽅法差不多,是启动它的连接器和容器,上⾯说了⼀个
Service包含⼀个容器和多个连接器
8. 默认的连接器是HttpConnector,所以会调⽤HttpConnector的start⽅法。这⾥有个两个关键的类:HttpConnector和HttpProcessor,它们都实现了Runnable接⼝,HttpConnector
负责接收http请求,HttpProcessor负责处理由HttpConnector接收到的请求。注意这⾥HttpProcessor会有很多的实例,最⼤可以有maxProcessor个,初始化是20个。所以在
threadStart⽅法中会启动⼀个后台线程来接收http连接
9. 这样,就会启动HttpConnector后台线程,它的run⽅法不断循环,主要就是新建⼀个ServerSocket来监听端⼝等待连接。serverSocket⼀直等待连接,得到连接后给HttpProcessor
的实例processor来处理,serverSocket则继续循环监听,⾄于processor具体怎么处理,还有很多要说,这⾥先不说。
Tomcat源码分析(⼆)--连接处理
1.  在上⼀节⾥已经启动了⼀个HttpConnector线程,并且也启动了固定数量的HttpProcessor线程。HttpConnector⽤来等待http连接,得到http连接后交给其中的⼀个HttpProcessor
线程来处理。接下⾥具体看⼀下HttpConnector是怎么得到连接得,以及HttpProcessor是怎么处理的
2. 这⾥很关键的就是socket = serverSocket.accept();和processor.assign(socket); 在循环⾥⾯内,serverSocket.accept();负责接收http请求然后赋值给socket,最后交给其中⼀个processor
处理。这⾥processor并不是等到需要的时候再实例化,⽽是在HttpConnector初始化的时候已经有了若⼲个processor。httpConnector⾥⾯持有⼀个包含HttpProcessor对象的栈,需要的
时候拿出来就是了。
3. 接下来由processor.assign(socket); 记住这个⽅法是异步的,不需要等待HttpProcessor来处理完成,所以HttpConnector才能不间断的传⼊Http请求
4. 很明显,在它的run⽅法⼀开始便是调⽤上⾯的await⽅法来等待(因为⼀开始available变量为false),所以HttpProcessor会⼀直阻塞,直到有线程来唤醒它。当从HttpConnector中调⽤
processor.assign(socket),会把socket传给此HttpProcessor对象,并设置available为true,调⽤notifyAll()唤醒该processor线程以处理socket。同时,在await⽅法中⼜把available
设置成false,因此⼜回到初始状态,即可以重新接受socket。这⾥处理socket的⽅法是process(socket),主要作⽤有两点,1:解析这个socket,即解析http请求,包括请求⽅法,请求协议等,以
填充request,response对象(是不是很熟悉,在servlet和jsp开发经常⽤到的request,response对象就是从这⾥来的)。2:传⼊request,response对象给和HttpConnector绑定的容器,让容器来
servlet和tomcat的关系调⽤invoke⽅法进⾏处理。
5. 在那些parse××⽅法⾥⾯会对request,response对象进⾏初始化,然后调⽤容器的invoke⽅法进⾏处理,⾄此,http请求过来的连接已经完美的转交给容器处理,容器剩下的问题就是要最终转
交给哪个servlet或者jsp的问题。前⾯我们知道,⼀个连接会跟⼀个容器相连,⼀个级别⼤的容器会有⼀个或者多个⼦容器,最⼩的容器是Wrapper,对应⼀个servlet,在这⾥我们只要知道请求的
路径决定了最终会选择哪个wrapper,wrapper最终会调⽤servlet的。⾄少⼀开始提出来的问题已经明⽩了。
Tomcat源码分析(三)--连接器是如何与容器关联的?
这篇⽂章要弄懂⼀个问题,我们知道,⼀个链接器是跟⼀个容器关联的,容器跟链接器是在什么时候关联上的?
1. 在明⽩这个问题前要先了解⼀下Digester库,这个库简单的说就是解析xml⽂件,这⾥有两个概念:模式和规则,所谓模式就是⼀个xml的标签,规则就是遇到⼀个xml标签需要做什么,看⼀下他主要的三个⽅法:
addObjectCreate(String pattern, String className, String attributeName)    根据模式pattern实例化⼀个对象className
addSetProperties(String pattern)  设置这个模式的属性
addSetNext(String pattern, String methodName, String paramType)  添加模式之间的关系,调⽤⽗模式的
上⾯可能不好理解,看tomcat是怎么⽤到Digester的,在org.apache.catalina.ateStartDigester()的⽅法⾥,在这个⽅法⾥有使⽤Digester来解析l⽂件
2. 遇到标签Server/Service/Connector的时候(这⾥简化了说法,应该是标签Server下的⼦标签Servic
e的⼦标签Connector,有点拗⼝),实例化HttpConnector,然后在它的上⼀级⽗容器StandardService
下调⽤addConnector,这样就把链接器HttpConnector添加进容器StandardService下了
3. 把⼀个链接器connector添加到StandardService的connectors数组⾥,然后关联上StandardService的容器
4. 当我们调⽤了digester.addRuleSet(new EngineRuleSet("Server/Service/"));⽅法,Digester便会⾃动调⽤到EngineRuleSet类的addRuleInstances⽅法,在⽅法⾥⾯⽆⾮也是添加各种模式和规则,
根据上⾯的添加规则,很容易知道这⾥⼜添加了⼀个StandardEngine对象(容器),然后⼜在该模式的上⼀级模式Server/Service添加StandardEngine跟StandardService的关系,即通过setContainer
⽅法把容器添加进StandardService⾥。
5. 把容器设置到StandardService下,在“同步代码块”处,把容器和链接器关联上了,⾄此,容器和链接器就关联上了。
Tomcat源码分析(四)--容器处理链接之责任链模式
1. Container()得到的容器应该是StandardEngine(其实应该是由l⽂件配置得到的,这⾥先假定是StandardEngine),StandardEngine没有invoke⽅法,它继承与
ContainerBase(事实上所有的容器都继承于ContainerBase,在ContainerBase类有⼀些容器的公⽤⽅法和属性)
2. 由代码可知ContainerBase的invoke⽅法是传递到Pipeline,调⽤了Pipeline的invoke⽅法。这⾥要说⼀下Pipeline这个类,这是⼀个管道类,每⼀个管道类Pipeline包含数个阀类,阀类是
实现了Valve接⼝的类,Valve接⼝声明了invoke⽅法。管道和阀的概念跟servlet编程⾥⾯的过滤器机制⾮常像,管道就像过滤器链,阀就好⽐是过滤器。不过管道中还有⼀个基础阀的概念,
所谓基础阀就是在管道中当管道把所有的普通阀都调⽤完成后再调⽤的。不管是普通阀还是基础阀,都实现了Value接⼝,也都继承于抽象类ValveBase。在tomcat中,当调⽤了管道的
invoke⽅法,管道则会顺序调⽤它⾥⾯的阀的invoke⽅法。
3. 其中StandardPipelineValveContext是管道⾥的⼀个内部类,内部类的作⽤是帮助管道顺序调⽤阀Value的invoke⽅法
内部类StandardPipelineValveContext的invokeNext⽅法通过使⽤局部变量来访问下⼀个管道数组,管道类的变量stage保存当前访问到第⼏个阀,valves保存管道的所有阀,在调⽤普通阀的
invoke⽅法是,会把内部类StandardPipelineValveContext本⾝传进去,这样在普通阀中就能调⽤invokeNext⽅法以便访问下⼀个阀的invoke⽅法
4. 这个阀的invoke⽅法,通过传进来到StandardPipelineValveContext(实现了ValveContext接⼝)的invokeNext⽅法来实现调⽤下⼀个阀的invoke⽅法。然后简单的打印了请求的ip地址。
最后再看StandardPipelineValveContext的invokeNext⽅法,调⽤完普通阀数组valves的阀后,开始调⽤基础阀basic的invoke⽅法,这⾥先说基础阀的初始化,在每⼀个容器的构造函数类就已经
初始化了基础阀。即在容器构造的时候就已经把基础阀添加进管道pipeline中,这样在StandardPipelineValveContext中的invokeNext⽅法⾥就能调⽤基础阀的invoke了,当
basic.invoke(request, response, this);进⼊基础阀StandardEngineValve
5. 在StandardEngine的基础阀StandardEngineValve⾥,调⽤了⼦容器invoke⽅法(这⾥⼦容器就是StandardHost),还记得⼀开始connector.invoke(request, response)
(即StandardEngine的invoke⽅法)现在顺利的传递到⼦容器StandardHost的invoke⽅法,变成了StandardHost.invoke(request, response)。由此可以猜测StandardHost也会传递给它的
⼦容器,最后传递到最⼩的容器StandardWrapper的invoke⽅法,然后调⽤StandardWrapper的基础阀StandardWrapperValue的invoke⽅法,由于StandardWrapper是最⼩的容器了,
不能再传递到其他容器的invoke⽅法了,那它的invoke⽅法做了什么?主要做了两件事, 1:创建⼀个过滤器链并  2:分配⼀个servlet 或者jsp
6. 这⾥先不关注jsp,只关注⼀下servlet,通过servlet = wrapper.allocate(); 进⼊StandardWrapper的allocate⽅法,allocate主要就是调⽤了loadServlet⽅法,在
loadServlet⽅法类⽤tomcat⾃⼰的类加载器实例化了⼀个servlet对象,并调⽤了该servlet的init和service⽅法。⾄此已经把请求传递到servlet的service(或者jsp的service)⽅法,
整个处理请求到这⾥就结束了,剩下的就是返回客户端了。
Tomcat源码分析(五)--容器处理连接之servlet的映射
本⽂所要解决的问题:⼀个http请求过来,容器是怎么知道选择哪个具体servlet?
1. ⼀个Context容器表⽰⼀个web应⽤,⼀个Wrapper容器表⽰⼀个servlet,所以上⾯的问题可以转换为怎么由Context容器选择servlet,答案是映射器。映射器是实现了Mapper接⼝的
类,作⽤就是根据请求连接(主要是协议和路径)来选择下⼀个容器,可以看做是⼀个哈希表,根据关键字段来选择具体的值,Mapper 接⼝
2. 在Tomcat源码分析(四)--容器处理链接之责任链模式中已经知道,请求连接到达StandardContext容器的invoke⽅法,最终会到达StandardContextValue阀的invoke⽅法⾥⾯,在这个
invoke⽅法中有⼀句这样的代码。这句代码表⽰容器会调⽤map⽅法来映射请求到具体的wrapper上,意思就是说,根据连接请求request 来选择wrapper。上⾯的map会调⽤⽗类ContainerBase
的map⽅法来到具体的映射器,⾄于这个映射器和容器是怎么关联上的,具体请参考这篇⽂章,⼤致原理是⼀样的。StandardContext 容器
有⼀个标准的映射器实现类StandardContextMapper,所以最终会调⽤到映射器StandardContextMapper的map⽅法,这个⽅法是选择servlet的关键
3. 分4中匹配模式(完全匹配,前缀匹配,扩展匹配,默认匹配)来选择wrapper,关键代码就是name = context.findServletMapping和wrapper = (Wrapper) context.findChild(name);这⾥
⾯context都是StandardContext。context.findServletMapping是根据匹配模式来到servlet名字,context.findChild是根据servlet名字到具体的wrapper。findServletMapping⽅法很
简单,就是在⼀个HashMap⾥⾯得到servlet名字
Tomcat源码分析(六)--⽇志记录器和国际化
1. 只要实现Logger就能有⼀个⾃⼰的⽇志记录器,其中setContainer是把⽇志记录器跟具体的容器关联,setVerbosity是设置⽇志的级
别,log是具体的⽇志记录函数。FATAL,ERROR,WARNING,
INFORMATION,DEBUG代表⽇志记录的五个级别,看单词就能明⽩意思。这⾥主要讲解⼀下FileLogger类,这是Tomcat的其中⼀个⽇志记录器,它把⽇志记录在⼀个⽂件中,FileLogger的启动
⽅法和关闭仅仅是出发⼀个⽣命周期事件,并不做其他的事情
Tomcat源码分析(七)--单⼀启动/关闭机制(⽣命周期)
1. Tomcat有很多组件,要⼀个⼀个启动组件难免有点⿇烦。由于Tomcat的包含关系是Catalina->Server->Service->容器/连接器/⽇志器等,于是可通过⽗组件负责启动/关闭它的⼦组件,
这样只要启动Catalina,其他的都⾃动启动了。这种单⼀启动和关闭的机制是通过实现Lifecycle接⼝来实现的
2. 当组件实现了Lifecycle接⼝,⽗组件启动的时候,即调⽤start⽅法时,只要在⽗组件的start⽅法中也调⽤⼦组件的start⽅法即可(只有实现统⼀的接⼝Lifecycle才能实现统⼀调⽤,如以下调⽤
⽅式:(Lifecycle)⼦组件.start())
3. 关键看((Lifecycle) server).start();这样便在启动Catalina的时候启动了Server,再看StandardServer的start⽅法
主要做了两件事,1:发送⽣命周期事件给监听者;2:启动⼦组件services(⾄于server怎么关联上services请看前⾯的⼏篇⽂章,以后都不再题怎么关联上的了)。
4. 这⾥先岔开⼀下,说⼀下,lifecycle是⼀个⼯具类LifecycleSupport的实例,每⼀个组件都有这样⼀个⼯具类,这个⼯具类的作⽤就是帮助管理该组件上的,包括添加和发
事件给
5. 先看构造⽅法,传⼊⼀个lifecycle,因为每个组件都实现了lifecycle,所以这⾥传⼊的实际上是⼀个组件,即每个组件都有⼀个LifecycleSupport与之关联,当要在组件中添加⼀个的时候,
实际上是添加进⼯具类LifecycleSupport的⼀个数组listeners中,当要发送⼀个组件⽣命周期的事件时,⼯具类就会遍历数组,然后再⼀个⼀个的发送事件。这⾥需要先实现我们
⾃⼰的类并且添加进我们需要监听的组件当中。实现类只要实现LifecycleListener接⼝就⾏
6. 我们需要做的就是实现LifecycleListener接⼝来拥有⾃⼰的,在lifecycleEvent⽅法⾥写⾃⼰监听到事件后该做的事情,然后添加进要监听的组件就⾏,⽐如当我们要看StandardServer
是否启动了,在上⾯StandardServer的start⽅法有⼀句这样的代码:lifecycle.fireLifecycleEvent(START_EVENT, null);即发送StandardServer启动的事件给跟它关联的。接下来回
到⼀开始,当server启动后,接着启动它的⼦组件service,即调⽤StandardService的start⽅法,这个⽅法跟StandardServer的start⽅法差不多,只是启动了连接器和容器,连接器的start
⽅法在前⾯的⽂章已经讲过了,主要是启动了n个处理器HttpProcessor组件。顶级容器是StandardEngine,它的start⽅法仅仅调⽤了⽗类ContainerBase的start⽅法
它启动了Tomcat其他所有的组件,包括加载器,映射器,⽇志记录器,管道等等,由这⾥也可以看出,
他们都实现了Lifecycle接⼝。统⼀关闭跟统⼀启动的逻辑差不多,这⾥就不再说了。
Tomcat源码分析(⼋)--载⼊器(加载器)
1. Tomcat就是⽤的这种⽅法。jdk建议我们实现⾃⼰的类加载器的时候是重写findClass⽅法,不建议重写loadclass⽅法,因为ClassLoader 的loadclass⽅法保证了类加载的⽗亲委托机制,
如果重写了这个⽅法,就意味着需要实现⾃⼰在重写的loadclass⽅法实现⽗亲委托机制
2. 当所有⽗类都加载不了,才会调⽤findClass⽅法,即调⽤到我们⾃⼰的类加载器的findClass⽅法
3. 在我们⾃⼰实现的类加载器中,defineClass⽅法才是真正的把类加载进jvm,defineClass是从ClassLoader继承⽽来,把⼀个表⽰类的字节数组加载进jvm转换为⼀个类。
4. 我们⾃⼰实现的类加载器跟系统的类加载器没有本质的区别,最⼤的区别就是加载的路径不同,系统类加载器会加载环境变量CLASSPATH中指明的路径和jvr⽂件,我们⾃⼰的类加载器
可以定义⾃⼰的需要加载的类⽂件路径.同样的⼀个class⽂件,⽤系统类加载器和⾃定义加载器加载进jvm后类的结构是没有区别的,只是他们访问的权限不⼀样,⽣成的对象因为加载器不同
也会不⼀样.当然我们⾃⼰的类加载器可以有更⼤的灵活性,⽐如把⼀个class⽂件(其实就是⼆进制⽂件)加密后(简单的加密就把0和1互换),系统类加载器就不能加载,需要由我们⾃⼰定义
解密类的加载器才能加载该class⽂件.
5. 现在来初步的看看Tomcat的类加载器,为什么Tomcat要有⾃⼰的类加载器.这么说吧,假如没有⾃⼰的类加载器,我们知道,在⼀个Tomcat中是可以部署很多应⽤的,如果所有的类都由

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。