主要分四部分:
- 套娃式架构设计
- 架构属于设计层次,源码是对设计的实现
- Tomcat的架构设计比较独特,属于套娃式架构设计
- 源码剖析的原则、方法和技巧
- Tomcat实例构建(启动过程源码分析)
- Servlet请求处理链路(Servlet如何被Tomcat处理的)
一、Tomcat架构设计
功能是从需求的角度进行描述的,架构是从设计的角度进行描述的,源码是从实现的角度进行描述的
1.1 Tomcat的功能(需求)
Tomcat两个非常重要的功能(身份)
- Http服务器功能:Socket通信(TCP/IP)、解析Http报文
- Servlet容器功能:有很多Servlet(自带系统级Servlet+自定义Servlet),Servlet处理具体的业务逻辑
1.2 Tomcat的架构(架构就是为了完成功能需求做的设计)
1.2.1 架构解读
什么是Tomcat架构: 为了实现上述的功能,Tomcat进行了很多的封装设计,封装出了很多的组件(组件在源代码中的体现就是Java类),组件与组件之间的关系就构成了所谓的Tomcat架构。
一个Service内部可以有多个Connector组件,因为一个Connector绑定一个端口进行监听,多个Connector可以监听多个端口,但是一个Service内部的多个Connector只能对应一个Servlet容器。
除了Connector组件和Container组件,Tomcat其实还定义了很多其他组件来工作(server-service-connector/container-engine-host-context-wrapper)。这些组件采用一层套一层的设计方式(套娃式),如果一个组件包含了其他组件,那么这个组件也称之为容器。
1.2.2 架构与配置文件的对应
conf目录下的server.xml文件:
- Server:Server容器就代表一个Tomcat实例(Catalina实例),其下可以有一个或者多个Service容器;
- Service:Service是提供具体对外服务的(默认只有一个),一个Service容器中又可以有多个Connector组件(监听不同端口请求,解析请求)和一个Servlet容器(做具体的业务逻辑处理);
- Engine和Host:Engine组件(引擎)是Servlet容器Catalina的核心,它支持在其下定义多个虚拟主机(Host),虚拟主机允许Tomcat引擎在将配置在一台机器上的多个域名,比如www.baidu.com、www.bat.com分割开来互不干扰;
- Context:每个虚拟主机又可以支持多个web应用部署在它下边,这就是我们所熟知的上下文对象Context,上下文是使用由Servlet规范中指定的Web应用程序格式表示,不论是压缩过的war包形式的文件还是未压缩的目录形式;
- Wrapper:在上下文中又可以部署多个servlet,并且每个servlet都会被一个包装组件(Wrapper)所包含(一个wrapper对应一个servlet)。
1.3 Tomcat套娃式架构设计的好处
- 一层套一层的方式,其实组件关系还是很清晰的,也便于后期组件生命周期管理
- tomcat这种架构设计正好和xml配置文件中标签的包含方式对应上,那么后续在解读xml以及封装对象的过程中就容易对应
- 便于子容器继承父容器的一些配置
二、源码剖析技巧
剖析源代码需要讲究一些原则,注意一些方法和技巧,否则很容易就在浩瀚的源代码海洋中迷失自己
好处:提高我们的架构思维、深入认识代码、深入理解一个项目/框架
原则:
- 定焦原则:抓主线(抓住一个核心流程去分析,不要漫无目的的去看源代码)
- 宏观原则:站在上帝的视角,先脉络后枝叶(切忌试图搞清楚看到的每一行代码)
方法和技巧
- 断点(观察调用栈)
- 反调(右键,Find Usages)
- 经验之谈(比如一些doXXX,service()…往往都是具体干活的一些方法)
- 见名思意(比如通过方法名称就可以联想到这个方法的作用)
- 多多实际动手操练,灵活运用上述方法技巧
三、Tomcat实例构建脉络
3.1 源码构建方式
说明:
- 基于Tomcat8.5.54的源码进行说明
- 添加了一个web_demo应用在webapps中用于演示,该项目只有一个名为resumeservlet的servlet
- Tomcat本身就是Java开发的软件,直接使用Tomcat的时候,Tomcat需要读取server.xml以及其他的配置文件,同时还需要找到它要去部署的工程项目。使用源码方式,依然如此
操作步骤:
步骤一:解压源码压缩包,得到目录apache-tomcat-8.5.54-src
下载地址:https://tomcat.apache.org/download-80.cgi
下载最下面的Source Code Distributions源码版本,我这里下载的是zip的
步骤二:进入apache-tomcat-8.5.54-src目录,创建一个pom.xml文件,文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>apache-tomcat-8.5.50-src</artifactId>
<name>Tomcat8.5</name>
<version>8.5</version>
<build>
<!--指定源目录-->
<finalName>Tomcat8.5</finalName>
<sourceDirectory>java</sourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<plugins>
<!--引入编译插件,指定编译级别和编码-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
<!--Tomcat是java开发的,封装了很多功能,它需要依赖一些基础的jar包-->
<dependencies>
<!--远程过程调用工具包-->
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<!--soap协议处理工具包-->
<dependency>
<groupId>javax.xml.soap</groupId>
<artifactId>javax.xml.soap-api</artifactId>
<version>1.4.0</version>
</dependency>
<!--解析webservice的wsdl文件工具-->
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<!--Eclipse Java编译器-->
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
<!--ant管理工具-->
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<!---easymock辅助单元测试-->
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
</dependencies>
</project>
步骤三:在apache-tomcat-8.5.54-src目录中创建source文件夹
步骤四:将conf、webapps目录移动到刚刚创建的source文件夹中
步骤五:将源码工程导入到IDEA中
IDEA中应当已经配置好Maven
步骤六:给tomcat的源码程序配置Run/Debug Configurations
注意这里要使用Java11,因为pom.xml中配置使用的是Java11
这里要给tomcat的源码程序启动类Bootstrap配置VM参数,因为tomcat源码运行也需要加载配置文件等
-Dcatalina.home=E:\Code\IdeaProjects\apache-tomcat-8.5.54-src/source
-Dcatalina.base=E:\Code\IdeaProjects\apache-tomcat-8.5.54-src/source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=E:\Code\IdeaProjects\apache-tomcat-8.5.54-src/source/conf/logging.properties
填写VM参数时根据自己tomcat源码项目放置位置进行更改
步骤七:Build项目,此时会报错
找到错误位置,使用Alt+Enter第一条即可修复该问题,然后重新Build即可
步骤八:运行项目就启动了tomcat,启动时会加载所配置的conf目录下的server.xml等配置文件,所以访问8080端口即可,但此时会遇到如下的一个错误:
原因是Tomcat源码中Jsp引擎Jasper没有被初始化,从而无法编译处理Jsp(因为Jsp是需要被转换成servlet进一步编译处理的),只需要在tomcat的源码ContextConfig类中的configureStart方法中增加一行代码将Jsp引擎初始化,如下
步骤九:重启Tomcat,正常访问即可。至此,Tomcat源码构建完毕。
3.2 启动过程源码剖析
3.2.1 生命周期统一管理组件LifeCycle
Tomcat要启动,肯定要把架构中提到的组件进行实例化(实例化创建–>销毁等:生命周期)。Tomcat中那么多组件,为了统一规范他们的生命周期,Tomcat抽象出了LifeCycle生命周期接口
LifeCycle生命周期接口方法:
LifeCycle生命周期接口的继承体系:
3.2.2 启动流程
(1)启动入口分析
startup.sh --> catalina.sh start --> java xxxx.jar org.apache.catalina.startup.Bootstrap(main) start(参数)
(2)启动流程图
(3)启动流程分析
① Bootstrap的main方法:
② Bootstrap的init方法:
经过前两步,设置了
catalinaDaemon = catalina对象
daemon = bootstrap对象
③ Bootstrap的load方法:
④ Bootstrap的start方法:
3.2.3 load初始化阶段
(1)流程图
(2)源码分析
① Catalina的load方法:
Server的init初始化:
② Digester的parse方法:
得到的root变量内容:
server相关值:
service相关值:
engine相关值:
得到的内容与解析的server.xml文件配置内容一致
③ LifecycleBase的init方法(使用了设计模式的模板方法):
此处的initInternal会跳转至实现该接口的具体类的对象中
④ StandardServer的initInternal方法:
会跳转至service的init方法中,即继续调用service实现的Lifecycle的init方法
⑤ StandardService的initInternal方法:
分别初始化service中的容器部分和connector部分
⑥ StandardEngine的initInternal方法:
⑦ ContainerBase的initInternal方法:
注:ContainerBase是StandardEngine、StandardWrapper、StandardContext和StandardHost的父类
⑧ Connector的initInternal方法:
绑定适配器:
初始化protocolHandler组件:
⑨ AbstractProtocol的init方法:
⑩ AbstractEndpoint的init方法:
⑪ NioEndpoint的bind方法:
3.2.4 start启动阶段
(1)流程图
与load过程很相似
(2)源码分析
① Catalina的start方法:
② LifecycleBase的start方法(同样使用了设计模式的模板方法):
④ StandardServer的startInternal方法:
⑤ StandardService的startInternal方法:
分别启动其中的engine和connector
3.2.4.1 Engine启动过程start源码分析
① StandardEngine的startInternal方法:
最后调用父类ContainerBase的startInternal方法
② ContainerBase的startInternal方法:
Engine调用它父类执行该方法时,子容器为Host:
将子容器Host提交到具体的StartChild线程类并行执行
注:该线程池只专门用来实例化Host
Host调用它父类执行该方法时,通过设置生命周期事件来进行实例化
③ StartChild线程类:
④ StandardHost的startInternal方法:
⑤ LifecycleBase的setStateInternal方法:
⑥ LifecycleBase的fireLifecycleEvent方法:
触发Host的生命周期事件后,将后续工作交给生命周期监听器HostConfig来进行
⑦ Hostconfig的lifecycleEvent方法:
捕获start事件,执行start方法:
⑧ Hostconfig的start方法:
⑨ Hostconfig的deployApps方法:
根据不同的应用部署方式,调用不同的方法
⑩ Hostconfig的deployDirectories方法:
以线程方式并行处理多个项目:
⑪ DeployDirectory线程类:
⑫ Hostconfig的deployDirectory方法:
通过xml解析对象进行分析:
设置一些context应用的必要属性:
完善context的过程在addChild方法中
⑬ StandardHost的addChild方法:
⑭ ContainerBase的addChild方法:
⑮ ContainerBase的addChildInternal方法:
⑯ StandardContext的startInternal方法:
work目录存放jsp转换为servlet的中间过程临时文件
给每个应用设置类加载器:
关键步骤(把具体每个应用的处理交给了ContextConfig):
查看已初始化的Servlet,其中两个为tomcat默认的servlet,另一个resumeservlet为自己的项目web_demo中的servlet:
由上图看出,此时只读取了servlet对应的类,但并未生成实例化对象。
loadOnStartup方法根据web.xml中配置servlet的load-on-startup来进行创建实例化对应servlet。执行之后,instance就有具体对象了。
load-on-startup大于0时在容器启动时加载,否则在第一次访问该servlet时加载
⑰ StandardContext的loadOnStartup方法:
⑱ StandardWrapper的load方法:
⑲ StandardWrapper的loadServlet方法(实际实例化servlet的方法):
3.2.4.2 Connector启动过程start源码分析
① Connector的startInternal方法:
② AbstractProtocol的start方法:
③ AbstractEndpoint的start方法:
④ NioEndpoint的startInternal方法:
Tomcat中的NIO模型:
(1) 获取请求的工作封装交给了Acceptor线程去完成
(2) poller线程(下面分析servlet请求处理时的入口)
检查selector中是否有数据到来的channel,如果有就要进行处理
⑤ AbstractEndpoint的startAcceptorThreads方法:
⑥ NioEndpoint的createAcceptor方法:
⑦ Acceptor线程类中的run方法:
四、Servlet请求处理链路
一个servlet请求 --> 最终需要找到能够处理当前servlet请求的servlet实例 --> servlet.service()
4.1 Mapper组件体系结构
(1)结构图
Tomcat中使用Mapper机制重新封装了Host-context-wrapper(servlet)之间的数据和关系。
在匹配出能够处理当前请求的对应Host、对应Context和对应Wrapper之前,mapper对象肯定已经初始化好了
(2)源代码结构
① Mapper类中有MappedHost数组,表示有多个Host
② MappedHost中有一个ContextList
③ ContextList类中有MappedContext数组,表示该Host有多个Context
④ MappedContext中有一个ContextVersion数组
⑤ ContextVersion中的MappedWrapper数组对应的就是servlet
⑥ MappedHost、MappedContext和MappedWrapper都有一个MapElement基类
4.2 mapper对象数据何时初始化的
StandardService --> startInternal --> mapperListener.start()中完成mapper对象初始化
(1)StandardService的startInternal方法:
基于已有信息数据完成Mapper对象的初始化
此处的mapperListener也遵从统一的生命周期管理
(2)MapperListener的startInternal方法:
(3)MapperListener的registerHost方法:
此时的变量情况:
4.3 Servlet请求处理流程示意
(1)基本流程
(2)详细流程
Poller线程是追踪入口
4.4 Servlet请求处理源码剖析
(1) Poller的run方法:
(2)Poller的processKey方法:
在processSocket处设置断点:
(3)启动服务器,从浏览器发起访问请求
(4)AdstractEndpoint的processSocket方法:
把传入的socket交给一个线程进行处理
(5)线程类SocketProcessorBase的run方法:
(6)SocketProcessor的doRun方法:
此处的getHandler为中间过程对象
(7)ConnectionHandler的process方法:
取不到对应Processor的话
仍没有则创建一个新的
找到完成之后,使用其进行process
此时的processor为Http11Processor,因为默认的http协议是1.1版本。它是用来解析Socket中的请求信息的
(8)AbstractProcessorLight的process方法:
(9)Http11Processor的service方法:
此时获取得到的是CoyoteAdapter,连接器组件适配器
(10)CoyoteAdapter的service方法:
传入的参数req和res是原生的Request和Response,第一步先进行适配转换
转换完成后交给后面的Container中的具体组件处理,一层一层找到对应servlet,该流程对应了postParseRequest方法,即利用Mapper进行匹配查找,下面的(11)-(13)说明了这一过程
匹配完成后调用下面的invoke逐级调用匹配的结果,下面的(14)-(22)说明了这一过程
(11)CoyoteAdapter的postParseRequest方法:
(12)Mapper的map方法:
(13)Mapper的internalMap方法进行具体的相关匹配:
分别根据name匹配host、context和mapper
匹配到的结果存放在参数mappingData中(mappingDate就在request对象中):
- 方法执行匹配前:
- 方法执行返回后匹配到对应实例:
(14)StandardEngineValve的invoke方法:
(15)AbstractAccessLogValve的invoke方法:
(16)ErrorReportValve的invoke方法:
(17)StandardHostValve的invoke方法:
Request的gerContext方法:
继续深入调用context
(18)AuthenticatorBase的invoke方法:
(19)StandardContextValve的invoke方法:
Request的getWrapper方法:
(20)StandardWrapperValve的invoke方法:
可以看到,执行完成后,得到的servlet即为对应的servlet
(21)ApplicationFilterChain的doFilter方法:
(22)ApplicationFilterChain的internalDoFilter方法:
此处是最终执行servlet中实际操作的部分!!
自己编写的ResumeServlet的内容
- 执行该行代码语句之前:
- 执行该行代码语句之后: