Tomcat源码剖析笔记总结(超长干货——逐步分析)

主要分四部分:

  • 套娃式架构设计
    • 架构属于设计层次,源码是对设计的实现
    • Tomcat的架构设计比较独特,属于套娃式架构设计
  • 源码剖析的原则、方法和技巧
  • Tomcat实例构建(启动过程源码分析)
  • Servlet请求处理链路(Servlet如何被Tomcat处理的)

一、Tomcat架构设计

功能是从需求的角度进行描述的,架构是从设计的角度进行描述的,源码是从实现的角度进行描述的

1.1 Tomcat的功能(需求)

Tomcat的功能图
Tomcat两个非常重要的功能(身份)

  • Http服务器功能:Socket通信(TCP/IP)、解析Http报文
  • Servlet容器功能:有很多Servlet(自带系统级Servlet+自定义Servlet),Servlet处理具体的业务逻辑

1.2 Tomcat的架构(架构就是为了完成功能需求做的设计)

1.2.1 架构解读

什么是Tomcat架构: 为了实现上述的功能,Tomcat进行了很多的封装设计,封装出了很多的组件(组件在源代码中的体现就是Java类),组件与组件之间的关系就构成了所谓的Tomcat架构。
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.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 源码构建方式

说明:

  1. 基于Tomcat8.5.54的源码进行说明
  2. 添加了一个web_demo应用在webapps中用于演示,该项目只有一个名为resumeservlet的servlet
  3. Tomcat本身就是Java开发的软件,直接使用Tomcat的时候,Tomcat需要读取server.xml以及其他的配置文件,同时还需要找到它要去部署的工程项目。使用源码方式,依然如此

image

操作步骤:

步骤一:解压源码压缩包,得到目录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的内容

  • 执行该行代码语句之前:

  • 执行该行代码语句之后:

  • 20
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
作为一个AI机器人,我并没有能力进行源码分析。但是我可以提供一些关于Tomcat8.5的相关信息。 Tomcat8.5是Apache Tomcat的一个版本,是一个开源的Web容器,使用Java编写,可用于部署Java Web应用程序。Tomcat8.5增加了对Java Servlet 3.1、JavaServer Pages 2.3、Java Unified Expression Language 3.0以及WebSocket 1.1的支持。Tomcat8.5还增强了对SSL/TLS的支持,包括支持ALPN协议和OCSP Stapling。Tomcat8.5还提供了一些新的特性,如异步I/O和NIO2。 在Tomcat8.5源码中,主要涉及到的模块有:Catalina、Coyote、Jasper、WebSocket和Tomcat Native。Catalina模块是Tomcat的核心模块,负责处理HTTP请求和响应。Coyote模块负责处理HTTP请求的接收和响应。Jasper模块负责处理JSP页面的编译和执行。WebSocket模块提供了基于WebSocket协议的通信。Tomcat Native模块提供了对APR(Apache Portable Runtime)库的支持,可以提高Tomcat的性能和可靠性。 在Tomcat8.5源码中,还有一些其他的模块,如:Cluster、Manager、Realm、Valve等。Cluster模块提供了集群支持,可以让多个Tomcat实例共享会话信息。Manager模块提供了Web应用程序管理的功能。Realm模块提供了认证和授权的支持。Valve模块提供了一些过滤器和拦截器,可以对HTTP请求和响应进行处理。 总体来说,Tomcat8.5源码是一个庞大而复杂的项目,需要对JavaWeb开发有一定的了解才能进行源码分析。如果有兴趣了解Tomcat8.5的源码,可以先从官方文档和源码注释入手,逐步深入了解各个模块的实现原理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值