浅谈Tomcat的启动流程(源码级别)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6


文章目录

  • ​​一、启动入口​​
  • ​​1、startup.sh​​
  • ​​2、catalina.sh​​
  • ​​二、基础组件概念梳理​​
  • ​​1、整体架构​​
  • ​​2、LifecycleBase抽象类​​
  • ​​3、实现了LifecycleBase抽象类的子类​​
  • ​​三、tomcat初始化(init)​​
  • ​​1、tomcat自定义类加载器​​
  • ​​四、tomcat加载(load)​​
  • ​​1、createStartDigester(第2步)​​
  • ​​1)抽象指定的层级关系​​
  • ​​2)addObjectCreate​​
  • ​​3)addRuleSet​​
  • ​​4)addRule​​
  • ​​2、getServer().init(第5步)​​
  • ​​1)JMX​​
  • ​​2)globalNamingResources.init()和services[i].init()​​
  • ​​3)StandardEngine、Connector​​
  • ​​五、tomcat启动(start)​​
  • ​​1、启动容器container.start()​​
  • ​​2、启动连接connector.start()​​
  • ​​3、补充连接中的protocolHandler.start()​​

本文仅涉及对tomcat启动流程的一个大致梳理,侧重讲解流程而非流程数据细节(具体流程过于细节,容易让人摸不着头脑)



一、启动入口

1、startup.sh

如果是直接启动tomcat,我们都是找到xxxx\apache-tomcat\bin目录下的startup.sh文件,然后直接双击启动。

观察对应的执行脚本,我们发现该脚本的目的是执行同目录下的catalina.sh文件,并且传递了一个start字符串



浅谈Tomcat的启动流程(源码级别)_开发语言


2、catalina.sh

1、前面传递了一个start字符串,会进入这个判断(后期Java代码中还会出现该start字符串)。



浅谈Tomcat的启动流程(源码级别)_开发语言_02


2、然后调用Java的org.apache.catalina.startup.Bootstrap类,并且传入一个start字符串(此处它并没有使用起那面传入的start字符串,而是直接写了一个固定的字符串start)



浅谈Tomcat的启动流程(源码级别)_java_03


3、最终会调用Java的这个main方法



浅谈Tomcat的启动流程(源码级别)_开发语言_04



二、基础组件概念梳理

1、整体架构

在进入Bootstrap的代码前,我们需要先了解tomcat的整体架构,以及对应的组件层级关系。



浅谈Tomcat的启动流程(源码级别)_java_05


上面的架构图可以简化为如下的流程图,本文主要梳理tomcat启动过程中的数据流程,不涉及太具体的数据内容



浅谈Tomcat的启动流程(源码级别)_java_06


2、LifecycleBase抽象类

这是一个实现了Lifecycle接口的抽象类,这个接口和抽象类,乃至对应的实现,在整体结构上是一个模板方法模式



浅谈Tomcat的启动流程(源码级别)_tomcat_07


观察LifecycleBase抽象类,我们主要关心一下几个方法,后文tomcat初始化和启动的时候会调用到:

  • init:该init方法来源于接口,在抽象类中对init方法进行了实现。抽象类中会调用子类的initInternal方法(同样还是模板方法模式)
  • start:同理,其对应子类实现方法为startInternal


浅谈Tomcat的启动流程(源码级别)_初始化_08


3、实现了LifecycleBase抽象类的子类

tomcat在初始化和启动的时候,会直接调用到对应的方法。为避免到时候你对于模板方法的跳转理不清了,所以先提前进行一个说明

记住这个Standard的开头



浅谈Tomcat的启动流程(源码级别)_Server_09



三、tomcat初始化(init)

bootstrap.init();

该方法主要注释如下:

1、获取项目的基本配置,完成catalina相关参数的设置

2、初始化tomcat自己会使用到的类加载器

3、完成catalinaLoader(catalina加载器)对象的初始化(调用setParentClassLoader方法)

4、完成catalinaDaemon对象的赋值

public void init() throws Exception {

// catalina.home表示安装目录
setCatalinaHome();
// catalina.base表示工作目录
setCatalinaBase();

// 初始化commonLoader、catalinaLoader、sharedLoader
// 其中catalinaLoader、sharedLoader默认其实就是commonLoader
initClassLoaders();

// 设置线程的所使用的类加载器,默认情况下就是commonLoader
Thread.currentThread().setContextClassLoader(catalinaLoader);

// 如果开启了SecurityManager,那么则要提前加载一些类
SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
if (log.isDebugEnabled()) {
log.debug("Loading startup class");
}
// 加载Catalina类,并生成instance
// 使用自定义的加载器进行加载,而非使用app类加载器加载,然后使用反射生成对应的实例。类比new Catalina();
Class<?> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();

// Set the shared extensions class loader
// 设置Catalina实例的父级类加载器为sharedLoader(默认情况下就是commonLoader)
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);

// 使用反射调用org.apache.catalina.startup.Catalin类的setParentClassLoader方法
method.invoke(startupInstance, paramValues);

// catalinaDaemon设置为org.apache.catalina.startup.Catalina的对象
// 后文会反复出现这个对象
catalinaDaemon = startupInstance;
}



梳理几个关键参数的赋值(一定要记清楚,否则后文出现对应参数时,你不知道对应的值为多少):

  • catalinaLoader、haredLoader:可以理解为commonLoader类加载器
  • catalinaDaemon:Catalina对象


1、tomcat自定义类加载器

tomcat类加载器的层级结构如下。在tomcat的源码中,其定义了自己的类加载器,很多类的加载方式都是使用自定义的类去加载的,而非直接new出来(你也可以根据,加载一个类的时候是否显式的指定tomcat的类加载器来判断)



浅谈Tomcat的启动流程(源码级别)_tomcat_10


对应的自定义类加载器的代码如下



浅谈Tomcat的启动流程(源码级别)_初始化_11



四、tomcat加载(load)

daemon.load(args);

该方法使用反射调用catalinaDaemon(Catalina对象)的load方法



浅谈Tomcat的启动流程(源码级别)_Server_12


Catalina类的load方法主要做了以下事情:

  1. initDirs、initNaming:初始化一些基本的参数信息
  2. createStartDigester:创建对应的文件解析器(包含指定的解析规则),用于后面解析server.xml文件
  3. inputSource.setByteStream(inputStream):获取对应的server.xml或者是server-embed.xml文件
  4. digester.parse(inputSource):根据第2步创建的文件解析器,解析配置文件
  5. getServer().init():初始化Server容器(可以根据tomcat架构看对应的位置)


1、createStartDigester(第2步)

创建对应的解析规则,在Java中你可以理解为创建相应的对象

初看这个方法,你可能比较懵,但是没关系,你只要理即为每个方法的作用即可。



浅谈Tomcat的启动流程(源码级别)_初始化_13


1)抽象指定的层级关系

类比server.xml文件,我们脑中是一定要抽象出对应的层级关系的:



浅谈Tomcat的启动流程(源码级别)_java_14

// Catalinna是顶层,对应的代码位置在解析server.xml文件的前一步
// 即digester.push(this)方法(此处的this即Catalinna)
Catalinna
- Server
- Listener
- GlobalNamingResources
- Service
- Connector


2)addObjectCreate

这个流程一定要明白,否则后期你调试的时候你就不知道对应的参数值是谁赋值的

addObjectCreate:创建一个Server对象,使用的类为StandardServer

addSetProperties:对应的server.xml文件中的标签层级为Server

addSetNext:然后调用对应Server对象的父节点(Catalinna类)的setServer方法

同理,第二段则可以描述为

  1. 创建一个GlobalNamingResources对象,使用的类为NamingResources
  2. 对应的server.xml文件中的标签层级为Server/GlobalNamingResources
  3. 然后调用父节点(StandardServer)的的setGlobalNamingResources
digester.addObjectCreate("Server","org.apache.catalina.core.StandardServer","className");
digester.addSetProperties("Server");
digester.addSetNext("Server", "setServer","org.apache.catalina.Server");


digester.addObjectCreate("Server/GlobalNamingResources","org.apache.catalina.deploy.NamingResources");
digester.addSetProperties("Server/GlobalNamingResources");
digester.addSetNext("Server/GlobalNamingResources","setGlobalNamingResources","org.apache.catalina.deploy.NamingResources");


3)addRuleSet

核心是调用addRule方法

传入的参数为指定的规则set,如NamingRuleSet、EngineRuleSet。然后会调用传入参数的addRuleInstances方法

public void addRuleSet(RuleSet ruleSet) {

String oldNamespaceURI = getRuleNamespaceURI();
String newNamespaceURI = ruleSet.getNamespaceURI();
if (log.isDebugEnabled()) {
if (newNamespaceURI == null) {
log.debug("addRuleSet() with no namespace URI");
} else {
log.debug("addRuleSet() with namespace URI " + newNamespaceURI);
}
}
setRuleNamespaceURI(newNamespaceURI);
// 处理对应的解析规则,
ruleSet.addRuleInstances(this);
setRuleNamespaceURI(oldNamespaceURI);
}



addRuleInstances方法同样是完成指定对象的创建,然后完成相应的赋值



浅谈Tomcat的启动流程(源码级别)_tomcat_15


4)addRule

第3)条中出现了一个addRule方法(其余和第2)点中的相同)

对于这种规则,你可以直接理解为需要处理的操作就是该类中的begin方法。

在digester.parse(inputSource)的时候(即tomcat调用load方法的第4步),他会拿到对应的rules列表,然后依次遍历执行对应的begin方法



浅谈Tomcat的启动流程(源码级别)_初始化_16


补充:

如果你仔细研究这一步,你会发现,这一步的工作量巨大,他会初始化很多的对象出来,加上有的对象实例化的时候又会实例化其对应的内部组件,所以这个过程还是有些许繁杂。

如处理Connector对象的时候,其会创建对应的Protocol协议处理器protocolHandler,甚至为该协议处理器设置线程池(调用setExecutor方法)。不过准确说这些应该是在第4步的时候才创建,当前步骤仅进行了初始化(调用digester.parse(inputSource)方法)


2、getServer().init(第5步)

如果你理清楚了上面的逻辑,你很容易就知道getServer方法,拿到的server对象就是StandardServer,即此处的操作为调用StandardServer类的init方法

StandardServer类以Standard开头,还记得我在第二章中提到的模板方法模式吗?

此处虽然调用的是接口的init方法,但是由于LifecycleBase对其进行了封装操作,所以该init方法,最终调用到的是initInternal方法。(不理解的跳到第二章的第2点)



1)JMX

在模板方法中,对应的initInternal方法会将对应的服务注册到JMX中,我们则可以直接通过jconsole命令来查看我们启动的服务的参数信息



浅谈Tomcat的启动流程(源码级别)_tomcat_17

1.在cmd中输入jconsole,选择BootStrap



浅谈Tomcat的启动流程(源码级别)_Server_18

2.查看对应的服务信息



浅谈Tomcat的启动流程(源码级别)_tomcat_19

3.我们修改了server.xml文件,对应的JMX也会相应的变化(你可以把JMX理解为,服务参数值的一个可视化界面)



浅谈Tomcat的启动流程(源码级别)_Server_20


2)globalNamingResources.init()和services[i].init()

这种写法在tomcat里面很常见(虽然我觉得并不是很好,在不了解整体架构的情况下,很难调试),一定要看明白,后面tomcat在start的时候还会出现

还是前面的那个模板方法,如果你不能直接想到应该去哪个类看对应的方法,建议你把第二章第2节,第四章第2节再回看一遍。

以services[i].init方法的代码为例,我将自己的思考的调用逻辑整理如下,希望对你有帮助:

  1. services是StandardServer类的一个数组属性,直接获取肯定没有数据,所以肯定有地方进行了赋值
  2. 该services属性在addService方法中进行了对应赋值操作,且对应的值就是该方法的入参
  3. addService方法就是在第四章第2节的位置进行调用的
  4. 此时你应该明白对应的参数是什么了吧?


浅谈Tomcat的启动流程(源码级别)_tomcat_21


所以services[i].init()方法最终调用的是StandardService类的initInternal(init内部转换调用)

在该类中,主要的处理逻辑与上层大同小异:

  1. 将对应的服务注册到JMX中
  2. 初始化container(对StandardEngine进行初始化)
  3. 初始化executor
  4. 初始化connector(对Connector进行初始化)



浅谈Tomcat的启动流程(源码级别)_初始化_22


3)StandardEngine、Connector

这一步初始化的两个对象,可以对应tomcat的架构图进行理解。位置在第二章的第1节。

  • Engine对象下面应该会去init对应的Host,Host又回去初始化对应的Context
  • Connector对象下面应该会去init对应的Mapper、Protocol、Coyote Adaptor,Protocol又会去初始化对应的Endpoint和Processor

如下图示例:



浅谈Tomcat的启动流程(源码级别)_Server_23


到目前为止我们已经完成了server.xml文件的解析操作。



五、tomcat启动(start)

daemon.start();

与前面load相同,该start方法内部通过反射调用Catalina类的start方法



浅谈Tomcat的启动流程(源码级别)_开发语言_24

在tomcat的启动过程中,代码的基本执行逻辑与load大同小异(该章就不再讲解过程,如果不知道执行流程的可以跳转到tomcact的load步骤),只是完成的功能差别很大。




1、启动容器container.start()



浅谈Tomcat的启动流程(源码级别)_java_25


该部分主要的功能为:部署应用

部署应用需要部署两部分,第一部分为server.xml中定义的Context。第二部分为webapp文件夹下的Context。

部署一个应用主要分为以下步骤

  1. 生成Context对象,server.xml中定义的Context在解析server.xml时就已经生成了,webapp文件夹下的是在部署前生成的
  2. 为每个应用创建一个WebappClassLoader
  3. 解析web.xml,创建一个WebXml类的对象,然后分别将将web.xml解析出来的属性赋值给WebXml类的对象
  4. 设置Context对象中的属性,比如有哪些Wrapper


2、启动连接connector.start()



浅谈Tomcat的启动流程(源码级别)_java_26


该部分主要的功能为:

  1. protocolHandler.start()启动Endpoint开始接收请求(底层会开启对应的的线程接受处理数据),我们浏览器发起一个请求后走的就是这个下面的逻辑。
  2. 构造Mapper对象(存放所有的映射的类),用来处理请求时,快速解析出当前请求对应哪个Context,哪个Wrapper。这里的思路和SpringMVC的的映射是一样的(我很难不怀疑SpringMVC设计的时候有参考tomcat的设计),当浏览器发起一个请求的时候,就会在Mapper类的内部类ContextVersion的变量上进行判断


浅谈Tomcat的启动流程(源码级别)_Server_27


3、补充连接中的protocolHandler.start()

补充对应的代码,在connector.start()这一步,会开启对应的线程接受浏览器发送的数据

该方法会调用AbstractProtocol的start方法,然后会调用endpoint.start()

1.选择何种io模型



浅谈Tomcat的启动流程(源码级别)_tomcat_28

2.调用对应的startInternal方法



浅谈Tomcat的启动流程(源码级别)_Server_29

3.最后调用startAcceptorThreads方法开启一个线程



浅谈Tomcat的启动流程(源码级别)_开发语言_30

4.接受对应的socket请求



浅谈Tomcat的启动流程(源码级别)_java_31


阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Tomcat