浅谈Tomcat的启动流程(源码级别)
Posted 默辨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Tomcat的启动流程(源码级别)相关的知识,希望对你有一定的参考价值。
文章目录
本文仅涉及对tomcat启动流程的一个大致梳理,侧重讲解流程而非流程数据细节(具体流程过于细节,容易让人摸不着头脑)
一、启动入口
1、startup.sh
如果是直接启动tomcat,我们都是找到xxxx\\apache-tomcat\\bin目录下的startup.sh文件,然后直接双击启动。
观察对应的执行脚本,我们发现该脚本的目的是执行同目录下的catalina.sh文件,并且传递了一个start字符串
2、catalina.sh
1、前面传递了一个start字符串,会进入这个判断(后期Java代码中还会出现该start字符串)。
2、然后调用Java的org.apache.catalina.startup.Bootstrap类,并且传入一个start字符串(此处它并没有使用起那面传入的start字符串,而是直接写了一个固定的字符串start)
3、最终会调用Java的这个main方法
二、基础组件概念梳理
1、整体架构
在进入Bootstrap的代码前,我们需要先了解tomcat的整体架构,以及对应的组件层级关系。
上面的架构图可以简化为如下的流程图,本文主要梳理tomcat启动过程中的数据流程,不涉及太具体的数据内容
2、LifecycleBase抽象类
这是一个实现了Lifecycle接口的抽象类,这个接口和抽象类,乃至对应的实现,在整体结构上是一个模板方法模式
观察LifecycleBase抽象类,我们主要关心一下几个方法,后文tomcat初始化和启动的时候会调用到:
- init:该init方法来源于接口,在抽象类中对init方法进行了实现。抽象类中会调用子类的initInternal方法(同样还是模板方法模式)
- start:同理,其对应子类实现方法为startInternal
3、实现了LifecycleBase抽象类的子类
tomcat在初始化和启动的时候,会直接调用到对应的方法。为避免到时候你对于模板方法的跳转理不清了,所以先提前进行一个说明
记住这个Standard的开头
三、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加载(load)
daemon.load(args);
该方法使用反射调用catalinaDaemon(Catalina对象)的load方法
Catalina类的load方法主要做了以下事情:
- initDirs、initNaming:初始化一些基本的参数信息
- createStartDigester:创建对应的文件解析器(包含指定的解析规则),用于后面解析server.xml文件
- inputSource.setByteStream(inputStream):获取对应的server.xml或者是server-embed.xml文件
- digester.parse(inputSource):根据第2步创建的文件解析器,解析配置文件
- getServer().init():初始化Server容器(可以根据tomcat架构看对应的位置)
1、createStartDigester(第2步)
创建对应的解析规则,在Java中你可以理解为创建相应的对象
初看这个方法,你可能比较懵,但是没关系,你只要理即为每个方法的作用即可。
1)抽象指定的层级关系
类比server.xml文件,我们脑中是一定要抽象出对应的层级关系的:
// 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方法
同理,第二段则可以描述为
- 创建一个GlobalNamingResources对象,使用的类为NamingResources
- 对应的server.xml文件中的标签层级为Server/GlobalNamingResources
- 然后调用父节点(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方法同样是完成指定对象的创建,然后完成相应的赋值
4)addRule
第3)条中出现了一个addRule方法(其余和第2)点中的相同)
对于这种规则,你可以直接理解为需要处理的操作就是该类中的begin方法。
在digester.parse(inputSource)的时候(即tomcat调用load方法的第4步),他会拿到对应的rules列表,然后依次遍历执行对应的begin方法
补充:
如果你仔细研究这一步,你会发现,这一步的工作量巨大,他会初始化很多的对象出来,加上有的对象实例化的时候又会实例化其对应的内部组件,所以这个过程还是有些许繁杂。
如处理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命令来查看我们启动的服务的参数信息
1.在cmd中输入jconsole,选择BootStrap
2.查看对应的服务信息
3.我们修改了server.xml文件,对应的JMX也会相应的变化(你可以把JMX理解为,服务参数值的一个可视化界面)
2)globalNamingResources.init()和services[i].init()
这种写法在tomcat里面很常见(虽然我觉得并不是很好,在不了解整体架构的情况下,很难调试),一定要看明白,后面tomcat在start的时候还会出现
还是前面的那个模板方法,如果你不能直接想到应该去哪个类看对应的方法,建议你把第二章第2节,第四章第2节再回看一遍。
以services[i].init方法的代码为例,我将自己的思考的调用逻辑整理如下,希望对你有帮助:
- services是StandardServer类的一个数组属性,直接获取肯定没有数据,所以肯定有地方进行了赋值
- 该services属性在addService方法中进行了对应赋值操作,且对应的值就是该方法的入参
- addService方法就是在第四章第2节的位置进行调用的
- 此时你应该明白对应的参数是什么了吧?
所以services[i].init()方法最终调用的是StandardService类的initInternal(init内部转换调用)
在该类中,主要的处理逻辑与上层大同小异:
- 将对应的服务注册到JMX中
- 初始化container(对StandardEngine进行初始化)
- 初始化executor
- 初始化connector(对Connector进行初始化)
3)StandardEngine、Connector
这一步初始化的两个对象,可以对应tomcat的架构图进行理解。位置在第二章的第1节。
-
Engine对象下面应该会去init对应的Host,Host又回去初始化对应的Context
-
Connector对象下面应该会去init对应的Mapper、Protocol、Coyote Adaptor,Protocol又会去初始化对应的Endpoint和Processor
如下图示例:
到目前为止我们已经完成了server.xml文件的解析操作。
五、tomcat启动(start)
daemon.start();
与前面load相同,该start方法内部通过反射调用Catalina类的start方法
在tomcat的启动过程中,代码的基本执行逻辑与load大同小异(该章就不再讲解过程,如果不知道执行流程的可以跳转到tomcact的load步骤),只是完成的功能差别很大。
1、启动容器container.start()
该部分主要的功能为:部署应用
部署应用需要部署两部分,第一部分为server.xml中定义的Context。第二部分为webapp文件夹下的Context。
部署一个应用主要分为以下步骤
- 生成Context对象,server.xml中定义的Context在解析server.xml时就已经生成了,webapp文件夹下的是在部署前生成的
- 为每个应用创建一个WebappClassLoader
- 解析web.xml,创建一个WebXml类的对象,然后分别将将web.xml解析出来的属性赋值给WebXml类的对象
- 设置Context对象中的属性,比如有哪些Wrapper
2、启动连接connector.start()
该部分主要的功能为:
- protocolHandler.start()启动Endpoint开始接收请求(底层会开启对应的的线程接受处理数据),我们浏览器发起一个请求后走的就是这个下面的逻辑。
- 构造Mapper对象(存放所有的映射的类),用来处理请求时,快速解析出当前请求对应哪个Context,哪个Wrapper。这里的思路和SpringMVC的的映射是一样的(我很难不怀疑SpringMVC设计的时候有参考tomcat的设计),当浏览器发起一个请求的时候,就会在Mapper类的内部类ContextVersion的变量上进行判断
3、补充连接中的protocolHandler.start()
补充对应的代码,在connector.start()这一步,会开启对应的线程接受浏览器发送的数据
该方法会调用AbstractProtocol的start方法,然后会调用endpoint.start()
1.选择何种io模型
2.调用对应的startInternal方法
3.最后调用startAcceptorThreads方法开启一个线程
4.接受对应的socket请求
以上是关于浅谈Tomcat的启动流程(源码级别)的主要内容,如果未能解决你的问题,请参考以下文章