提示:tomcat整体架构分析,tomcat核心组件详解、tomcat请求全流程、tomcat设计模式分析。责任链模式设计、tomcat设计详解、tomcat调优的前置文档
前言
本来想搞个tomcat的调优,毕竟是高频面试题。不过要想调优,得先知道tomcat的底层架构,才能更方便的调优。本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!
一、相关概念
1、tomcat的概念
tomcat是一个servlet容器,同时也是一个web服务器。
2、web应用部署的3种方式
- 拷贝到webapps目录下
// 指定appBase
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
/**
host: 虚拟主机的相关配置,
name: localhost是本身,你也可以改成具体的ip或者
appBase: 这个目录下的文件会帮你部署,可以是相对路径,也可以是绝对路径
unpackWARs: true代表自动把war包给解压。
autoDeploy: 热部署(自动刷新)
- server.xml 的Context标签下配置Context
<Context docBase="D:\mvc" path="/mvc" reloadable="true">
/**
context: 一个context标签对应一个web应用。
path:指定访问该web应用的url入口。(context-path)
docBase:指定web应用的文件路径,可以给定绝对路径,也可以给定相对于<Host>中appBase属性的相对路径。例如:
docBase="F:\Resource\mvc\target\mvc-1.0-SNAPSHOT"
reloadable:如果这个属性设为true,tomcat服务器在运行状态下会监听在WEB-INF/classes和WEB-INF/lib目录下class文件的改动。
如果监听到class文件被更新的。服务器会自动重新加载web应用。
如果server.xml是下面这样的:
则代表的是 http://localhost:8080/mvc/user.do
其中你的controller写的是: @GetMapping(value = “user.do”)
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
<!-- http://localhost:8080/mvc/user.do path:context-path -->
<Context docBase="F:\Resource\mvc\target\mvc-1.0-SNAPSHOT"
path="/mvc" reloadable="true" />
- 在$CATALINA_BASE/conf/[enginename]/[hostname]/ 目录下,(默认是conf/Catalina/localhost)创建xml文件,文件名就是contextPath。比如创建mvc.xml,path就是/mvc
注意:想要用根路径访问,文件名为ROOT.xml
<Context docBase="D:\mvc" reloadable="true">
二、tomcat的整体架构
tomcat要实现两个核心功能
- 处理socket链接,负责网络字节流Request和Response对象转换
- 加载和管理Servlet,以及具体处理Request请求
因此Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事。连接器负责对外交流,容器负责内部处理。
1、tomcat架构图
从图中我们可以看到。
- Connector:Tomcat的连接器,用于接收请求并将其发送给容器。
- Container:Tomcat的容器,负责管理Servlet,Jsp和静态资源生命周期
- Engine:Tomcat的引擎,管理容器的生命周期和分配请求
- Host:Tomcat的主机,可以管理多个web应用程序的配置信息
- Context:Tomcat的上下文,用于管理多个web应用程序
- Servlet:Tomcat的Servlet,负责处理请求并生成响应
- JSP:Tomcat的JSP,用于动态生成web内容
一个tomcat中可以有多个service,每一个service可以分为容器Engine、链接Connector。
其中的容器Engine,每个容器有多个Host,每一个host都可以有多个context,然后每个context内又有多个servlet(tomcat将servlet封装成了Wrapper)
以 http://localhost:8080/mvc2 为例。
localhost,就会在多个host中找到是localhost的host,如果是指定的ip,则是具体的ip。8080,则会在这个host中找到8080的context。然后找到mvc2的servlet。然后去处理servlet中的逻辑,然后返回结果。
2、tomcat核心组件详解
2.1、server组件
指的就是整个Tomcat服务器,包含多组服务(services),负责管理和启动各个service,同时监听8005端口发送过来的shutdown命令,用于关闭整个容器
2.2、service组件
每个service组件都包含若干个用于接收客户端消息的Connector和处理请求的Engine组件。每个service组件还包含了若干个Executor组件,每个exevutor都是一个线程池,它可以为service内所有组件提供线程池执行任务。一个tomcat内可以有多个service,这样也是基于灵活性的考虑。通过一个tomcat内配置多个serivce,可以实现通过调用不同的端口去访问同一个机器上面的不同应用。
2.3、连接器Connector组件
Tomcat与外部世界的连接器,监听固定端口接收外部请求,传递给Container,并将Container处理的结果返回给外部。连接器对servlet容器屏蔽了不同的应用层协议及I/O模型,无论是Http还是AJP,在容器中获取到的都是一个标准的ServletRequest对象。
2.4、容器Container组件
容器,就是用来装载东西的器具,在tomcat中,容器就是用来装载Servlet的。Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。
- Engine : 引擎,Servlet顶层容器,用来管理多个虚拟站点,一个Service最多只有一个Engine;
- Host : 虚拟主机,负责web应用的部署和Context的创建。可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可以部署多个Web应用程序
- Context: Web应用上下文,包含多个Wrapper,负责web配置的解析、管理所有的web资源,一个Context对应一个web应用程序
- Wrapper:表示一个Servlet,最底层的容器,是对Servlet的封装,负责Servlet实例的创建、执行和销毁
可以对着Tomcat中的server.xml来理解:
<Server> //顶层组件,可以包括多个Service
<Service> //顶层组件,可包含一个Engine,多个连接器
<Connector/>//连接器组件,代表通信接口
<Engine>//容器组件,一个Engine组件处理Service中的所有请求,包含多个Host
<Host> //容器组件,处理特定的Host下客户请求,可包含多个Context
<Context/> //容器组件,为特定的Web应用处理所有的客户请求
</Host>
</Engine>
</Service>
</Server>
tomcat启动期间会通过解析server.xml,利用反射创建相应的组件,所以xml中的标签和源码一一对应。
3、请求定位Servlet的过程
Tomcat是用Mapper组件来完成这个任务的。Mapper组件的功能就是将用户请求的url定位到一个Servlet,他的工作原理是:Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里Servlet映射的路径,你可以想象这些配置信息就是一个多层次的map,当一个请求到来时,Mapper组件通过解析请求url里面的域名和路径,再到自己保存的Map里面查找,就能定位到一个Servlet。一个请求url最只会定位到一个Wrapper容器,也就是一个Servlet。
三、Tomcat架构设计精髓分析
1、Connector高内聚低耦合设计
优秀的设计应考虑高内聚、低耦合
- 高内聚是指相关度比较高 功能要尽可能的集中,不要分散
- 低耦合是指两个相关的模块要紧可鞥减少依赖的部分和降低依赖程度,不要让俩个模块产生强依赖
Tomcat连接器要实现的功能:
- 监听网络端口
- 接收网络连接请求
- 读取请求网络字节流
- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象
- 将Tomcat Reuqest对象转为标准的ServletRequest
- 调用Servlet容器,得到ServletResponse
- 将ServletResponse转成TomcatResponse对象
- 将Tomcat Response转为网络字节流
- 将响应字节流写回给浏览器
分析连接器的详细功能,我们会发现连接器需要完成3个高内聚功能:
- 网络通信
- 应用层协议解析
- Tomcat Request/Response与ServletRequest/ServletResponse的转换
因此,Tomcat的设计者设计了3个组件来分别实现这3个功能,分别是:EndPoint、Processor、Adapter
- EndPoint 负责提供字节流给Processor
- Processor负责提供Tomcat Request对象给Adapter
- Adapter 负责提供ServletReqest对象给容器
除了这些变化点,系统也存在一些相对稳定的部分,因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分,抽象基类 AbstractProtocol 实现了 ProtocolHandler 接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol 和 AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类
1.1、ProtocolHandler
连接器用ProtocolHandler来处理网络连接和应用层协议,包含了2个重要组件:EndPoint和Processor
连接器用ProtocolHandler接口来封装通信协议和I/O模型的差异,ProtocolHandler内部又分为EndPoint和Processor模块,EndPoint负责底层Socket通讯,Processor负责应用层协议解析,连接器通过适配器Adapter调用容器。
1.2、EndPoint
endPoint是通讯端点。即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此,EndPoint是用来实现TCP/IP协议的。EndPoint是一个接口,对应的抽象实现类是AbstractEndpoint,而AbstractEndpoint的具体子类,比如在NoiEndpoint和Nio2Endpoint中,有两个重要的子组件:Acceptor和SocketProcessor、其中Acceptor用于监听Socket连接请求,SocketProcessor用于处理接收到的Sovket请求,他实现Runanble接口,在Run方法里调用协议处理组件Processor进行处理,为了提供处理能力,SovketProcessor被提交到线程池来执行,而这个线程池叫做执行器(Executor)。
1.3、Processpr
Processor用来实现HTTP/AJP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。Processor是一个接口,定义了请求的处理等方法,他的抽象实现类AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现,具体的视线有AJPProcessor、HTTP11Processor等,这些具体的实现类实现了特定协议的解析方法和请求处理方式。
EndPoint接收到Soket连接后,生成一个SocketProcessor任务提交到线程池去处理,SovketProcessor的Run方法会调用Processor组件去解析应用层协议,Processor通过解析生成的Request对象后,会调用Adapter的Service方法。
1.4、Adapter
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Reuquest类。但是这个Request对象不是标准的ServletRequest,也就一位这,不能用TomcatRequest作为参数来调用容器,Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模型的经典运用,连接器调用CoyoteAdapter的Service方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转换为ServletRequest,在调用容器的Service方法。
2、设计复杂系统的基本思路
首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象类中定义目标方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。
3、父子同期组合模式设计
(tomcat设计了4中容器:Engine、Host、Context、Wrapper,tomcat是怎么管理这些容器的?)
tomcat采用组合模式来管理这些容器,具体的实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使用用户对单容器对象和组合同期对象的使用具有一致性。(我们从2.1的架构图可以知道每个services下都有一个engine,每个engine下都有多个Host,每个host下面都有多个context,每个context下都有多个wrapper,让这4个都继承一个公共的类Container,这样就能用一个Container来表示这4个容器了,不同的实现)
Container 接口定义如下:
public interface Container extends Lifecycle{
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}
4、PipeLine-Valve责任链模式设计
连接器中的Adapter会调用容器的Service方法来执行Servlet,最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,会把请求传递给Host继续处理,以此类推,最后这个请求会传给Wrapper容器,Wrapper会调用最终的Servlet来处理。这个调用过程是怎么实现的呢?就是用Pipeline-Vavle管道。
4.1、PipeLine-Vavle责任链模式
PipeLine-Vavle是责任链模式,责任链模式是指一个请求处理的过程中,有很多的处理者依次对请求进行处理,每个处理者负责做自己对应的处理,处理完了疑惑,在调用下一个处理者继续处理
4.2、为什么要使用管道机制?
因为一个比较复杂的大型系统重,如果一个对象需要经过很复杂的逻辑处理,直接处理这些复杂的业务逻辑,扩展性,可重用性都是很差的。更好的解决方案是采用管道机制,用一条管道把多个对象(这些对象是指阀门部件,也就是这个很复杂的逻辑拆出来的一小块一小块的逻辑处理)连接起来,整体看起来就像若干个阀门嵌套在管道中一样,而处理逻辑放在阀门上。
PipeLine-Vavle责任链模式,从名字也能看出来,就俩组件,一个管道(pipeline)、一个阀门(vavle)。
4.3、Vavle接口设计
由于Pipeline是为容器设计的,所以他在设计时加入了一个Contained接口,就是为了制定当前Pipeline所属的容器
tomcat源码: org.apache.catalina.Pipeline
public interface Pipeline extends Contained {
// 基础的处理阀
public Valve getBasic();
public void setBasic(Valve valve);
// 对节点(阀门)增删查
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
// 获取第一个节点,遍历的起点,所以需要有这方法
public Valve getFirst();
// 是否对所有节点(阀门)都支持处理Servlet3异步处理
public boolean isAsyncSupported();
// 找到所有不支持Servlet3异步处理的阀门
public void findNonAsyncValves(Set<String> result);
}
Pipeline维护了Valve链表,Valve可以插入到Pipeline中,对请求做某些处理,整个调用链的触发是Vavle来完成的,Valve完成自己的处理后,调用getNext.invoke()来触发下一个Vavle调用,每个容器都有一个Pipeline对象,只要触发这个Pipeline的第一个Valve,这个容器里Pipeline的Valve就会被调用到。Basic Valve处于Valve链表的末端,他是Pipeline中必不可少的一个Valve,负责调用下层容器的Pipeline里的第一个Valve。
- 当Adapter来请求了以后,先进入到Engine层,然后engine能够拿到(Pipeline的)getFirst,拿到第一个valve
- 然后从在engine中,就会一级一级的,从first拿到最后一个Basic
- 然后engine的basic就会getHost,拿到新的pipeline中的getFirst,然后再一级一级的拿到basic。。。直到拿到最下面的basic。(当然,每次都是先拿到管道pipeline,然后在用pipeline中的getFirst,然后invoke,然后nextInvoke。。。一级一级的往下走)
整个调用过程由连接器中的Adapter触发的,他会调用Engine的第一个Vavle:
真要通俗的讲,可以认为,先(每层都有pipeline)用管道pipiline中的getFirst拿到第一个,然后invoke,nextinvoke,然后直到basic,到basic了,在获取下一层(engine找host,host找context,context找wrapper)的pipeline,然后下一层的管道在getFirst,在invoke。。。
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFilter(),最终会调到Servlet的service方法
filterChain.doFilter(request.getRequest(), response.getResponse());
4.4、Valve和Filter的区别:
- Valve是Tomcat的私有机制,与Tomcat的基础架构/API是紧耦合的,Servlet API是共有的标准,所有的web容器包裹jetty都支持Filter机制。
- Valve工作在web容器级别,拦截所有应用的请求;而Servlet Filter工作在应用级别,只能拦截某个web应用的所有请求
管道/阀门 | 过滤器链/过滤器 |
---|---|
管道(Pipeline) | 过滤器链(FilterChain) |
阀门(Valve) | 过滤器(Filter) |
底层实现为具有头、尾指针的单向链表 | 底层实现为数组 |
Valve的核心方法invoke(request,response) | Filter核心方法doFilter(request,response,chain) |
pipeline.getFilter().invoke(request,respnse) | filterchain.doFilter(request,response) |
链表嘛,有单向链表、双向链表的,只是因为我们的请求是单向的,因此他是设计的单向的。
请求是单向的,因此我们链表单向的就行,就是值有next,没有pre。
5、Tomcat生命周期设计
tomcat里面有很多的组件,那这么大的系统,怎么管理这些组件呢?而且tomcat是怎么管理这些组件的生命周期呢,而且他还要支持热加载、热部署、后续可能还有针对于组件的扩展啊之类的,所以他的设计一定要扩展性比较高,要符合开闭原则(就是说你可以新增类,但是不能改旧类的逻辑)
5.1、一键式启停: LifetCycle接口
系统设计就是要找到系统的变化点和不变点。这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程。而变化点是每个组件的具体的初始话方法是不同的。因此我们把不变点抽象出来为一个接口,这个接口和生命周期有关,叫做LifeCycle。这个接口有这些方法:init()、start()、stop()、destory(),每个具体的组件去实现这些方法。
public interface Lifecycle {
/** 第1类:针对监听器 **/
// 添加监听器
public void addLifecycleListener(LifecycleListener listener);
// 获取所有监听器
public LifecycleListener[] findLifecycleListeners();
// 移除某个监听器
public void removeLifecycleListener(LifecycleListener listener);
/** 第2类:针对控制流程 **/
// 初始化方法
public void init() throws LifecycleException;
// 启动方法
public void start() throws LifecycleException;
// 停止方法,和start对应
public void stop() throws LifecycleException;
// 销毁方法,和init对应
public void destroy() throws LifecycleException;
/** 第3类:针对状态 **/
// 获取生命周期状态
public LifecycleState getState();
// 获取字符串类型的生命周期状态
public String getStateName();
}
在父组件的init()方法里需要创建子组件并调用子组件的init()方法,同样,在父组件的start()方法中也需要调用子组件的start()方法,因此调用者可以无差别调用各组件的init()方法和start()方法,这就是组合模式的使用。只需要调用最顶层组件,也就是server组件的init()和start()方法,整个tomcat就被启动起来了。
5.2、可扩展性:LifeCycle事件
各个组件的实现都是复杂多变的,如果将来需要增加新的逻辑,直接修改start()方法吗?这样会违法开闭原则。组件的init()和start()调用是由他的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化,上层组件的启动会触发子组件的启动,因此我们把组件的生命周期定义为一个个的状态,把状态的转变看做是一个事件,而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式。
其实就是在LifeCycle接口里加入俩方法:添加监听器和删除监听器。
5.3、观察这模式
监听器,观察者模式,每个组件都可以实现自己的监听,只要让组件去实现这个生命周期的接口就行(他里面有监听器的接口,只需要去实现就行)
我们还需要定义一个Enum来表示组件有哪些状态,以及处在什么状态会触发什么样的事件
5.4、组件的生命周期状态变化
组件的生命周期状态变化如下:
5.5、重用性
LifeCycleBase抽象基类
一般来说,实现类不止一个,不同的类在实现接口时往往会有一些相同的逻辑,如果让各个子类都去实现一遍,就会有重复的代码。那子类如果重用这部分逻辑呢?就是定义一个基类来实现共同的逻辑,然后让各个子类去继承他,就达到了重用的目的。而基类中往往定义一些抽象方法,所谓的抽象方法就是说基类不会去实现这些方法,而是调用这些方法来实现骨架逻辑。
Tomcat定义一个基类LifeCycleBase来实现LifeCycle接口,把一些公共的逻辑放到基类中去,比如生命状态的转变与维护、生命周期时间的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法。为了避免跟基类中的方法同名,我们把具体子类的实现方法改个名字,在后面加上Internal,叫initlenternal()、startInternal()等。
5.6、模版设计模式
模版设计模式(骨架抽象类和模版方法):LifeCycleBase实现了LifeCycle接口中的所有方法,还定义了相应的抽象方法交给具体子类去实现。
总结
tomcat的整体架构就分析完毕了,我们下一步就是,tomcat调优应该怎么搞,哈哈,是不是还蛮期待的。毕竟各种调优,各种原理,是面试官最喜欢问的问题。当然,了解底层原理,也有助于我们后续解决问题。