本文基于Tomcat 7,参考了 汪建的《Tomcat内核设计剖析》,也许对理解Tomcat知识体系、面试有所帮助。
Servlet规范
所谓Servelt规范,是一系列的接口和约定的说明。Tomcat实现了Servlet规范,因此有必要先大致了解一下Servlet规范。
1. Servlet接口
核心接口:javax.servlet.Servlet
public interface Servlet {
void init(ServletConfig config) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
String getServletInfo();
void destroy();
}
常用Servlet:HttpServelt
生命周期:
- 加载实例化(由Web容器完成)
- 初始化(init)
- 处理请求(service)
- 销毁(destroy)
一个问题,想想看:
Servlet是单例吗?
一般来说,在Servlet容器中,每个Servlet类智能对应一个Servlet对象,所有请求都由同一个Servlet对象处理! 因此要小心并发问题!
当客户端第一次请求某个Servlet时,Servlet容器将会根据web.xml的配置实例化这个Servlet对象。当有新的客户端请求到来时,一般不会再实例化该Servlet类,也就是说会有多个线程在使用这个Servlet实例。
2. ServletRequest、ServletResponse
没啥好讲的
3. ServletContext
一个Web应用对应一个ServeltContext
接口的实例
4. Filter
涉及到一个设计模式:责任链模式
5. 会话
Servlet并没有提出协议无关的会话规定,每个协议需要自己规定。
HTTP对应的会话接口为 HttpSession
。实现有两种:
- Cookie是常用的会话跟踪机制,其中Cookie的标准名字必须为JSESSIONID
- URL重写,即在URL后面添加一个jsessionid参数,不建议使用
5. web应用
当一个Web应用程序部署到容器中时,在Web程序开始处理客户端请求之前,必须按照下列步骤顺序执行:
- 实例化web.xml中
<listener>
元素标识的每个事件监听器的一个实例 - 对于已实例化且实现了
ServletContextListener
接口的监听器实例,调用其contextInitialized()
方法 - 实例化web.xml中
<filter>
元素标识的每个过滤器的一个实例,并调用每个过滤器的init方法来初始化 - 根据Servlet配置的load-on-startup属性大小,按照顺序实例化Servlet,并调用其init()方法(load-on-startup只有为正数才能在容器启动时加载,且数值越小越优先)
6. Servlet映射
没啥好说的,Map
Tomcat整体架构及组件
Tomcat内部主要组件
如下图
Tomcat请求流转过程
如下图
流程说明:
- 当Tomat启动后,Connector组件的接收器(Acceptor)将会监听是否有客户端套接字连接并接收Socket
- 一旦接收到客户端的连接,则将连接交给线程池Executor处理,开始执行请求响应任务
- Http11Processor组件负责从客户端连接中读取消息报文,然后开始解析HTTP的请求行、请求头部、请求体。将解析后的报文封装成Request对象,方便后面处理时通过Request对象获取HTTP协议的相关值
- Mapper组件根据HTTP协议请求行的URL属性值和请求头部的Host属性值匹配该有哪个Host、哪个Context、哪个Wrapper容器来处理请求
- CoyoteAdaptor组件负责将Connector组件和Engine容器连接起来,把前面处理过程中生成的请求对象Request和响应对象Response传递到Engine容器,调用它的管道。
- 后续Engine、Host、Context和Wrapper容器依次处理请求,并且处理时均要通过相关容器的阀门(Valve)
- Wrapper容器对应的Servlet对象处理完氢气后,会将结果输出到客户端中
对照上述的组件图,下面分别简单介绍一下几个重要的组件
Server
Tomcat最顶级的组件,代表Tomcat的运行实例,在一个JVM中只有包含一个Server。
一个Server可能会包含多个Service
对应配置文件为server.xml
Server组件图
几个重要的组件
下面介绍几个比较重要的组件
1. JreMemoryLeakPreventionListener
该组件主要是用来解决JRE库内存泄露问题的。
1.1 问题描述
Tomcat中重加载一个Web应用是通过实例化一个新的ClassLoader来实现的。如果旧的ClassLoader无法被回收,就会造成内存泄露。
案例一:如果Web应用中一些生命周期很长的单例对象(如 JRE库中的某些类)引用了Tomcat的ClassLoader(如 WebappClassLoader),且该单例对象会在Java程序整个运行周期内存在的话,就可能导致无法卸载该ClassLoader,从而出现内存泄露的情况。
案例二:如果Tomcat的ClassLoader被某个线程持有,然后该线程启动了一个新线程,并且新线程无限循环(如 JRE库中的 Disposer),那么新线程也会继承父线程的ClassLoader,从而造成Tomcat的ClassLoader无法被回收,出现内存泄露的情况。
1.2 解决方法
JreMemoryLeakPreventionListener
则是用来解决这个问题的。它的主要作用是,在加载那些特殊的对象时,使用系统自己的ClassLoader,而不是Tomcat的ClassLoader。等到这些特殊的对象加载结束后,再将ClassLoader还原。这样就可以解决这个问题了。
主要功能代码如下:
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try
{
// Use the system classloader as the victim for all this
// ClassLoader pinning we're about to do.
Thread.currentThread().setContextClassLoader(
ClassLoader.getSystemClassLoader());
// 加载一些特殊的对象
} finally {
Thread.currentThread().setContextClassLoader(loader);
}
2. ThreadLocalLeakPreventionListener
这个listener主要是用来解决ThreadLocal
内存泄露问题的
2.1 问题描述
如果Tomcat Web应用的线程池ThreadLocal对象引用了其ClassLoader(如 WebappClassLoader),由于一般线程池生命周期比较长,若此线程池没有释放,则可能会导致Web应用重加载后,旧的ClassLoader无法释放,从而造成内存泄露。
2.2. 解决方法
通过ThreadLocalLeakPreventionListener
类监听Server stop事件,如果发生了Server stop事件,则将线程池内的所有线程销毁,这样就可以解决这个问题了。
主要功能代码如下:
Engine engine = (Engine) context.getParent().getParent();
Service service = engine.getService();
Connector[] connectors = service.findConnectors();
if (connectors != null) {
for (Connector connector : connectors) {
ProtocolHandler handler = connector.getProtocolHandler();
Executor executor = null;
if (handler != null) {
executor = handler.getExecutor();
}
if (executor instanceof ThreadPoolExecutor) {
ThreadPoolExecutor threadPoolExecutor =
(ThreadPoolExecutor) executor;
threadPoolExecutor.contextStopping();
} else if (executor instanceof StandardThreadExecutor) {
StandardThreadExecutor stdThreadExecutor =
(StandardThreadExecutor) executor;
stdThreadExecutor.contextStopping();
}
}
}
shutdown命令
Tomcat启动后,主线程会进入等待shutdown命令的环节,它会不断尝试读取客户端发送过来的消息,一旦匹配上 shutdown 命令,则会跳出循环,关闭Tomcat。而daemon线程则负责接收客户端请求,处理客户端报文。
图示如下:
相关源码如下:
org.apache.catalina.startup.Catalina#start
if (await) {
await();
stop();
}
org.apache.catalina.core.StandardServer#await
// Match against our command string
boolean match = command.toString().equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
break;
} else
log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
Service
Service是服务的抽象,它代表请求从接收到处理的所有组件的集合。
一个Service可能会包含若干个Connector、一个Engine、若干Executor线程池组件
Connector
Connect的职责:接收客户端连接,进行客户端请求处理
JIoEndpoint
JIoEndpoint为BIO模式,主要处理流程如下:
关注几个类方法:
Connector
的初始化:org.apache.catalina.connector.Connector#initInternal
Connector
的启动:org.apache.catalina.connector.Connector#startInternal
Acceptor
的处理:org.apache.tomcat.util.net.JIoEndpoint.Acceptor#run
SocketProcessor
的处理:org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor#run
NioEndpoint
NioEndpoint为非NIO模式,主要处理流程如下:
几个关键组件:
LimitLatch:负责对连接数的控制,基于AQS并发框架实现
Acceptor:负责接收套接字连接并注册到通道队列里
Poller:负责轮询检查事件列表
SocketProcessor:任务定义器
Executor:负责处理套接字的连接池
AprEndpoint
AprEndpoint为本地库IO模式(比较复杂,感觉不用特别关注),主要处理流程如下:
AprEndpoint有两种处理模式:
- 如果操作系统支持
TCP_DEFER_ACCEPT
参数,则Acceptor通过APR获取到套接字后,直接创建SocketWithOptionsProcessor
对象,最后直接放到线程池中执行套接字的读写和逻辑处理,整个过程都是阻塞的。 - 如果操作系统不支持
TCP_DEFER_ACCEPT
参数,则Acceptor通过APR获取到套接字后,将套接字放到待轮询队列PollSet中,而PollSet则不断通过APR检测已准备好的套接字,接着创建SocketProcessor
对象,最后放入线程池中执行,接下来的过程也是阻塞的。
TCP_DEFER_ACCEPT
参数优化:
使用了TCP_DEFER_ACCEPT
参数优化的区别在于,三次握手之后,最后一次的ACK后连接并不会被接收,而是当客户端数据发送到来时才会被接收。这样一来有一个好处,就是连接只要被接收就肯定有数据。
Engine
Engine代表全局Servlet引擎
一个Engine可以包含若干个Host容器
Host
Host组件代表虚拟主机
一个Host虚拟主机可以存放若干Web应用的抽象(即 Context容器)
HostConfig组件
Host容器存放的是Context级别的容器(即 Web应用)。当Tomcat启动后,需要加载Web应用,设置相关的属性和配置,这其中就要用到HostConfig
。
HostConfig
是Host容器的生命周期监听器,它实现了org.apache.catalina.LifecycleListener
接口,在其 lifecycleEvent
方法中,会监听 Tomcat 启动、停止等操作,然后进行相应的操作。
HostConfig 的一个重要的功能就是 Context容器(即 Web应用)的部署,其支持以下类型的Web应用部署。
- Descriptor描述符类型
- War包类型
- 目录类型
其中Descriptor类型的部署,支持 Web应用的重部署
和重加载
;而 War包类型和目录类型,只支持 Web应用的重加载
。
Context
Context组件是Web引用的抽象。(我们开发的Web应用最终部署到Tomcat中就会对应一个Context对象)
一个Context容器会有若干个Servlet(即 Wrapper容器)
比较重要的组件:
- Loader组件:Web应用加载器,用于加载Web应用的资源,它要保证不同Web应用之间的资源隔离
- Manager组件:会话管理器,用于管理对应Web容器的会话
- NamingResource组件:命名资源,它负责将Tomcat配置文件的server.xml 和 Web应用的 context.xml资源和属性映射到内存中
- Mapper组件:Servlet映射器,它属于Context内部的路由映射器,只负责该Context容器的路由导航
- 过滤器组件:
FilterDef
、ContextFilterMaps
、ApplicationFilterConfig
TODO - WebappClassLoader:类加载器的隔离 TODO
- ApplicationContext:是Servlet规范中的ServletContext接口的标准实现,表示某个Web应用的运行环境。
- ServletContainerInitializer:Web容器启动时,让第三方组件做一些初始化工作
- MemoryLeakTrackingListener监听器:辅助完成关于内存泄露跟踪的工作。
Wrapper
Wrapper是Tomcat中最小的容器级别。
一般来说,一个Wrapper对应一个Servlet对象(即 所有线程共用同一个Servlet对象)。SingleThreadModel是例外,可能对应一个Servlet对象池(SingleThreadModel已经是Deprecated了)。
Servlet工作机制的大致流程是:Request -> StandardEngineValve -> StandardHostValve -> StandardContextValve -> StandardWrapperValve -> 实例化并初始化Servlet对象 -> 由过滤器链执行过滤操作 -> 调用该Servlet对象的service方法 -> Response
比较重要的组件:
- 过滤器链
- Comet模式的支持
- WebSocket协议的支持
- 异步Servlet(参考Dubbo异步调用)
根据请求资源的不同,Servlet可以分为三类:
- 普通Servlet,处理普通的业务逻辑请求
- JspServlet,访问JSP页面
- DefaultServlet,访问静态资源
Valve
管道模式类似于责任链模式,顾名思义,就像一条管道将多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,而处理逻辑就放在阀门上
。
Tomcat 4个级别的容器中,分别都有自己的基础阀门。如下:
StandardEngineValve
StandardHostValve
StandardContextValve
StandardWrapperValve
Tomcat中常用的阀门有:
AccessLogValve
:记录客户端访问日志
ErrorReportValve
:将错误以HTML格式输出的阀门
PersistentValve
:将请求会话持久化的阀门
RemoteAddrValve
:访问IP控制限制的阀门
RemoteIPValve
:代理或负载均衡处理的阀门
SemaphoreValve
:控制容器并发访问的饭呢
日志
Tomcat采用工厂模式(LogFactory)生成日志对象,底层使用JDK自带的日志工具(DirectJDKLog),而没有使用第三方日志工具,以减少包的引入。没有采用JDK日志工具的默认配置,而二十通过配置系统变量和重写某些类达到特定效果。
国际化
Tomcat日志的国际化,使用了JDK里面的三个类:MessageFormat、Locale、ResourceBundle,而Tomcat中引用StringManager类把这三个类封装起来,方便操作。而StringManager类采用了一个Java包对应一个StringManager对象的设计,折中的考虑使性能与资源得以同时兼顾。
Tomcat类加载
Tomcat 7中的类加载器,能够实现公共Jar的共享 和 各个Web应用之间的隔离。加载器的继承关系如下:
Tomcat中多了Common类加载器和WebApp类加载器。
- Common类加载器加载Tomcat公用的一些类与组件
- WebApp类加载是用于加载Web应用程序的,每一个类加载器负责加载一个Web应用
T
omcat启动的时候,就会将Common类加载器设置为线程上下文类加载器。
相关代码:
org.apache.catalina.startup.Bootstrap#init()
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
会话
Web容器的会话机制补充了HTTP协议的无状态性。Tomcat在服务端使用jsessionid 与 客户端的cookie配合,完成了其的会话机制。
其jsession传递方式一般有三种:
- Cookie方式
- 重写URL方式,即将jsessionid附加到url上
- 表单隐藏方式
Tomcat中Session相关的类:
- 标准会话对象:
StandardSession
- 增量会话对象:
DeltaSession
- 标准会话管理器:
StandardManager
- 持久化会话管理器:
PersistentManager
支持多种会话存储方式,主要针对的是单个会话的持久化,存储方式包括:- 文件方式存储
FileStore
- 数据库存储
JDBCStore
- 文件方式存储
- 集群增量会话管理器:
DeltaManager
- 全节点赋值
- 只复制会话增量
- 集群备份会话管理器:
BackupManager
- 集群的会话节点一般为:一主一备多备份
- 会话黏贴(Session Stick)机制:一种会话定位技术,即在Tomcat节点上生成一种包含位置信息的会话ID,它一般附带了Tomcat实例名,当客户端再次请求时,负载均衡器会解析会话ID中的位置信息并转发到相应节点上
- 集群RPC通信:
Tribes框架