1. Tomcat 类加载机制
在Tomcat
中,分别包含以下几个加载器:
Common
类加载器,用于加载Tomcat
和各个Web
应用共享的类Share
类加载器,用于加载各个Web
共享的类Server
类加载器,用于加载Tomcat
各个类WebApp
类加载器,用于加载各个WebApp
中的类
对于Tomcat
来说,各个类加载器的父子关系如下:
由上图和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
的整体结构如下:
其中:
Server
作为整个容器基础,负责整个容器的开启与关闭。Server
中可以包含多个Service
,Service
作为一个服务,每个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
,webapps
和work
目录每个Tomcat
实例需要单独配置一份,在配置文件中通过catalina.base
进行配置。
5.Tomcat解析Url方式
Tomcat
实现了Servlet
的URL
解析规范,在Servlet
中规定Url
配置方式包含以下几种:
- 精确匹配
- 路径匹配
- 扩展匹配
- 默认匹配
Tomcat
在处理这些配置的时候,是逐层处理的,前面说过Tomcat
的service
中包含了很多容器组件,容器都是分层扩展,因此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
初始化的时候,就会检查该Servlet
的LoadOnStartUp
参数是否被设置,如果没有设置,则将其标记为未初始化,如果已经设置,则将其存在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
的时候,便返回这个包装类:OutPutBuffer
,OutPutBUffer
中便设置了缓存。当需要flush()
的时候,才真正的将缓存中的数据写入到内部的outPutStream
中。
9. Tomcat 的请求处理流程
前面说过Tomcat
将内部server
分为了好几个不同的模块,在启动的时候,Tomcat
会启动连接器模块:Connector
,Connector
通过ProtocolHandler
来处理消息,不同的协议,和IO
方式对应不同的ProtocolHandler
,ProtocolHandler
主要分为3个模块,分别是EndPoint,Porcesser,Adapter
,其中EndPoint
负责配置Socket
,并开始监听端口,Processer
在监听到消息后,读取消息并根据消息协议(比如HTTP)进行简单的转换,并生成Request
和Response
,然后将两个对象交给Adapter
,Adapter
主要作用便是将接受的request(org.apache.coyote.Request)
,response适配为容器需要的request(org.apache.catalina.connector.Request),response
,然后将对象转发给Service
中的容器进行后续的处理,容器由上到下分别为Engine
->Host
->Context
->Servlet
每个容器中,还可以自定义不同的业务处理,Tomcat
通过责任链模式可以很方便的修改业务处理逻辑。最主要的类便是valve
和pipeline
.
10. Tomcat 热部署原理
我们知道Tomcat
是具有热部署功能的,在Tomcat
中,首先它定义了自己的ClassLoader
,其次,Tomcat
每次在部署JSP
文件的时候,都会记录文件的修改时间的时间戳,每当经过设定时间后(intmodificationTestInterval
),Tomcat
都会检测JSP
文件当前的最后一次修改的时间戳,当时间戳不相同的时候,便重新加载JSP
文件。
11. Tomcat 后台定时管理
在Tomcat
中,存在很多后台定时任务,比如扫描 JSP
,查看Session
是否过期等,最开始Tomcat
为每个任务都单独的设置了一个定时线程,后来发现这样比较耗费性能,后来通过监听者模式修改成了整个系统共用一个定时任务,其他定时任务只用监听这个系统,当触发了指定的时间后,自己启动线程执行任务即可。