Tomcat的架构与源码分析学习笔记

举报
长路 发表于 2022/11/28 20:27:42 2022/11/28
【摘要】 文章目录前言Tomcat核心源码分析学习笔记1、认识Tomcat架构Tomcat架构:封装了许多的组件(类)Tomcat架构的好处2、Tomcat源码构建方式2.1、详细构建过程2.2、注意之后构建项目的language level(error:java 无效的源发行版11)2.3、源码分析(初始化与启动阶段)生命周期Lifecycle接口①初始化阶段②启动阶段2.4、Servlet请求处理链路①

@[toc]

前言

本篇博客是学习B站教程手撕 tomcat 核心源码15讲【全网最详细tomcat 源码解析,底层原理 】的学习笔记,如有错误请指出。

所有博客文件目录索引:博客目录索引(持续更新)

Tomcat核心源码分析学习笔记

Tomcat两个最重要的功能:

  • Http服务器(Connector):Socket通信(TCP)、解析HTTP报文。
  • Servlet容器:自带servlet以及我们可以自定义ServletServlet来处理业务逻辑处理。

Tomcat启动逻辑是基于观察者模式的。

LifecycleBase中的init()start()方法就使用到了模板方法模式LifecycleBase是一个抽象类,其实现了Lifecycle的接口,一个组件实现类实际上继承了该抽象类,每次执行如init()start()方法时实际上会去执行抽象类中的对应方法,在该方法中来真正执行重写的如initInternal();startInternal();方法。

  • image-20210519095417001

Connector中的Apapter中使用到了适配器模式:将原生的request以及response转换成HttpRequestHttpResponse



1、认识Tomcat架构

Tomcat架构:封装了许多的组件(类)

简单描述:套娃式架构设计

  • Server(tomcat实例,只有一个),可包含多个Service服务,默认是一个,也没有必要多个。
  • Service服务:可包含多个Connector(监听与解析TCP/IP协议与HTTP报文)与Container(Servlet容器用来处理请求,包含多个servlet)。
  • Connector(叫做Coyote):多个Connector主要是用来监听不同的端口,默认是三个,多个只能对应一个Container。其中包含三个组件分别处理TCP/IP协议、HTTP报文、适配器(将Request转为其他ServletRequest)。
  • Container(Servlet容器,又叫做cataline):其中有多个servlet来处理不同的业务请求,并返回ServletResponse响应,Wrapper中包含着servlet。
    • EngineHost:Engine组件(引擎)是Servlet容器Catalina的核⼼,它⽀持在其下定义多个虚拟主机(Host),虚拟主机允许Tomcat引擎在将配置在⼀台机器上的多个域名,⽐如www.baidu.com、www.bat.com分割开来互不⼲扰;
    • Context:每个虚拟主机⼜可以⽀持多个web应⽤部署在它下边,这就是我们所熟知的上下⽂对象Context,上下⽂是使⽤由Servlet规范中指定的Web应⽤程序格式表示,不论是压缩过的war包形式的⽂件还是未压缩的⽬录形式;在AppBase中进行配置,在webapps目录下每一个都可以看做是一个Context,
    • Wrapper:在上下⽂中⼜可以部署多个servlet,并且每个servlet都会被⼀个包装组件(Wrapper)所包含(⼀个wrapper对应⼀个servlet)。

image-20210518103702913

  • AJP协议:是早期apache(静态服务器)与tomcat(动态服务器)结合进行通信使用的AJP协议。
  • 网络IO:早期默认网络IO模型使用BIO,tomcat8.0之后版本使用NIO,APR是apache的可扩展的移植包需要安装在linux系统上后再进行指定(并发调优),可以进行重新指定。
  • Adapter:适配器,将一个东西转为另一个东西如Request转为ServletReuqest进行一层封装。

接下来通过看一下配置文件来更加了解整体架构

server.xml:通过看/conf/server.xml来看一下整体架构

<?xml version="1.0" encoding="UTF-8"?>
<!-- 对应一个Server实例 -->
<Server port="8005" shutdown="SHUTDOWN">

  <!-- 配置监听器 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
  <Listener SSLEngine="on" className="org.apache.catalina.core.AprLifecycleListener"/>
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>

  <!-- 全局资源的配置 -->
  <GlobalNamingResources>
    <Resource auth="Container" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase"/>
  </GlobalNamingResources>

  <!-- service服务:实际可以设置多份(看做是做个tomcat),一般一份就够了 -->
  <Service name="Catalina">

    <!-- Connector(连接器):用来监听8080端口,主要进行通信与解析 -->
    <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>

    <!-- Engine(引擎):Servlet容器的引擎 -->
    <Engine defaultHost="localhost" name="Catalina">
      <!-- 一些权限相关的可以使用它来做 -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
      </Realm>

      <!-- Host(虚拟主机,可多份):在一个虚拟主机下可以配置多个应用Context这里不体现,Host可以设置多份如www.blog.changlu.com,www.watch.changlu.com -->
      <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" pattern="%h %l %u %t &quot;%r&quot; %s %b" prefix="localhost_access_log" suffix=".txt"/>
      </Host>
    </Engine>
  </Service>

</Server>

认识ContextWrapper:你将一个项目放在webapps目录下就是一个Context,Context下就是Wrapper(对应Servlet,一个Wrapper就是一个Servlet)。



Tomcat架构的好处

1、组件关系:一层套一层的关系,组件关系清晰,便于后面的组件生命周期管理。

2、层次明确:server.xml配置文件中标签与架构设计对应上,解读xml以及封装对象关系更加容易。

3、通过这种组件式的关系,能够让子容器继承父容器的一些配置。



2、Tomcat源码构建方式

2.1、详细构建过程

本次源码构建采用Tomcat 8.5.66版本

源码下载地址:Tomcat8.5.66下载页面

image-20210518135446469

接着按照下面几个步骤来进行构建:

步骤⼀:解压源码压缩包,得到⽬录 apache-tomcat-8.5.50-src。

步骤⼆:进⼊ apache-tomcat-8.5.50-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>8</source>
                    <target>8</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.50-src ⽬录中创建 source ⽂件夹,将原本项目中的conf、webapps ⽬录移动到source ⽂件夹。

image-20210518135804174

步骤五:将源码⼯程导⼊到 IDEA 中;

步骤六:给 tomcat 的源码程序启动类 Bootstrap 配置 VM 参数,因为 tomcat 源码运⾏也需要加载配置⽂件等。

  • 本项目是java se项目,所以创建一个application配置起始运行类以及vm参数:
  • image-20210518135928657
# 注意修改其中的source目录路径(1、2、3路径修改),注意这里是windows环境下,Mac环境\要改成/,并且去掉C:
# 这里是在启动时添加四个参数,之后运行可以获取到
-Dcatalina.home=C:\Users\93997\Desktop\apache-tomcat-8.5.66-src\source
-Dcatalina.base=C:\Users\93997\Desktop\apache-tomcat-8.5.66-src\source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=C:\Users\93997\Desktop\apache-tomcat-8.5.66-src\source\conf\logging.properties

步骤七:由于Tomcat源码中Jsp引擎Jasper没有被初始化,⽆法编译处理Jsp。所以找到ContextConfig类,在configureStart()方法中添加初始化操作:

image-20210518140349104

//初始化JSP解析引擎-jasper
context.addServletContainerInitializer(new JasperInitializer(),null);

步骤八:启动项目,出现以下信息表示成功运行!

image-20210518140548904

此时访问http://localhost:8080/,即可访问到汤姆猫页面!



2.2、注意之后构建项目的language level(error:java 无效的源发行版11)

Project strucutre中的ProjectMoudles中设置language level为8

image-20210518140929745

image-20210518140939450

②Settings中设置项目版本为8

image-20210518141040208

③确保Maven配置的插件设置为版本8

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
        <source>8</source>
        <target>8</target>
    </configuration>
</plugin>


2.3、源码分析(初始化与启动阶段)

生命周期Lifecycle接口

Tomcat启动过程中,一定会将组件进行实例化,为了统一规范它们的生命周期,Tomcat抽象除了LifeCycle生命周期接口,生命周期中包含初始化init()、启动start()stopdestory等。

下面是LifeCycle接口的继承图,我们需要关注其中的LifecycleBase抽象类:

image-20210519151150396

图中你可以看到StandardEngineStandardServer等一些组件实际上都可以看做是继承的抽象类LifecycleMBeanBase(实际继承的是该类的子类),他们都有LifeCycle接口中的方法,通过使用模板方法设计模式,来定义好每个方法其中的执行过程来进行多个组件初始化的相同步骤。


①初始化阶段

指的是init()阶段。(对应在daemon.load(args); 方法中执行)

image-20210518184229068

Bootstrapmain()启动

  • 实例化Bootstrap,调用init()方法,在init()方法中实例化org.apache.catalina.startup.Catalina,并调用Catalina实例的setParentClassLoader()方法。最后给Object catalinaDaemon赋值Catalina实例。简而言之就是实例化Catalina类

  • volatile Bootstrap daemon = Bootstrap实例,引导类实例化并赋值。

  • 执行最重要的两步骤:

    daemon.load(args);  //load加载初始化,调用Catalina的load()方法
    daemon.start();  //start启动,调用Catalina的start()方法  
    

catalinaload():通过使用Digester进行加载server.xml,其中该实例执行parse()进行解析xml配置文件。

  • 注意解析server.xml过程就是实例化各个组件的过程,解析到Catalina实例中的Server属性(StandardServer类)中。在StandardServer实例中包含配置文件中的所有信息、以及组件。
    • image-20210518161005291
  • 调用StandardServer.init(),其中执行了LifecycleBase的init(),其中又执行了StandardServerinitInternal()方法重要的来了,其中遍历了services(即servcice服务),执行Service服务的init(),接着其中继续执行Engine以及Connectorinit()
    • 注意在EngineinitInternal()中(即组件初始化),创建了一个线程池ThreadPoolExecutor,这个线程池在engine组件的start阶段使用。目的是由于engine可以有多个Host所以若是有多个Host就放置在线程池中进行执行。
  • 依旧是在StandardServerinitInternal()方法中执行ListenerConnectorinit(),在连接器中有ProtocalHandler实例以及Adapter实例,后者先进行实例,前者绑定了配置文件中的端口8080实现监听并且默认使用NIO网络模型。

说明:这个初始化阶段主要就进行对应的实例化以及初始化操作,其中包含了读取配置文件server.xml的操作。



②启动阶段

image-20210519152003085

较重要的说明多线程处理方式处理Host主机;start过程中的HostConfig中根据webapps目录下进行遍历来进行创建context实例,有三种解析方式xmlwar包,文件目录形式。通过多线程处理方式处理多个应用context,加载context过程中进一步封装wrapper也就是Servlet。中间过程会创建work目录用来存放jsp生成的servlet

StandardContext中的startInternal()里的fireLifecycleEvent()方法读某个Contextweb.xml配置文件。

之后就是connector的启动过程,NIO的多路复用解决多个无用请求切换的问题,使用poller线程来检查selector是否有数据到来的channel

说明:先是进行Enginestart()启动,接着进行连接器Connector及其内部组件的启动。



2.4、Servlet请求处理链路

①Servlet请求处理分析

分析⼀个servlet是如何被tomcat处理的?

  • ⼀个serlvet请求—> 最终我们是需要找到能够处理当前serlvet请求的servlet实例 —> serlvet.service()

image-20210519200059441



②Servlet请求处理流程示意

Poller线程是追踪⼊⼝

image-20210519200213840

HTTP11Processor(处理HTTP1.1的处理器)中进行requestresponse对象的封装,之后将requestresponse交给Adapter封装成ServletRequest以及ServletResponse

中间会根据你的请求地址去一一匹配HostContextWrapper封装到MappingData对象中,

image-20210519141005241

在获取servlet之后,会有一个FilterChain链,首先会去执行过滤器链,过滤器链中具有request核心业务请求。

PS:一般过滤器你需要去实现Filter接口。



③Mapper组件体系结构

image-20210519201014385

Tomcat中使⽤Mapper机制重新封装了Host-context-wrapper(serlvet)之间的数据和关系当请求到来时,根据请求uri匹配出能够处理当前请求的那个Host、那个Context、那个Wrapper。那么此时mapper对象肯定已经初始化好了。

疑问:mapper对象数据是什么时候初始化的?

  • StandardService—>startInternal—>mapperListener.start()中完成mapper对象初始化。


小总结

首先进行初始化load(),之后进行启动start()

  • load()过程:实例化Catalina,接着实例Bootstrap 。先根据server.xml来进行解析(解析出Server),最大容器就是Catalina容器中的Server实例(StandardServer)。其中执行了Service服务的init(),并执行Engine以及Connectorinit()
    • Engine进行组件初始化时创建了线程池,为之后启动做准备。
    • Connector中创建其中的各个组件实例,绑定监听端口以及使用NIO网络模型。
  • start()阶段:多线程方式来处理多个Host主机(暂且只有一个localhost)。通过HostConfig类来遍历webapps目录并创建Context实例,包含三种解析方式(xml、war包、文件目录)。接着多线程方式处理多个context应用,在加载context过程中封装wrapper(也就是servlet),过程中创建work目录。在StandardContext中的StartInternal()里读取某个Contextweb.xml配置文件。接着就是Connector的启动,使用NIO网络模型进行通信,其中使用poller线程来检查selector是否有数据到来的channel

Servlet请求处理:通过Pollezr线程检测出需要处理的Socket后交给其他线程来处理,使用Processor解析处理socket,封装Request以及Response。之后使用适配器CoyoteAdapter来将RequestResponse进行适配转换为Servletxxx,并进行路径映射(匹配HostContextWrapper,匹配过程通过Mapper组件进行)。匹配到之后调用Wrapper得到Servlet,进行构造FilterChain(过滤器链)。

注意Servlet核心业务是在FilterChain过滤器链中进行的!



参考文章

[1]. Tomcat启动过程源码分析一 从startup.bat开始分析整个执行过程

[2]. META-INF目录是干啥用的?

较好文章,值的反复查看

Tomcat运行过程和简单模拟

tomcat-------------tomcat文件结构及简介

请求在Tomcat中的运行流程

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。