Tomcat的架构与源码分析学习笔记
@[toc]
前言
本篇博客是学习B站教程手撕 tomcat 核心源码15讲【全网最详细tomcat 源码解析,底层原理 】的学习笔记,如有错误请指出。
所有博客文件目录索引:博客目录索引(持续更新)
Tomcat核心源码分析学习笔记
Tomcat
两个最重要的功能:
Http
服务器(Connector):Socket通信(TCP)、解析HTTP报文。Servlet
容器:自带servlet
以及我们可以自定义Servlet
。Servlet
来处理业务逻辑处理。
Tomcat
启动逻辑是基于观察者模式的。
LifecycleBase
中的init()
、start()
方法就使用到了模板方法模式:LifecycleBase
是一个抽象类,其实现了Lifecycle
的接口,一个组件实现类实际上继承了该抽象类,每次执行如init()
、start()
方法时实际上会去执行抽象类中的对应方法,在该方法中来真正执行重写的如initInternal();
、startInternal();
方法。
Connector
中的Apapter
中使用到了适配器模式:将原生的request
以及response
转换成HttpRequest
、HttpResponse
。
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。Engine
和Host
: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)。
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 "%r" %s %b" prefix="localhost_access_log" suffix=".txt"/>
</Host>
</Engine>
</Service>
</Server>
认识Context
与Wrapper
:你将一个项目放在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下载页面
接着按照下面几个步骤来进行构建:
步骤⼀:解压源码压缩包,得到⽬录 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 ⽂件夹。
步骤五:将源码⼯程导⼊到 IDEA 中;
步骤六:给 tomcat 的源码程序启动类 Bootstrap 配置 VM 参数,因为 tomcat 源码运⾏也需要加载配置⽂件等。
- 本项目是java se项目,所以创建一个application配置起始运行类以及vm参数:
# 注意修改其中的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()
方法中添加初始化操作:
//初始化JSP解析引擎-jasper
context.addServletContainerInitializer(new JasperInitializer(),null);
步骤八:启动项目,出现以下信息表示成功运行!
此时访问http://localhost:8080/,即可访问到汤姆猫页面!
2.2、注意之后构建项目的language level(error:java 无效的源发行版11)
①Project strucutre
中的Project
与Moudles
中设置language level
为8
②Settings中设置项目版本为8
③确保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()
、stop
、destory
等。
下面是LifeCycle
接口的继承图,我们需要关注其中的LifecycleBase
抽象类:
图中你可以看到StandardEngine
、StandardServer
等一些组件实际上都可以看做是继承的抽象类LifecycleMBeanBase
(实际继承的是该类的子类),他们都有LifeCycle
接口中的方法,通过使用模板方法设计模式,来定义好每个方法其中的执行过程来进行多个组件初始化的相同步骤。
①初始化阶段
指的是
init()
阶段。(对应在daemon.load(args);
方法中执行)
Bootstrap
的main()
启动
-
实例化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()方法
catalina
的load()
:通过使用Digester
进行加载server.xml
,其中该实例执行parse()
进行解析xml
配置文件。
- 注意解析
server.xml
过程就是实例化各个组件的过程,解析到Catalina
实例中的Server
属性(StandardServer
类)中。在StandardServer
实例中包含配置文件中的所有信息、以及组件。 - 调用
StandardServer.init()
,其中执行了LifecycleBase的init()
,其中又执行了StandardServer
的initInternal()
方法重要的来了,其中遍历了services
(即servcice服务),执行Service
服务的init()
,接着其中继续执行Engine
以及Connector
的init()
。- 注意在
Engine
的initInternal()
中(即组件初始化),创建了一个线程池ThreadPoolExecutor
,这个线程池在engine组件的start阶段使用。目的是由于engine可以有多个Host所以若是有多个Host就放置在线程池中进行执行。
- 注意在
- 依旧是在
StandardServer
的initInternal()
方法中执行Listener
、Connector
的init()
,在连接器中有ProtocalHandler
实例以及Adapter实例,后者先进行实例,前者绑定了配置文件中的端口8080实现监听并且默认使用NIO
网络模型。
说明:这个初始化阶段主要就进行对应的实例化以及初始化操作,其中包含了读取配置文件server.xml的操作。
②启动阶段
较重要的说明:多线程处理方式处理Host主机;start
过程中的HostConfig
中根据webapps
目录下进行遍历来进行创建context
实例,有三种解析方式xml
,war
包,文件目录形式。通过多线程处理方式处理多个应用即context
,加载context
过程中进一步封装wrapper
也就是Servlet
。中间过程会创建work
目录用来存放jsp
生成的servlet
。
在StandardContext
中的startInternal()
里的fireLifecycleEvent()
方法读某个Context
的web.xml
配置文件。
之后就是connector
的启动过程,NIO
的多路复用解决多个无用请求切换的问题,使用poller
线程来检查selector
是否有数据到来的channel
。
说明:先是进行Engine
的start()
启动,接着进行连接器Connector
及其内部组件的启动。
2.4、Servlet请求处理链路
①Servlet请求处理分析
分析⼀个servlet是如何被tomcat处理的?
- ⼀个serlvet请求—> 最终我们是需要找到能够处理当前
serlvet
请求的servlet
实例 —>serlvet.service()
②Servlet请求处理流程示意
Poller
线程是追踪⼊⼝
在HTTP11Processor
(处理HTTP1.1的处理器)中进行request
、response
对象的封装,之后将request
、response
交给Adapter
封装成ServletRequest
以及ServletResponse
。
中间会根据你的请求地址去一一匹配Host
、Context
、Wrapper
封装到MappingData
对象中,
在获取servlet
之后,会有一个FilterChain
链,首先会去执行过滤器链,过滤器链中具有request
核心业务请求。
PS:一般过滤器你需要去实现Filter接口。
③Mapper组件体系结构
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
以及Connector
的init()
。- 在
Engine
进行组件初始化时创建了线程池,为之后启动做准备。 - 在
Connector
中创建其中的各个组件实例,绑定监听端口以及使用NIO
网络模型。
- 在
- ②
start()
阶段:多线程方式来处理多个Host主机(暂且只有一个localhost
)。通过HostConfig
类来遍历webapps
目录并创建Context
实例,包含三种解析方式(xml、war包、文件目录)。接着多线程方式处理多个context应用,在加载context
过程中封装wrapper
(也就是servlet
),过程中创建work目录。在StandardContext
中的StartInternal()
里读取某个Context
的web.xml
配置文件。接着就是Connector
的启动,使用NIO
网络模型进行通信,其中使用poller线程来检查selector是否有数据到来的channel。
Servlet
请求处理:通过Pollezr
线程检测出需要处理的Socket后交给其他线程来处理,使用Processor
解析处理socket
,封装Request
以及Response
。之后使用适配器CoyoteAdapter
来将Request
及Response
进行适配转换为Servletxxx
,并进行路径映射(匹配Host
、Context
、Wrapper
,匹配过程通过Mapper
组件进行)。匹配到之后调用Wrapper
得到Servlet
,进行构造FilterChain
(过滤器链)。
注意:Servlet
核心业务是在FilterChain
过滤器链中进行的!
参考文章
[1]. Tomcat启动过程源码分析一 从startup.bat开始分析整个执行过程
[2]. META-INF目录是干啥用的?
较好文章,值的反复查看
- 点赞
- 收藏
- 关注作者
评论(0)