文章目录
一、Tomcat的架构
1.1 架构简介
Tomcat总体架构设计的非常优雅。
首先一个Tomcat服务器实例代表一个Server,Server负责接收请求和处理请求并返回结果。Tomcat这地方将请求的接收和处理进行了解耦,每个Server分为Connector和Container两部分,Connector可以有N个,分别负责不同的协议。
同时每个Server可以部署多个项目,绑定多个端口,所以将Connector和Container封装为一个Service。
每个Service包含的Connector监听一个端口,负责处理该端口不同协议的请求,将不同协议的请求转化为统一的request。而Container每个Service只能持有一个,负责request的处理。(每个Connector绑定一个端口,不同Service绑定的端口不同,例如ServiceA可能持有两个Connector,分别绑定8080和8081,ServiceB持有一个Connector,绑定8083)
每个Container实际为一个Engine,每个Engine可以包含N个Host,对应不同的ip或者域名,每个Host包含N个Context,每个Context表示一个具体的Web应用,而每个Context包含N个Wrapper,每个Wrapper封装一个Servlet。
例如:www.aaa.com和www.bbb.com两个域名的请求都交给同一个Tomcat处理,则www.aaa.com:8080/test和www.bbb.com:8080/test就会交给绑定了8080这个端口的Service,然后根据www.aaa.com和www.bbb.com分别寻找Container中对应的Host,然后再从Host下分别找test这个Context。
各个组件的嵌套关系类似下图:
1.2 生命周期
同时,所有组件又都实现了顶层的接口Lifecycle,即生命周期管理接口,该接口有以下几个方法:
- init():初始化组件
- start():启动组件
- stop():停止组件
- destory():销毁组件
每个生命周期都可能对应几个状态的转换,可以为组件添加事件监听器(LifecycleListener)用于监听组件的状态变化。
1.3 Connector
Connector的主要职责:
- 监听端口
- 将客户请求按照指定协议解析
- 根据请求路径匹配Mapper,得到处理该请求的容器
- 将请求交给正确容器处理
- 返回结果
Connector持有一个ProtocolHandler,负责处理具体的协议,例如Http11NioProtocol表示基于NIO的Http 1.1协议处理器。Protocol又包含一个Endpoint负责启动Socket绑定端口并监听,和一个Processor负责按指定协议分析请求数据,并根据请求路径匹配具体的容器。
Processor是如何匹配容器的?这是根据Mapper来匹配的,Mapper负责维护容器映射信息,同时按照映射规则查找容器。
Mapper又是如何动态维护容器 的呢?通过MapperListener,MapperListener类实现了ContainerListener, LifecycleListener接口,可以在容器组件状态发生变化的时候,动态地注册或者取消容器组件的映射信息。(该组件由Service持有,监听该Service下所有的容器组件)
1.4 Pipeline和Value
为了每个Container组件的可扩展性,Tomcat的每个组件都采用了职责链的模式,即每个组件都持有一个Pipeline和一个Value,Pipeline负责构建Value链,Value负责进行请求的处理。
1.5 总体架构图
从图中可以看出,只论Tomcat的话,Tomcat从Bootstrap类main方法启动,该类会创建一个Catalina实例,Catalina持有一个Server的引用。Server持有所有Service引用,Service持有各自所有的Connector、Mapper、MapperListener和Container(Engine)的引用,MapperListener持有Mapper方便监听事件发生后动态更改Mapper。
1.6 WebAppClassLoader
每个web应用都有自己所属的web应用类加载器实例,作用是隔离各个web应用,提高了灵活性,让各个web应用互不影响。
二、Tomcat启动流程
2.1 流程图
2.2 流程
-
执行bootstrap的main方法。该方法创建一个Bootstrap对象,调用init()方法。init方法初始化一个ClassLoader,并通过ClassLoader创建一个Catalina对象,赋给catalinaDemon变量。
-
Catalina解析conf/server.xml,借助封装的Digester XML解析框架解析该XML文件,该框架会形成一个栈,然后创建完一个对象并全部设置好以后,即遇到end()方法会将对象出栈,Sever对象在栈底,当其出栈,整个Tomcat就设置完毕了。
- 创建Server对象并且添加生命周期监听器。
- 创建Service实例,添加Service的工作线程池
- 为Service添加生命周期监听器(默认无监听该组件的监听器),注意Service对象创建出来时就包含Mapper和MapperListener,其中将this传入MapperListener,让其实现对自己包含容器组件进行监听,具体的监听见server.start()这个步骤。
- 为Service添加Connector,调用service的addConnector,在添加Connector之前会先给Connector添加生命周期监听(默认无该组件监听器)
- 创建Engine实例,并为Engine添加集群配置、生命周期监听(HostConfig类)、 添加安全配置(设置默认Value)等等
- 创建Host实例,为Host添加集群配置、生命周期监听(HostConfig类)、添加安全设置(设置默认Value等等)
- 解析server.xml中的Context配置。(Context加载有2个入口,在解析server.xml构造server时通过
start()
方法构建是其中之一。另一个是HostConfig这个监听器来自动扫描部署目录,该方式包括3种具体的部署形式:①Context.xml部署 ② web目录部署 ③war包部署)。默认server.xml是不设置context的,因为这样很麻烦,每次web应用的改动都需要到tomcat安装目录下改动server.xml。 - Context解析主要的工作:为Context添加生命周期监听(具体类由< context>标签中的classname属性指定)、为Context指定类加载器(用于隔离web应用)、添加会话管理器、添加初始化参数、添加安全配置以及静态资源配置、为Context添加默认的拦截器Value(Spring MVC框架的入口)、为Context添加守护资源和Cookie处理器。
-
完成上述步骤后,Server对象已经创建完成,调用其
init()
方法,然后逐层调用子组件的init()
方法完成初始化 (包括MapperListener的初始化)。 -
调用server的
start()
方法,逐层调用子组件的start()
方法。当执行到Host的start()
方法时会发出START_EVENT事件,由Host持有的HostConfig生命周期监听器感知到并处理。(HostConfig负责处理的生命周期事件包括:START_EVENT、PERIODIC_EVENT、STOP_EVENT
,前两者与web应用部署相关,其中PERIODIC_EVENT
负责守护资源的监控,当守护资源发生变化时,重新部署web,STOP_EVENT
在Host停止时注销其对应的MBean)注意:在Service.start()时会调用MapperListener的start(),此时会根据MappListener持有的this指针,得到Engine,然后获取到所有的子Container,循环将这些子组件注册到MapperListener上,这样每个子组件调用addChild、removeChild等方法时,MapperListener可以感知到,然后动态维护容器组件的映射。
(此时web.xml已经解析完毕,Host、一些使用web.xml配置的context都已经解析完毕了,通过其余方式配置的context属于动态添加的,MapperListener通过addChild事件感知,并动态添加到Mapper中) -
HostConfig感知到START_EVENT事件后,会依次执行
deployDescriptors()
、deployWARs()
、deployDirectories()
方法完成按照context.xml文件部署web应用、按照war包部署web、按照web目录部署。public void lifecycleEvent(LifecycleEvent event) { try { this.host = (Host)event.getLifecycle(); if (this.host instanceof StandardHost) { this.setCopyXML(((StandardHost)this.host).isCopyXML()); this.setDeployXML(((StandardHost)this.host).isDeployXML()); this.setUnpackWARs(((StandardHost)this.host).isUnpackWARs()); this.setContextClass(((StandardHost)this.host).getContextClass()); } } catch (ClassCastException var3) { log.error(sm.getString("hostConfig.cce", new Object[]{event.getLifecycle()}), var3); return; } if (event.getType().equals("periodic")) { this.check(); } else if (event.getType().equals("before_start")) { this.beforeStart(); //感知到start事件 } else if (event.getType().equals("start")) { this.start(); } else if (event.getType().equals("stop")) { this.stop(); } } -------------------------------------------- public void start() { //....其他代码 if (this.host.getDeployOnStartup()) { this.deployApps(); } } ----------------------------- protected void deployApps() { File appBase = this.host.getAppBaseFile(); File configBase = this.host.getConfigBaseFile(); String[] filteredAppPaths = this.filterAppPaths(appBase.list()); this.deployDescriptors(configBase, configBase.list()); this.deployWARs(appBase, filteredAppPaths); this.deployDirectories(appBase, filteredAppPaths); }
-
按照context.xml部署web应用,会根据Host的xmlBase属性指定,若未指定去默认的目录
/conf/< Engine名称>/< Host名称>
下寻找。对于该目录下的多个context.xml,使用线程池完成解析。- 使用Digester解析配置文件,创建Context实例
- 添加生命周期监听器
- 通过Host的
addChild()
添加到Host中,在此过程中会判断Host是否已经启动,如果已经启动则直接启动context (调用start方法)。 - 将Context.xml、web应用目录、web.xml等添加到守护资源,以便文件发生变更时重新部署或者加载web应用。(文件变更通过文件现在修改时间与上次修改时间进行比较进行判断。 重新部署是当context.xml发生改变时进行,重新生成context实例,重新加载是web.xml发生改变,重启同一个context对象)
-
按照war包部署,对Host的appBase路径下所有符合条件的war包进行扫描,然后由线程池完成部署,过程大致同上一步
-
按照web目录部署,将web应用复制到Host指定appBase下即可。过程类似于前面的方式。
-
在
START_EVENT
的处理过程中创建完Context实例并添加到Host后,会调用start()方法(每个线程负责一个Context的创建和启动)。创建的Context默认实现来是standardContext类,其start()过程如下:- 发布正在启动的JMX通知,可以通过添加NotificationListener来监听web应用的启动。(JMX是JVM提供扩展,用于监控和管理java程序和jvm)
- 启动当前context维护的JNDI资源,如果使用JNDI,还会添加NamingContextListener
- 初始化WebResourceRoot并启动,该类维护了所有的web应用资源,主要用于类加载和按照路径查找资源文件。
- 创建web应用类加载器
- 设置默认的cookie处理器
- 设置字符集映射
- 初始化临时目录,然后检查依赖
- 启动安全组件(Realm)
- 发布
CONFIGURE_START_EVENT
事件,由该实例持有ContextConfig类监听并进行处理 (完成Servlet的创建)。(该监听器在创建Context实例时就已经被注入了,ContextConfig负责解析web.xml
以及寻找Context的ServletContainerInitializer
并添加到Context中。) - 将WebResourceRoot添加到ServletContext中
- 启动Context的子节点和Pipeline,创建会话管理器
- 创建实例管理器,用于创建Servlet、Filter等对象
- 启动添加到当前Context的ServletContainerInitializer (SpringBoot在外置Tomcat中启动使用此种方法)。该类的实例由前面处理
CONFIGURE_START_EVENT
事件的ContextConfig查找并添加到Context中。 - 实例化应用监听器(ApplicationListener),分为事件监听器和生命周期监听器两大类,这些监听器可以通过context.xml、web.xml、编程方式(ServletContainerInitializer)来添加到Context中,并触发其中
ServletContextListener
的contextInitialized()方法 (springboot在外置tomcat的启动方式,具体见后文)
。 - 启动会话管理器
- 实例化FilterConfig、Filter,并调用Filter.init()方法初始化
- 对于loadOnstartup≥0的wrapper,调用wrapper.load(),进行Servlet的实例化和初始化。(Servlet在第9步完成创建)
- 启动后台定时线程,监控守护资源的变更
- 发布正在运行的JMX通知
- 清理无用的资源
- 设置Context的状态,成功设置为STARTING,失败为FAILED
-
Tomcat启动完毕
三、Tomcat请求处理流程
3.1 请求流程图
3.2 请求流程
如果MVC框架,则会添加额外的逻辑,使得最终映射到DispatcherServlet,然后由DispatcherServlet持有的HandlerMapping匹配最终的处理器。
MVC框架的请求处理逻辑网上有很多,也可以参考:
此处详细分析一下Tomcat收到请求是如何匹配到对应的Container,此处的核心是Mapper。Mapper、Container、Connector三者是通过CoyoteAdapter联系在一起的(适配器模式)。其中Mapper、Container被Service持有,Connector持有Service的引用,Adapter持有Connector的引用。
具体流程:
- Connector接收请求,读取数据,封装为一个Request对象,并创建一个Response对象,然后将Request(
org.apache.coyote.Request
)和Response作为参数传入CoyoteAdapter.service()
- service()方法根据Connector发送的 Request和Response构造创建
org.apache.catalina.connector.Request
和org.apache.catalina.connector.Response
。 - 解析Request中的URI,并检查URI是否合法,不合法返回404
- 进行映射,将映射结果存储到Request的mappingData中。
- 进行映射时,首先会调用Adapter的postParseRequest()方法,该方法通过一个循环来进行匹配,每次循环中都会通过Mapper.map()方法进行匹配。(在调用前创建三个变量,version表示版本号,为null则表示匹配所有版本、versionContext暂存按照会话ID匹配的context、mapRequired表示是否继续映射,循环通过该变量控制)
- map()按照请求路径进行匹配。(先查找Host,再查找Context,最后查找Wrapper,匹配到的Host、Context、Wrapper都会加入到mappingData中)
- 进行一些判断(是否允许追踪、鉴权、认证等)
- 开始执行,过程如下: