【Tomcat与网络8】从源码看Tomcat的层次结构

在前面我们介绍了如何通过源码来启动Tomcat,本文我们就来看一下Tomcat是如何一步步启动的,以及在启动过程中,不同的组件是如何加载的。

一般,我们可以通过 Tomcat 的 /bin 目录下的脚本 startup.sh 来启动 Tomcat,如果是window那就用startup.bat来启动。 那我们执行了这个脚本后发生了什么呢?

目录

1.整体结构

2.文件解析与组件创建器—Catalina

3.Catalina如何孕育出众多组件的

4.Server 组件

5.Service 组件

6.Engine 组件


1.整体结构

在一篇中,我们看到tomcat启动是从类Bootstrap里的Main方法开始的,因此对于Tomcat而说,Bootstrap就是创造万物的工具。

之后的工作,可以通过下面这张流程图来了解一下。

  1. Tomcat 本质上是一个 Java 程序,因此 startup.sh 脚本最 核心的工作就是启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。
  2. Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。
  3. Catalina 是一个启动类,它通过解析 server.xml来创建相应的组件,并调用 Server 的 start 方法。
  4. Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start() 方法。
  5. Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。

这样 Tomcat 的启动就算完成了。下面我来详细介绍一下上面这个启动过程中提到的几个非常关键的启动类和组件。

如果我们以一个互联网大厂, 比如腾宝,那么 Bootstrap就是创始人马老师,而Server则是不同的事业群,例如支付宝、天猫、阿里云等等。Service 是事业群总经理,一般事业群里会有多个部门,而在Tomcat里,Service管理两个职能部门:一个是对外的市场部,也就是连接器组件;另一个是对内的研发部,也就是容器组件。

再向下,Engine 则是研发部经理,之后的Service就是具体干活的你和我,在Tomcat里就是Servlet。

2.文件解析与组件创建器—Catalina

Catalina 的主要任务就是创建 Server,它不是直接 new 一个 Server 实例就完事了,而是需要解析 server.xml,所以打开Catalina类的代码,我们会发现很多篇幅都是和解析xml或者Digester类有关系,后者也是解析文件的。

Catalina的作用就是把在 server.xml 里配置的各种组件一一创建出来,接着调用 Server 组件的 init 方法和 start 方法,这样整个 Tomcat 就启动起来了。作为“管理者”,Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩子”。

public void start() {//1. 如果持有的 Server 实例为空,就解析 server.xml 创建出来if (getServer() == null) {load();}//2. 如果创建失败,报错退出if (getServer() == null) {log.fatal(sm.getString("catalina.noServer"));return;}//3. 启动 Servertry {getServer().start();} catch (LifecycleException e) {return;}// 创建并注册关闭钩子if (useShutdownHook) {if (shutdownHook == null) {shutdownHook = new CatalinaShutdownHook();}Runtime.getRuntime().addShutdownHook(shutdownHook);}// 用 await 方法监听停止请求if (await) {await();stop();}
}

那什么是“关闭钩子”,它又是做什么的呢?如果我们需要在 JVM 关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向 JVM 注册一个“关闭钩子”。“关闭钩子”其实就是一个线程,JVM 在停止之前会尝试执行这个线程的 run 方法。下面我们来看看 Tomcat 的“关闭钩子”CatalinaShutdownHook 做了些什么。

protected class CatalinaShutdownHook extends Thread {@Overridepublic void run() {try {if (getServer() != null) {Catalina.this.stop();}} catch (Throwable ex) {...}}
}

从这段代码中你可以看到,Tomcat 的“关闭钩子”实际上就执行了 Server 的 stop 方法,Server 的 stop 方法会释放和清理所有的资源。

3.Catalina如何孕育出众多组件的

我们提的这些重要的组件是在Catalina的哪里创建的呢?如果没有找到位置,我们总是会感觉少了点什么。

这个创建的入口是Catalina里调用createStartDigester()来创建的,创建之后的内容通过Digester来管理。我们分别看创建各个组件的起始位置:

【1】创建Server实例

        digester.addObjectCreate("Server","org.apache.catalina.core.StandardServer","className");digester.addSetProperties("Server");digester.addSetNext("Server","setServer","org.apache.catalina.Server");

Catania中Server的默认实现类是类:org.apache.catalina.core.StandardServer。但是我们可以童工属性className来修改。

【2】创建全局J2EE的企业命名上下文JNDI

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

JDNI这个名字经常见到,但是从来没用过,不管了。

【3】为Server增加生命周期监听器

        digester.addRule("Server/Listener",new ListenerCreateRule(null, "className"));digester.addSetProperties("Server/Listener");digester.addSetNext("Server/Listener","addLifecycleListener","org.apache.catalina.LifecycleListener");

Server元素支持配置生命周期监听器,用于为当前的Servlet实例添加LifecycleListener监听器

顺着这个方法向下看,我们会可以看到为Service添加Connector等等很多组件。

看这个有什么用呢?主要是帮助我们找到了学习的入口,知道怎么初始化,我们就能慢慢理清楚怎么工作了。

4.Server 组件

Server 组件的具体实现类是 StandardServer,我们来看下 StandardServer 具体实现了哪些功能。Server 继承了 LifeCycleBase,它的生命周期被统一管理,并且它的子组件是 Service,因此它还需要管理 Service 的生命周期,也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。Server 在内部维护了若干 Service 组件,它是以数组来保存的,那 Server 是如何添加一个 Service 到数组中的呢?

@Override
public void addService(Service service) {service.setServer(this);synchronized (servicesLock) {// 创建一个长度 +1 的新数组Service results[] = new Service[services.length + 1];// 将老的数据复制过去System.arraycopy(services, 0, results, 0, services.length);results[services.length] = service;services = results;// 启动 Service 组件if (getState().isAvailable()) {try {service.start();} catch (LifecycleException e) {// Ignore}}// 触发监听事件support.firePropertyChange("service", null, service);}}

从上面的代码你能看到,它并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组,这样做的目的其实是为了节省内存空间。

这种用法很明显效率是不高的,因此在工程里极少看到,这里也算增长了我们的见识吧。

除此之外,Server 组件还有一个重要的任务是启动一个 Socket 来监听停止端口,这就是为什么你能通过 shutdown 命令来关闭 Tomcat。不知道你留意到没有,上面 Caralina 的启动方法的最后一行代码就是调用了 Server 的 await 方法。

在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。

5.Service 组件

Service 组件的具体实现类是 StandardService,我们先来看看它的定义以及关键的成员变量。

public class StandardService extends LifecycleBase implements Service {// 名字private String name = null;//Server 实例private Server server = null;// 连接器数组protected Connector connectors[] = new Connector[0];private final Object connectorsLock = new Object();// 对应的 Engine 容器private Engine engine = null;// 映射器及其监听器protected final Mapper mapper = new Mapper();protected final MapperListener mapperListener = new MapperListener(this);

StandardService 继承了 LifecycleBase 抽象类,此外 StandardService 中还有一些我们熟悉的组件,比如 Server、Connector、Engine 和 Mapper。

那为什么还有一个 MapperListener?这是因为 Tomcat 支持热部署,当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式。

作为“管理”角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。我们来看看 Service 启动方法:

protected void startInternal() throws LifecycleException {//1. 触发启动监听器setState(LifecycleState.STARTING);//2. 先启动 Engine,Engine 会启动它子容器if (engine != null) {synchronized (engine) {engine.start();}}//3. 再启动 Mapper 监听器mapperListener.start();//4. 最后启动连接器,连接器会启动它子组件,比如 Endpointsynchronized (connectorsLock) {for (Connector connector: connectors) {if (connector.getState() != LifecycleState.FAILED) {connector.start();}}}
}

从启动方法可以看到,Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。

6.Engine 组件

最后我们再来看看顶层的容器组件 Engine 具体是如何实现的。Engine 本质是一个容器,因此它继承了 ContainerBase 基类,并且实现了 Engine 接口。

public class StandardEngine extends ContainerBase implements Engine {
}

我们知道,Engine 的子容器是 Host,所以它持有了一个 Host 容器的数组,这些功能都被抽象到了 ContainerBase 中,ContainerBase 中有这样一个数据结构:

protected final HashMap<String, Container> children = new HashMap<>();

ContainerBase 用 HashMap 保存了它的子容器,并且 ContainerBase 还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如 ContainerBase 会用专门的线程池来启动子容器。

for (int i = 0; i < children.length; i++) {results.add(startStopExecutor.submit(new StartChild(children[i])));
}

所以 Engine 在启动 Host 子容器时就直接重用了这个方法。

那 Engine 自己做了什么呢?我们知道容器组件最重要的功能是处理请求,而 Engine 容器对请求的“处理”,其实就是把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的。

通过前面的学习,我们知道每一个容器组件都有一个 Pipeline,而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:

final class StandardEngineValve extends ValveBase {public final void invoke(Request request, Response response)throws IOException, ServletException {// 拿到请求中的 Host 容器Host host = request.getHost();if (host == null) {return;}// 调用 Host 容器中的 Pipeline 中的第一个 Valvehost.getPipeline().getFirst().invoke(request, response);}}

这个基础阀实现非常简单,就是把请求转发到 Host 容器。你可能好奇,从代码中可以看到,处理请求的 Host 容器对象是从请求中拿到的,请求对象中怎么会有 Host 容器呢?这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/661353.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【HarmonyOS应用开发】UIAbility实践第二部分(六)

内容接上篇 【HarmonyOS应用开发】UIAbility实践第一部分&#xff08;五&#xff09; 末尾含示例源码 三、UIAbility的生命周期 当用户浏览、切换和返回到对应应用的时候&#xff0c;应用中的UIAbility实例会在其生命周期的不同状态之间转换。 UIAbility类提供了很多回调&a…

关于字符串处理

文章目录 关于字符串处理1、取字符串的长度2、跳过前面的字符3、取字符串右边的字符4、掐头去尾5、取倒数的范围6、删左留右7、删右留左8、查找替换9、大小写转换 关于字符串处理 1、取字符串的长度 [rootlocalhost ~]#strabcd1128 #定义变量 [rootlocalhost ~]#echo ${#str}…

IEEE| IceNet《IceNet for Interactive Contrast Enhancement》论文超详细解读(翻译+精读)

学习资料&#xff1a; 论文题目&#xff1a;《IceNet for Interactive Contrast Enhancement》&#xff08;用于交互式对比度增强的IceNet&#xff09;原文地址&#xff1a;export.arxiv.org/pdf/2109.05838v2.pdf 目录 ABSTRACT—摘要 翻译 精读 I. INTRODUCTION—简介 翻…

通俗易懂三大范式

通俗易懂三大范式 第一范式说的是每个字段不可再分 第二范式说的是不能存在部分依赖&#xff08;不能由联合主键的部分就可以推出其他字段&#xff0c;必须整个联合主键才能推出其他字段&#xff09; 第三范式说的是不能存在间接依赖(A&#xff08;主键&#xff09;→B,B→C…

瑞_23种设计模式_工厂模式

文章目录 1 什么是工厂模式案例案例代码 2 简单工厂模式&#xff08;Simple Factory&#xff09;2.1 简单工厂模式的结构2.2 案例改进——简单工厂模式2.3 案例改进代码实现2.4 简单工厂模式优缺点2.5 拓展——静态工厂 3 工厂方法模式&#xff08;Factory Method&#xff09;★…

【零基础学习CAPL】——CAN报文的发送(按下按钮同时周期性发送)

🙋‍♂️【零基础学习CAPL】系列💁‍♂️点击跳转 文章目录 1.概述2.面板创建3.系统变量创建4.CAPL实现4.1.函数展示4.2.全量报文展示5.效果1.概述 本章主要介绍使用CAPL和Panel在按下按钮时发送周期性CAN报文。 本章主要在“【零基础学习CAPL】——CAN报文的发送(配合P…

数组与字符串深度巩固

经过再三思考觉得今天就写一篇关于数组与字符串相关的文章吧&#xff01;其中字符串主要通过练习来巩固知识亦或是获得新知识。好接下来将进行我们的学习时刻了。 首先我们来思考一个问题&#xff0c;你真的了解数组的数组名吗&#xff1f;数组名真的就单单一个名字而已吗&…

Mac安装配置JDK

Mac安装配置jdk 下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads/#java8 下载jdk1.8及以上需要Oracle账号及密码 dokidoki811163.com\pass:Zywxmxbt1314… 安装jdk 双击安装包&#xff0c;点击.pkg&#xff0c;按照提示安装&#xff0c;配置环境之前…

微信小程序(二十八)网络请求数据进行列表渲染

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.GET请求的规范 2.数据赋值的方法 源码&#xff1a; index.wxml <!-- 列表渲染基础写法&#xff0c;不明白的看上一篇 --> <view class"students"><view class"item">&…

介绍一个超好用的API管理工具:Apipost

Apipost是一款集API调试、生成文档、Mock、测试于一体的协同工具。单个工具可以同时满足接口测试、生成/分享文档、Mock、流程测试等功能&#xff0c;还有超实用的多人多角色间实时协作的功能。将前端、后端、测试三种角色串联起来&#xff0c;从而实现工作流程无缝衔接、提高研…

【Qt】—— 项⽬⽂件解析

目录 &#xff08;一&#xff09;.pro⽂件解析 &#xff08;二&#xff09;widget.h⽂件解析 &#xff08;三&#xff09;main.cpp⽂件解析 &#xff08;四&#xff09;widget.cpp⽂件解析 &#xff08;五&#xff09;widget.ui⽂件解析 &#xff08;一&#xff09;.pro⽂…

P1083 [NOIP2012 提高组] 借教室

P1083 [NOIP2012 提高组] 借教室 题目描述 在大学期间&#xff0c;经常需要租借教室。大到院系举办活动&#xff0c;小到学习小组自习讨论&#xff0c;都需要向学校申请借教室。教室的大小功能不同&#xff0c;借教室人的身份不同&#xff0c;借教室的手续也不一样。 面对海…

net 一台路由器如何让两个不同网段的终端可以通信。

# 终端设备自己设置就行了 # 路由器的设置 The device is running! #################################################### <Huawei> Feb 1 2024 21:21:09-08:00 Huawei %%01IFPDT/4/IF_STATE(l)[0]:Interface GigabitEt hernet0/0/0 has turned into UP state. <…

C++层uevent获取

本文用的是#include <cutils/uevent.h> 主要讲述android中怎么在C层接收uevent uevent 是 kernel层向用户层发送的一个事件 首先创建一个线程用于循环去获取uevent void testUevent {//创建一个线程一直循环pthread_t thread;int ret pthread_create(&thread, nu…

Win11系统连接带HDMI接口的显示器后,电脑没有声音如何调试

解决这个问题的方法很简单&#xff0c;没有那么复杂。之所以使用HDMI接口连接了显示器后没声音&#xff0c;原因就是HDMI接口是包含音频视频两种信号的接口。当电脑的HDMI接口被使用时&#xff0c;系统就会默认从HDMI设备输出声音信号了&#xff0c;而此时如果HDMI设备没有声音…

取巧方式el-select单选重复选择

前言&#xff1a;之前产品是可以多选&#xff0c;我就一想在el-select 加个multiple不就完事了吗&#xff1f;我兴高采烈几分钟就实现了这个选择框&#xff0c;可是后面说单选也要重复多选几个&#xff0c;顿时我就****,又不想自己写个 首先安装element-plus 一定要安装2.5版本…

OSPF排错

目录 实验拓扑图 实验要求 实验排错 故障一 故障现象 故障分析 故障解决 故障二 故障现象 故障分析 故障解决 故障三 故障现象 故障分析 故障解决 故障四 故障现象 故障分析 故障解决 故障五 故障现象 故障分析 故障解决 故障六 故障现象 故障分析 …

wespeaker项目grpc-java客户端开发

非常重要的原始参考资料&#xff1a; 链接: triton-inference-server/client github/grpc java ps&#xff1a; 使用grpc协议的其它项目python/go可以参考git hub目录client/tree/main/src/grpc_generated下的其它项目 其它链接&#xff1a; 想要系统了解triton-inference-ser…

【linux】运维-磁盘空间不足-用到的命令(简洁)

【linux】运维-磁盘空间不足-用到的命令 常用&#xff1a; 注&#xff1a;du -s 和 -d 不能同时都用, -s | -d n 注&#xff1a;df -H 和 -h 区别 -H 1K1000 -h 1K1024 #-T 显示文件系统类型 -h 高可读性显示 df -Th #-c显示总和 ;sort -r 倒序显示 ;2>/dev/nul…

小白初探|神经网络与深度学习

一、学习背景 由于工作的原因&#xff0c;需要开展人工智能相关的研究&#xff0c;虽然不用参与实际研发&#xff0c;但在项目实施过程中发现&#xff0c;人工智能的项目和普通程序开发项目不一样&#xff0c;门槛比较高&#xff0c;没有相关基础没法搞清楚人力、财力如何投入&…