本文转自:http://blog.csdn.net/haitao111313/article/category/1179996java
1. Tomcat主要有两个组件,链接器和容器,所谓链接器就是一个http请求过来了,链接器负责接收这个请求,而后转发给容器。容器即servlet容器,容器有不少层,分别是Engine,web
Host,Context,Wrapper。最大的容器Engine,表明一个servlet引擎,接下来是Host,表明一个虚拟机,而后是Context,表明一个应用,Wrapper对应一个servlet。从链接器apache
传过来链接后,容器便会顺序通过上面的容器,最后到达特定的servlet。要说明的是Engine,Host两种容器在不是必须的。实际上一个简单的tomcat只要链接器和容器就能够了,编程
但tomcat的实现为了统一管理链接器和容器等组件,额外添加了服务器组件(server)和服务组件(service),添加这两个东西的缘由我我的以为就是为了方便统一管理链接器和数组
容器等各类组件。一个server能够有多个service,一个service包含多个链接器和一个容器,固然还有一些其余的东西,看下面的图就很容易理解Tomcat的架构了:浏览器
2. 一个父组件又能够包含多个子组件,这些被统一管理的组件都实现了Lifecycle接口。只要一个组件启动了,那么他的全部子组件也会跟着启动,好比一个server启动了,它的全部子缓存
service都会跟着启动,service启动了,它的全部链接器和容器等子组件也跟着启动了,这样,tomcat要启动,只要启动server就好了,其余的组件都会跟随着启动tomcat
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();是新建server.xml文件实例,后面的服务组件都是要根据这个文件来的
6. Digester是一个外部jar包里面的类,主要的功能就是解析xml里面的元素并把元素生成对象,把元素的属性设置成对象的属性,并造成对象间的父子兄弟等关系。
Digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className");//建立一个org.apache.catalina.core.StandardServer对象,实际上这里并无真正
建立出一个对象,而是添加一个模式,只是后面建立的对象是根据这些模式和server.xml来的,因此能够暂时这么理解。真正建立对象是在start方法里面的digester.parse(is),is是
server.xml文件的流,digester刚才已经添加了StandardServer和StandardService等服务组件,也添加了StandardServer和StandardService的关系以及StandardService和链接器
HttpConnector,容器StandardHost的关系,因此调用digester.parse(is)方法后就会根据模式和server.xml文件来生成对象以及他们之间的相互关系。这样咱们便有了服务器组件
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绑定的容器,让容器来
调用invoke方法进行处理。
5. 在那些parse××方法里面会对request,response对象进行初始化,而后调用容器的invoke方法进行处理,至此,http请求过来的链接已经完美的转交给容器处理,容器剩下的问题就是要最终转
交给哪一个servlet或者jsp的问题。前面咱们知道,一个链接会跟一个容器相连,一个级别大的容器会有一个或者多个子容器,最小的容器是Wrapper,对应一个servlet,在这里咱们只要知道请求的
路径决定了最终会选择哪一个wrapper,wrapper最终会调用servlet的。至少一开始提出来的问题已经明白了。
这篇文章要弄懂一个问题,咱们知道,一个连接器是跟一个容器关联的,容器跟连接器是在何时关联上的?
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.startup.Catalina.createStartDigester()的方法里,在这个方法里有使用Digester来解析server.xml文件
2. 遇到标签Server/Service/Connector的时候(这里简化了说法,应该是标签Server下的子标签Service的子标签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下,在“同步代码块”处,把容器和连接器关联上了,至此,容器和连接器就关联上了。
1. connector.getContainer()获得的容器应该是StandardEngine(其实应该是由server.xml文件配置获得的,这里先假定是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)方法,
整个处理请求到这里就结束了,剩下的就是返回客户端了。
本文所要解决的问题:一个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方法来找到具体的映射器,至于这个映射器和容器是怎么关联上的,具体请参考 Tomcat源码分析(三)--链接器是如何与容器关联的?这篇文章,大体原理是同样的。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名字
1. 只要实现Logger就能有一个本身的日志记录器,其中setContainer是把日志记录器跟具体的容器关联,setVerbosity是设置日志的级别,log是具体的日志记录函数。FATAL,ERROR,WARNING,
INFORMATION,DEBUG表明日志记录的五个级别,看单词就能明白意思。这里主要讲解一下FileLogger类,这是Tomcat的其中一个日志记录器,它把日志记录在一个文件中,FileLogger的启动
方法和关闭仅仅是出发一个生命周期事件,并不作其余的事情
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接口。统一关闭跟统一启动的逻辑差很少,这里就再也不说了。
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中是能够部署不少应用的,若是全部的类都由
系统类加载器来加载,那么部署在Tomcat上的A应用就能够访问B应用的类,这样A应用与B应用之间就没有安全性可言了。还有一个缘由是由于Tomcat须要实现类的自动重载,因此也
须要实现本身的类加载器。Tomcat的载入器是实现了Loader接口的WebappLoader类,也是Tomcat的一个组件,实现Lifecycle接口以便统一管理,启动时调用start方法,在start
方法主要作了如下的事情:
1:建立一个真正的类加载器以及设置它加载的路径,调用createClassLoader方法
2:启动一个线程来支持自动重载,调用threadStart方法
6. 在createClassLoader方法内,实例化了一个真正的类加载器WebappClassLoader,代码很简单。WebappClassLoader是Tomcat的类加载器,它继承了
URLClassLoader(这个类是ClassLoader的孙子类)类。重写了findClass和loadClass方法。Tomcat的findClass方法并无加载相关的类,只是从已经加载的类中查找
这个类有没有被加载,具体的加载是在重写的loadClass方法中实现,从上面的对java的讨论可知,重写了loadClass方法就意味着失去了类加载器的父亲委托机制,须要本身
来实现父亲委托机制。
7. 上面的代码很长,但实现了Tomcat本身的类加载机制,具体的加载规则是:
1:由于全部已经载入的类都会缓存起来,因此先检查本地缓存
2:如本地缓存没有,则检查上一级缓存,即调用ClassLoader类的findLoadedClass()方法;
3:若两个缓存都没有,则使用系统的类进行加载,防止Web应用程序中的类覆盖J2EE的类
4:若打开标志位delegate(表示是否代理给父加载器加载),或者待载入的类是属于包触发器的包名,则调用父类载入器来加载,若是父类载入器是null,则使用系统类载入器
5:从当前仓库中载入相关类
6:若当前仓库中没有相关类,且标志位delegate为false,则调用父类载入器来加载,若是父类载入器是null,则使用系统类载入器(4跟6只能执行一个步骤的)
8. 先检查系统ClassLoader,所以WEB-INF/lib和WEB-INF/classes或{tomcat}/libs下的类定义不能覆盖JVM 底层可以查找到的定义(譬如不能经过定义java.lang.Integer替代底层的实现
1. JSESSIONID是一个惟一标识号,用来标识服务器端的Session,也用来标识客户端的Cookie,客户端和服务器端经过这个JSESSIONID来一一对应。这里须要说明的是Cookie已经包含
JSESSIONID了,能够理解为JSESSIONID是Cookie里的一个属性。
2. 让我假设一次客户端链接来讲明我对个这三个概念的理解:
Http链接自己是无状态的,即前一次发起的链接跟后一次没有任何关系,是属于两次独立的链接请求,可是互联网访问基本上都是须要有状态的,即服务器须要知道两次链接请求是否是同
一我的访问的。如你在浏览淘宝的时候,把一个东西加入购物车,再点开另外一个商品页面的时候但愿在这个页面里面的购物车还有上次添加进购物车的商品。也就是说淘宝服务器会知道这
两次访问是同一个客户端访问的。
客户端第一次请求到服务器链接,这个链接是没有附带任何东西的,没有Cookie,没有JSESSIONID。服务器端接收到请求后,会检查此次请求有没有传过来JSESSIONID或者Cookie,
若是没有JSESSIONID和Cookie,则服务器端会建立一个Session,并生成一个与该Session相关联的JSESSIONID返回给客户端,客户端会保存这个JSESSIONID,并生成一个与该
JSESSIONID关联的Cookie,第二次请求的时候,会把该Cookie(包含JSESSIONID)一块儿发送给服务器端,此次服务器发现这个请求有了Cookie,便从中取出JSESSIONID,而后
根据这个JSESSIONID找到对应的Session,这样便把Http的无状态链接变成了有状态的链接。可是有时候浏览器(即客户端)会禁用Cookie,咱们知道Cookie是经过Http的请求头部
的一个cookie字段传过去的,若是禁用,那么便得不到这个值,JSESSIONID便不能经过Cookie传入服务器端,固然咱们还有其余的解决办法,url重写和隐藏表单,url重写就是把
JSESSIONID附带在url后面传过去。隐藏表单是在表单提交的时候传入一个隐藏字段JSESSIONID。这两种方式都能把JSESSIONID传过去。
3. 代码主要就是从http请求头部的字段cookie获得JSESSIONID并设置到reqeust的sessionid,没有就不设置。这样客户端的JSESSIONID(cookie)就传到tomcat,tomcat
把JSESSIONID的值赋给request了。这个request在Tomcat的惟一性就标识了。
4. Session只对应用有用,两个应用的Session通常不能共用,在Tomcat一个Context表明一个应用,因此一个应用应该有一套本身的Session,Tomcat使用Manager来管理各个应用
的Session,Manager也是一个组件,跟Context是一一对应的关系,怎么关联的请参考Tomcat源码分析(一)--服务启动,方法相似。Manager的标准实现是StandardManager,
由它统一管理Context的Session对象(标准实现是StandardSession),可以猜测,StandardManager必定可以建立Session对象和根据JSESSIONID从跟它关联的应用中查找
Session对象。事实上StandardManager确实有这样的方法,可是StandardManager自己没有这两个方法
5. Session是在何时生成的?仔细想一想,咱们编写servlet的时候,若是须要用到Session,会使用request.getSession(),这个方法最后会调用到HttpRequestBase的
getSession()方法,因此这里有个重要的点:Session并非在客户端第一次访问就会在服务器端生成,而是在服务器端(通常是servlet里)使用request调用getSession方法才
生成的。可是默认状况下,jsp页面会调用request.getSession(),即jsp页面的这个属性<%@ page session="true" %>默认是true的,编译成servlet后会调用
request.getSession()。因此只要访问jsp页面,通常是会在服务器端建立session的。可是在servlet里就须要显示的调用getSession(),固然是在要用session的状况。
1. 在Tomcat的世界里,一个Host容器表明一个虚机器资源,Context容器表明一个应用,所谓的部署器就是可以把Context容器添加进Host容器中去的一个组件。显然,一个Host
容器应该拥有一个部署器组件。
2. 在Catalina的createStartDigester()方法中,向StandardHost容器中添加了一个HostConfig的实例。HostConfig类实现了LifecycleListener接口,也就是说它是个监听器类,
能监听到组件的生命周期事件
3. 若是监听到StandardHost容器启动开始了,则调用start方法来
4. ((Deployer) host).install(contextPath, url);会调用到StandardHost的install方法,再由StandardHost转交给StandardHostDeployer的install方法,
StandardHostDeployer是一个辅助类,帮助StandardHost来实现发布应用,它实现了Deployer接口,看它的install(URL config, URL war)方法(它有两个install方法,分别
用来发布上面不一样方式的应用