浅谈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方法主要做了以下事情:

  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中你可以理解为创建相应的对象

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



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方法

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

  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方法同样是完成指定对象的创建,然后完成相应的赋值



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方法的代码为例,我将自己的思考的调用逻辑整理如下,希望对你有帮助:

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



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

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

  1. 将对应的服务注册到JMX中
  2. 初始化container(对StandardEngine进行初始化)
  3. 初始化executor
  4. 初始化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。

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

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




2、启动连接connector.start()



该部分主要的功能为:

  1. protocolHandler.start()启动Endpoint开始接收请求(底层会开启对应的的线程接受处理数据),我们浏览器发起一个请求后走的就是这个下面的逻辑。
  2. 构造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的启动流程(源码级别)的主要内容,如果未能解决你的问题,请参考以下文章

Tomcat组件架构图梳理

浅谈tomcat1.7 --- windows环境tomcat启动的流程;

浅谈Tomcat接收到一个请求后在其内部的执行流程(源码)

知识小罐头09(tomcat8启动源码分析 下)

知识小罐头08(tomcat8启动源码分析 上)

浅谈HDFS源码的启动与心跳流程