Tomcat 源码分析总结与收获

1. Tomcat 类加载机制

Tomcat中,分别包含以下几个加载器:

  • Common类加载器,用于加载Tomcat和各个Web应用共享的类
  • Share类加载器,用于加载各个Web共享的类
  • Server类加载器,用于加载Tomcat各个类
  • WebApp类加载器,用于加载各个WebApp中的类

对于Tomcat来说,各个类加载器的父子关系如下:

image

由上图和JVM的双亲委派机制可以看出来,WebApp加载器按道理来说应该作为最底层的加载器。但是在Tomcat的默认配置中并不是这样,默认情况下webApp会首先通过JVM的类加载器加载类,如果JVM类加载器未成功加载,则直接交给WebApp加载,如果webApp依然没有加载成功,则再交给父加载器加载。

显然这样违背了双亲委派机制,Tomcat为何要这样设计呢?因为这样能够最大程度的减少其他类对webApp类的影响,比如如果某个公共类的jar包被放在了SharedClassLoader中,而某个webApp却依赖了这个jar包的更高版本,此时webApp便可能会抛出NoSuchMethodException

2. Tomcat 的线程模型

Tomcat中,提供了4种线程IO模型:

描述
BIO 阻塞式IO,采用传统的java IO进行操作,该模式下每个请求都会创建一个线程,适用于并发量小的场景
NIO 同步非阻塞,比传统BIO能更好的支持大并发,tomcat 8.0 后默认采用该模式
APR tomcat 以JNI形式调用http服务器的核心动态链接库来处理文件读取或网络传输操作,需要编译安装APR库
AIO 异步非阻塞,tomcat8.0后支持

Tomcat中,支持的协议包含HTTP1.1,HTTP2.0以及AJP协议,因此在Tomcat中存在以上4种模型结合3种协议的处理器。

3. Tomcat的系统结构

Tomcat中,将各个功能拆分为多个模块,各个模块的结合形式如下:

Tomcat的整体结构如下:

image

其中:

  • Server作为整个容器基础,负责整个容器的开启与关闭。
  • Server中可以包含多个ServiceService作为一个服务,每个Service负责监控一个端口
  • Service中 每个Service包含多个连接器(Connector)和容器以及其他组件
  • Connector主要负责创建不同的IO模型以及接收不同的协议,比如HTTP,AJP
  • 容器主要通过分级的方式对不同层级的信息处理,从上到下主要包含有:Engine,Host,Context,Servlet

4. Tomcat 的HOME 目录和BASE目录

Tomcat中,如果需要启动多个Tomcat实例的话,Tomcat提供了一种共享基础代码配置,对于Tomcat来说,主要分为以下几个目录:

  • bin
  • conf
  • lib
  • logs
  • temp
  • webapps
  • work

这些文件目录中,对于bin目录和lib目录,所有的Tomcat都可以共享这个目录。可以通过catalina.home配置此目录

对于conf,logs,temp,webappswork目录每个Tomcat实例需要单独配置一份,在配置文件中通过catalina.base进行配置。

5.Tomcat解析Url方式

Tomcat实现了ServletURL解析规范,在Servlet中规定Url配置方式包含以下几种:

  • 精确匹配
  • 路径匹配
  • 扩展匹配
  • 默认匹配

Tomcat在处理这些配置的时候,是逐层处理的,前面说过Tomcatservice中包含了很多容器组件,容器都是分层扩展,因此Url在解析的时候,也是直接分成进行解析。最终形成一个树结构。

Tomcat在解析Url的时候,也是首先判断Host,当Host成功匹配后,再继续查找Context,当Context成功匹配后,在继续查找Servlet

由于Servlet包含4中模糊匹配,并且在一个Context中可能存在很多个Servlet因此Tomcat使用的是数组的二分查找,这样更加有利于Servlet的模糊查找。

6.Tomcat 管理Servlet生命周期

Servlet规范中,规定了一些生命周期,比如Servlet类中init()方法和destory()方法在整个系统中保证只调用一次,并且规定可以通过配置loadOnStartUp参数确定是在第一次请求的时候调用init()方法还是在系统启动的时候就调用init()方法

同时,在Servlet中规定,在整个系统中,保证同一个Servlet类只存在一个对象。

其实由上面的上面的规定可以看出这完全符合单例类的特点。但是在Tomcat中,并不是通过单例来实现的,首先,在Tomcat中,每个Servlet对象都被StandardWrapper所包含,在StandardWrapper初始化的时候,就会检查该ServletLoadOnStartUp参数是否被设置,如果没有设置,则将其标记为未初始化,如果已经设置,则将其存在TreeMap中,通过权重进行按顺序初始化。这是单例不好实现的一部分。

同时对于整个系统只存在一个相同的Servlet对象,Tomcat采用了典型的双重锁校验完成。

7.Tomcat 解析XML文件生成对应的对象

我们知道Tomcat的配置文件是xml,同时Tomcat会根据xml文件配置的内容通过反射实例化类,在Tomcat的实现中,它是借助了Degiste框架实现的,使用Degister只用在实例化Degiste的时候,配置好初始化规则,他就会自动按照规则实例化相关类,并赋值到相关对象的属性中(通过setNext)从而最终生成一个大的对象。

8. Tomcat 中的HTTP缓存功能

我们知道,在HTTP请求过程中,有些时候服务器会先返回HTTP Header,然后等Body全部写入后再返回Body,也就是说,在Tomcat存在一个缓存,当用户写入数据的时候,并不是直接就传给HTTP客户端,而是将数据先缓存起来,只有当数据量到达阈值的时候或者调用了fush()方法的时候才会真正的将数据发送给客户端,那这个缓存是如何处理的呢?

Tomcat中,首先将OutputStream进行简单的包装,当用户需要获取OutputStream的时候,便返回这个包装类:OutPutBufferOutPutBUffer中便设置了缓存。当需要flush()的时候,才真正的将缓存中的数据写入到内部的outPutStream中。

9. Tomcat 的请求处理流程

前面说过Tomcat将内部server分为了好几个不同的模块,在启动的时候,Tomcat会启动连接器模块:ConnectorConnector通过ProtocolHandler来处理消息,不同的协议,和IO方式对应不同的ProtocolHandlerProtocolHandler主要分为3个模块,分别是EndPoint,Porcesser,Adapter,其中EndPoint负责配置Socket,并开始监听端口,Processer在监听到消息后,读取消息并根据消息协议(比如HTTP)进行简单的转换,并生成RequestResponse,然后将两个对象交给AdapterAdapter主要作用便是将接受的request(org.apache.coyote.Request),response适配为容器需要的request(org.apache.catalina.connector.Request),response,然后将对象转发给Service中的容器进行后续的处理,容器由上到下分别为Engine->Host->Context->Servlet每个容器中,还可以自定义不同的业务处理,Tomcat通过责任链模式可以很方便的修改业务处理逻辑。最主要的类便是valvepipeline.

10. Tomcat 热部署原理

我们知道Tomcat是具有热部署功能的,在Tomcat中,首先它定义了自己的ClassLoader,其次,Tomcat每次在部署JSP文件的时候,都会记录文件的修改时间的时间戳,每当经过设定时间后(intmodificationTestInterval),Tomcat都会检测JSP文件当前的最后一次修改的时间戳,当时间戳不相同的时候,便重新加载JSP文件。

11. Tomcat 后台定时管理

Tomcat中,存在很多后台定时任务,比如扫描 JSP,查看Session是否过期等,最开始Tomcat为每个任务都单独的设置了一个定时线程,后来发现这样比较耗费性能,后来通过监听者模式修改成了整个系统共用一个定时任务,其他定时任务只用监听这个系统,当触发了指定的时间后,自己启动线程执行任务即可。