文章目录
本来这篇文章只是我前辈引路我自学tomcat的一篇水文,看到大家反应很好,我觉得我有义务那它完善一下,谢谢你们!
一、官网下载Tomcat 的源码导入IDEA
1、地址:http://tomcat.apache.org/ 左侧 Download Tomcat 9,在网页最下面下载Tomcat源码;
下载完了直接解压得到apache-tomcat-9.0.39文件夹。
2、打开IDEA导入解压后的文件夹 , 选择File->Open->选择tomcat的源码目录(我下载的是apache-tomcat-9.0.39);
注意:打开的目录是刚刚解压的文件夹,而不是打开解压文件夹的中的那个子目录(我是这么操作的)。
二、Tomcat启动重要文件
startup源码分析
我觉得要研究一个技术的源码要从它是怎么启动运行的开始,特别是很复杂的源码,所有我就从Tomcat的启动开始。
在Tomcat的bin目录下有两个启动Tomcat的文件,一个是startup.bat,它用于windows环境下启动Tomcat;另一个是startup.sh,它用于Linux环境下Tomcat的启动。大概看了下这两个文件中的实现思路差不多一样的,我就看了startup.bat(windows启动文件)
以下是startup.bat文件,我加了一些注解:
通过以上阅读可以得到一个结论: startup.bat文件实际上就做了一件事情 -> 启动catalina.bat
这样我也明白了,为什么在此之前我在windows下配置了catalina.bat就可以使用catalina run 启动Tomcat了。所以未必一定要通过startup.bat来启动Tomcat,用catalina.bat start也是可以的。使用startup.bat其中重要的一步就是把"start"参数传递给catalina.bat
。
catalina.bat源码分析
既然在 startup.bat有关联到 catalina.bat ,那么就肯定要看看这个文件是干嘛的。
由于注解比代码多,我就梳理一下大概的执行逻辑
首先会直接跳到mainEntry代码段 -> 在确定CATALINA_HOME下有catalina.bat后再把CATALINA_HOME赋给变量CATALINA_BASE -> 之后再去获得CLASSPATH(就是我们配置的环境变量,在jdk1.6之后Java环境变量是不用配置classpath路径的)-> 系统拿到classpath路径后把它和CATALINA_HOME拼接在一起,最终定位到一个叫bootstrap.jar的文件;
然后到 doStart
代码块(当然还有doDebug和doRun)并设定参数,最终跳转到execCmd
代码段;
通过以上阅读可以得到一个结论: catalina.bat最终执行了Bootstrap类中的main方法。
读完catalina.bat会发现,我们可以通过设定不同的参数让Tomcat以不同的方式运行。比如说:在IDEA中可以选择debug等模式启动Tomcat的,也可以为其配置参数,在catalina.bat中我们可以看到了启动Tomcat背后的运作流程。
注意:不管是 Tomcat 的启动还是关闭都是通过这个main 方法来入手的。
//public final class Bootstrap Bootstrap类的main方法
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init(); //注意这个init()方法
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
刚刚说了,既然启动是靠Bootstrap的main()方法,那么不妨这么设置一下来运行项目:
运行tomcat源码我知道有两种方式,一种是使用 Ant 工具进行编译源码,一种是使用 Maven 工具,我使用的是 Maven,使用 Ant 得去下载 Ant 工具进行编译源码,一种是使用 Maven 工具,我使用的是 Maven,使用 Ant 得有IDEA Ant 的插件。我就说一下常用 Maven的操作:
首先:新建 catalina-home文件夹和像下图一样新建第一个pom.xml文件,并在apache-tomcat-9.0.39-src文件里新建第二个pom.xml(这两个pom文件可以在IDEA新建,也可以去其他项目中引入)
将apache-tomcat-9.0.39-src中的 conf
文件复制到新建的catalina-home目录之下,再将以下内容复制到apache-tomcat-9.0.39-src中的pom.xml中,为什么需要这些依赖?就是运行tomcat需要这些依赖作为支撑。
<?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>Tomcat9.0</artifactId>
<name>Tomcat9</name>
<version>9.0.39</version>
<build>
<finalName>Tomcat9</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>2.3</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.9</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-apache-log4j</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-commons-logging</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>javax.xml.rpc</groupId>
<artifactId>javax.xml.rpc-api</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>4.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
再将以下内容复制到外层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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yang</groupId>
<artifactId>apache-tomcat-study</artifactId>
<name>Tomcat 9.0 Study</name>
<version>1.0</version>
<packaging>pom</packaging>
<!--表示外层项目包含内层项目,这里要是tomcat版本不同的话,得修改一下,其实就是目录名-->
<!--这种方式,类似构建微服务子模块与root项目的关系-->
<modules>
<module>apache-tomcat-9.0.39-src</module>
</modules>
</project>
再:① 在项目配置中设置JDK和源码目录:File->Project Structure->project->project SDK
②设置 java包设置为apche-tomcat-9.0.39-src这个项目的Sources文件:File->Project Structure->Modules
注意:Modules 没有项目就添加一下。一个父模块,一个子模块。
另外:这里有一个问题,就是把 test 文件夹设置为了 Test 了之后 运行我们的项目 TestCookieFilter 类就会报错。
这时候有两种解决办法,一是不把 test 文件夹设置为 Test ,二是看到有的直接删除了这个类。
然后:配置 Edit Configrations
Main class:
org.apache.catalina.startup.Bootstrap
vm options配置:
-Dcatalina.home="E:\Code\apache-tomcat-9.0.39-src\catalina-home"
-Dfile.encoding=UTF8
-Duser.language=en
-Duser.region=US
保存运行即可即可。
三、Tomcat启动流程分析
前置知识:
- Tomcat启动流程中启动若干个组件,主要分为
Connector(连接器)
和Container(容器)
;- 其中连接器中包含EndPoint(接受请求)、processor(处理请求)、Adapter(适配请求),然后会到达连接池,这些组件都是在 coyote项目中。
- 其中容器中包含Engine(引擎)、Host(虚拟主机)、Context(应用上下文)、Wrapper(包装servlet的对象),最后才是一个个servlet,这个大组件属于catalina项目。
启动的分析思路就先看到这里,先来看看service.xml到底配置了什么,于是就延伸出来了以下知识。我先说一下Tomcat 部署项目的方式,然后再来引入Tomcat的四大Servlet容器。
1、部署项目的方式
背景:我把Tomcat源码这个项目跑在了IDEA中
有三种方式:这三种方式并不是凭空产生,是有源码对应的。
前置知识:HostConfig类是一个事件监听器类,它是用于部署项目的。
(1)方式一:将web项目(应用)打包成 war 包,之后直接把这个war包放在webapps文件里
这种方式很好理解,就是把应用交给Tomcat去执行。
那么问题是Tomcat是怎么知道这个war包在webapps下,并知道它就是一个应用呢?
这个问题一会看到四大容器之一的Host就知道了。
在启动过程中,有线程回去读取webapps文件夹中的所有项目文件,假设读取当前文件的后缀名是以".war"结尾的,那么就会根据是否自动解压部署的配置去解压为文件夹,然后进行项目的部署。
主配置文件server.xml中的Host节点中的,appBase属性值就是配置该虚拟主机默认的项目对应文件目录为webapps。这样就可以找到每一个虚拟主机对应的项目所在文件夹。
(2)方式二:文件夹部署(只要有.class字节码即可运行项目)
第一种部署方式运行项目时,就是解压 war 包并且放到当前webapps目录下,此时是可以删除掉 war包的,只存在这个解压过来的文件也是可以独立运行项目的,这就引出了第二种部署方式,文件夹部署。
(3)方式三:配置应用指定的位置
把应用的位置指定在Host配置中。就是在主配置文件中的Host节点下添加Context节点
由如这样配置:
参数说明:
path:指定项目访问的uri路径;
docBase:指定项目的所在物理地址,比如说哪一个盘符下
reloadable:设置该项目是否开启热部署,就是在Tomcat启动过程中会定时去扫描该应用项目是否有更新,
假如有就会对该项目解部署之后再重新部署。
2、Tomcat中Container管理四大Servlet容器
先来看看有哪一些,分别是
(1)Engine
表示整个Catalina Servlet引擎,是Container 组件的最高层,用来管理Host 或者Context的实现,是指定默认的Host为 localhost,名字指定为 Catalina,这就是为什么我们不指定Host也可以使用localhost虚拟主机来访问到应用。
也就是说一个 Engine 对应一个
List<Host> hosts; 在源码中加载Host虚拟主机是创建一个线程池来异步启动多个Host,
包括Context应用的加载也是创建一个线程池来部署应用的。
(2)Host
:我理解它是一个Engine管理下的一个虚拟主机,默认的虚拟主机是 localhost
,也就是使用这个虚拟的主机来告诉我们要访问的 Tomcat 服务的位置,比如说使用 localhost://8080这个就是对应一个虚拟主机,然后再这个Host 里面来配置相应的应用,这样就是顺利访问到我们指定要访问的应用了。
当然可以存在多个虚拟主机,虽然它们的都是对应同一个 Tomcat,但是对应的应用不一样。一个虚拟主机里面也可以对应不同的应用。说白了,多个Host就是来做一个多个应用的指定位置的。
这也解释了为什么 Tomcat 会去webapps里找应用。
也就是说一个 Host 对应一个
List<Context> contexts;
(3)Context
:直接理解的话,是上下文,但是我理解它是一个应用,或者是一个应用的配置,使用文件描述符配置文件的话,就会使用到 Context 容器。一个Context可以对应一个应用。
也就是说一个 Context 对应一个
List<Servlet> servlets;
(4)Wrapper
:我理解它就是来对我们同一个 Servlet 的不同实例来进行分类管理的,也就是多次请求Tomcat 中同一个 Servlet 资源就会产生不同的 Servlet 实例,然而这些实例不可能任意在容器中,这样就不要管理,会造成混乱,所以就用 Weapper 来对同一类Servlet 的不同实例进行分类。Wrapper 还用来管理一个 Servlet 的生命周期。
也就是一个 Wrapper 对应一个 Servlet 类型。
List<Servlet> servlets;
而一个Context 就对应了一个Wrapper了
List<Wrapper> wrappers;
Http请求在Container中的传递流程
好了,以上就是对 Container 接口的四大子接口的分析,它们分别对应四大 Servlet容器。
花了一个晚上搞清楚了这些接口和容器的关系了,直接看这个时序图:
3、分析启动时序图
从Bootstrap类的main方法开始,Tomcat会以链的方式逐级调用各个模块的init()方法进行初始化。待各个模块都初始化后,又会逐级调用各个模块的start()方法启动各个模块。
Bootstrap类首先会创建一个本类对象,然后调用init()方法进行初始化。
这里说一下,实例化是在堆空间中开辟相应的空间并赋默认值,初始化是调用 init() 方法赋实际值的一个过程。
假如是正常执行 start 的方式的话,可以看到在设置等待后,调用了本类对象的load()方法。查看load()方法的源码:
可以看到方法的最后通过反射的方式调用了成员变量catalinaDaemon的load()方法。通过跟踪源码可以看到catalinaDaemon是Bootstrap类的一个私有成员变量。
public final class Bootstrap {
private static final Log log = LogFactory.getLog(Bootstrap.class);
/**
* Daemon object used by main.di
*/
private static final Object daemonLock = new Object();
private static volatile Bootstrap daemon = null;
/**
* Daemon reference.
*/
//在init()方法中使用反射机制创建catalina赋给catalinaDaemon
private Object catalinaDaemon = null;
它会在Bootstrap的init()方法中通过反射的方式完成初始化。下面我们回过头来看init()方法的源码
可以看到init()方法创建了一个Catalina对象, 并把该对象赋给了catalinaDaemon。
之后再执行 getServer 方法来创建 Server 容器。
// Lifecycle是所有组件的父接口,因为所有组件都会涉及到初始化、启停、销毁
public interface Service extends Lifecycle {
/**
* @return the <code>Container</code> that handles requests for all
* <code>Connectors</code> associated with this Service.
*/
public Container getContainer();
来到了 Service 里,你会看到第一句就定义一个获取 Container
的方法,也就是可以获取一个唯一的Container。 在这个 Service 里,你会发现有 Executor
和Connctor
。
此时也就是说可以走 Container 被继承的四个 Servlet 容器了。
到此也就可以画出简单的一个UML图。来清晰展示这些接口之间的大概关系。
实现与继承关系:
总结一下 tomcat 的启动步骤如下:
(1)先新建 Bootstrap
对象,并通过其调用 init()
方法(初始化),init() 方法初始化 Tomcat 的类构造器。再利用反射机制创建catalinaDaemon
。
(2)利用之前创建的 daemon 的load()方法,其实本质就是调用了 catalinaDaemon
的load 方法。去加载和解析相应的配置文件,比如server.xml。
(3)执行daemon的 start
方法,其实本质就是调用了 catalinaDaemon
的 start 方法。
这首先会去启动顶级容器 serve
容器,server 启动后去启动 service
容器,再接着启动两个核心组件 Container
、Connector
。Container 会去启动 Engine等容器,Connector 会创建Request 和 Response。
(4)启动完成后,就是一直等待,假如转入参数是 shutdown
就会开始执行关闭 Tomcat 的逻辑。
四、Request请求过来Tomcat在干嘛
1、Pipeline
…
以后有时间再更新吧,近期有工作任务
有用点个关注,手留余香!😗 😗 😗