面试官:谈谈 Tomcat 架构及启动过程,我一脸懵逼。。

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 这个题目命的其实是很大的,写的时候还是很忐忑的,但我尽可能把这个过程描述清楚。因为这是读过源码以后写的总结,在写的过程中可能会忽略一些前提条件,如果有哪些比较突兀就出现,或不好理解的地方可以给我提 Issue,我会尽快补充修订相关内容。

很多东西在时序图中体现的已经非常清楚了,没有必要再一步一步的作介绍,所以本文以图为主,然后对部分内容加以简单解释。


绘制图形使用的工具是 PlantUML + Visual Studio Code + PlantUML Extension


本文对 Tomcat 的介绍以 Tomcat-9.0.0.M22 为标准。


https://tomcat.apache.org/tomcat-9.0-doc/index.html


Overview

image.png

Bootstrap 作为 Tomcat 对外界的启动类,在 $CATALINA_BASE/bin 目录下,它通过反射创建 Catalina 的实例并对其进行初始化及启动。

Catalina 解析 $CATALINA_BASE/conf/server.xml 文件并创建 StandardServer、StandardService、StandardEngine、StandardHost 等

StandardServer 代表的是整个 Servlet 容器,他包含一个或多个 StandardService

StandardService 包含一个或多个 Connector,和一个 Engine,Connector 和 Engine 都是在解析 conf/server.xml 文件时创建的,Engine 在 Tomcat 的标准实现是 StandardEngine

MapperListener 实现了 LifecycleListener 和 ContainerListener 接口用于监听容器事件和生命周期事件。该监听器实例监听所有的容器,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,当容器有变动时,注册容器到 Mapper。

Mapper 维护了 URL 到容器的映射关系。当请求到来时会根据 Mapper 中的映射信息决定将请求映射到哪一个 Host、Context、Wrapper。

Http11NioProtocol 用于处理 HTTP/1.1 的请求

NioEndpoint 是连接的端点,在请求处理流程中该类是核心类,会重点介绍。

CoyoteAdapter 用于将请求从 Connctor 交给 Container 处理。使 Connctor 和 Container 解耦。

StandardEngine 代表的是 Servlet 引擎,用于处理 Connector 接受的 Request。包含一个或多个 Host(虚拟主机), Host 的标准实现是 StandardHost。

StandardHost 代表的是虚拟主机,用于部署该虚拟主机上的应用程序。通常包含多个 Context (Context 在 Tomcat 中代表应用程序)。Context 在 Tomcat 中的标准实现是 StandardContext。

StandardContext 代表一个独立的应用程序,通常包含多个 Wrapper,一个 Wrapper 容器封装了一个 Servlet,Wrapper的标准实现是 StandardWrapper。

StandardPipeline 组件代表一个流水线,与 Valve(阀)结合,用于处理请求。 StandardPipeline 中含有多个 Valve, 当需要处理请求时,会逐一调用 Valve 的 invoke 方法对 Request 和 Response 进行处理。特别的,其中有一个特殊的 Valve 叫 basicValve,每一个标准容器都有一个指定的 BasicValve,他们做的是最核心的工作。

StandardEngine 的是 StandardEngineValve,他用来将 Request 映射到指定的 Host;

StandardHost 的是 StandardHostValve, 他用来将 Request 映射到指定的 Context;

StandardContext 的是 StandardContextValve,它用来将 Request 映射到指定的 Wrapper;

StandardWrapper 的是 StandardWrapperValve,他用来加载 Rquest 所指定的 Servlet,并调用 Servlet 的 Service 方法。

Tomcat init

image.png

当通过 ./startup.sh 脚本或直接通过 java 命令来启动 Bootstrap 时,Tomcat 的启动过程就正式开始了,启动的入口点就是 Bootstrap 类的 main 方法。

启动的过程分为两步,分别是 init 和 start,本节主要介绍 init;

初始化类加载器。

通过从 CatalinaProperties 类中获取 common.loader 等属性,获得类加载器的扫描仓库。CatalinaProperties 类在的静态块中调用了 loadProperties() 方法,从 conf/catalina.properties 文件中加载了属性.(即在类创建的时候属性就已经加载好了)。

通过 ClassLoaderFactory 创建 URLClassLoader 的实例

通过反射创建 Catalina 的实例并设置 parentClassLoader

setAwait(true)。设置 Catalina 的 await 属性为 true。在 Start 阶段尾部,若该属性为 true,Tomcat 会在 main 线程中监听 SHUTDOWN 命令,默认端口是 8005.当收到该命令后执行 Catalina 的 stop() 方法关闭 Tomcat 服务器。

createStartDigester()。Catalina 的该方法用于创建一个 Digester 实例,并添加解析 conf/server.xml 的 RuleSet。Digester 原本是 Apache 的一个开源项目,专门解析 XML 文件的,但我看 Tomcat-9.0.0.M22 中直接将这些类整合到 Tomcat 内部了,而不是引入 jar 文件。

parse() 方法就是 Digester 处理 conf/server.xml 创建各个组件的过程。值的一提的是这些组件都是使用反射的方式来创建的。特别的,在创建 Digester 的时候,添加了一些特别的 rule Set,用于创建一些十分核心的组件,这些组件在 conf/server.xml 中没有但是其作用都比较大,这里做下简单介绍,当 Start 时用到了再详细说明:

EngineConfig。LifecycleListener 的实现类,触发 Engine 的生命周期事件后调用,这个监听器没有特别大的作用,就是打印一下日志

HostConfig。LifecycleListener 的实现类,触发 Host 的生命周期事件后调用。这个监听器的作用就是部署应用程序,这包括 conf/ / / 目录下所有的 Context xml 文件 和 webapps 目录下的应用程序,不管是 war 文件还是已解压的目录。 另外后台进程对应用程序的热部署也是由该监听器负责的。

ContextConfig。LifecycleListener 的实现类,触发 Context 的生命周期事件时调用。这个监听器的作用是配置应用程序,它会读取并合并 conf/web.xml 和 应用程序的 web.xml,分析 /WEB-INF/classes/ 和 /WEB-INF/lib/*.jar中的 Class 文件的注解,将其中所有的 Servlet、ServletMapping、Filter、FilterMapping、Listener 都配置到 StandardContext 中,以备后期使用。当然了 web.xml 中还有一些其他的应用程序参数,最后都会一并配置到 StandardContext 中。

reconfigureStartStopExecutor() 用于重新配置启动和停止子容器的 Executor。默认是 1 个线程。我们可以配置 conf/server.xml 中 Engine 的 startStopThreads,来指定用于启动和停止子容器的线程数量,如果配置 0 的话会使用 Runtime.getRuntime().availableProcessors() 作为线程数,若配置为负数的话会使用 Runtime.getRuntime().availableProcessors() + 配置值,若和小与 1 的话,使用 1 作为线程数。当线程数是 1 时,使用 InlineExecutorService 它直接使用当前线程来执行启动停止操作,否则使用 ThreadPoolExecutor 来执行,其最大线程数为我们配置的值。

需要注意的是 Host 的 init 操作是在 Start 阶段来做的, StardardHost 创建好后其 state 属性的默认值是 LifecycleState.NEW,所以在其调用 startInternal() 之前会进行一次初始化。

Tomcat Start[Deployment]

image.png

图中从 StandardHost Start StandardContext 的这步其实在真正的执行流程中会直接跳过,因为 conf/server.xml 文件中并没有配置任何的 Context,所以在 findChildren() 查找子容器时会返回空数组,所以之后遍历子容器来启动子容器的 for 循环就直接跳过了。

触发 Host 的 BEFORE_START_EVENT 生命周期事件,HostConfig 调用其 beforeStart() 方法创建 CATALINA_BASE/webapps& $CATALINA_BASE/conf/ / / 目录。

触发 Host 的 START_EVENT 生命周期事件,HostConfig 调用其 start() 方法开始部署已在 CATALINA_BASE/webapps & $CATALINA_BASE/conf/ / / 目录下的应用程序。

解析 $CATALINA_BASE/conf/ / / 目录下所有定义 Context 的 XML 文件,并添加到 StandardHost。这些 XML 文件称为应用程序描述符。正因为如此,我们可以配置一个虚拟路径来保存应用程序中用到的图片,详细的配置过程请参考 开发环境配置指南 – 6.3. 配置图片存放目录

部署 $CATALINA_BASE/webapps 下所有的 WAR 文件,并添加到 StandardHost。

部署 $CATALINA_BASE/webapps 下所有已解压的目录,并添加到 StandardHost。

特别的,添加到 StandardHost 时,会直接调用 StandardContext 的 start() 方法来启动应用程序。启动应用程序步骤请看 Context Start 一节。


在 StandardEngine 和 StandardContext 启动时都会调用各自的 threadStart() 方法,该方法会创建一个新的后台线程来处理该该容器和子容器及容器内各组件的后台事件。StandardEngine 会直接创建一个后台线程,StandardContext 默认是不创建的,和 StandardEngine 共用同一个。后台线程处理机制是周期调用组件的 backgroundProcess() 方法。详情请看 Background process 一节。

MapperListener

addListeners(engine) 方法会将该监听器添加到 StandardEngine 和它的所有子容器中

registerHost() 会注册所有的 Host 和他们的子容器到 Mapper 中,方便后期请求处理时使用。

当有新的应用(StandardContext)添加进来后,会触发 Host 的容器事件,然后通过 MapperListener 将新应用的映射注册到 Mapper 中。

Start 工作都做完以后 Catalina 会创建一个 CatalinaShutdownHook 并注册到 JVM。CatalinaShutdownHook 继承了 Thread,是 Catalina 的内部类。其 run 方法中直接调用了 Catalina 的 stop() 方法来关闭整个服务器。注册该 Thread 到 JVM 的原因是防止用户非正常终止 Tomcat,比如直接关闭命令窗口之类的。当直接关闭命令窗口时,操作系统会向 JVM 发送一个终止信号,然后 JVM 在退出前会逐一启动已注册的 ShutdownHook 来关闭相应资源。

image.png

StandRoot 类实现了 WebResourceRoot 接口,它容纳了一个应用程序的所有资源,通俗的来说就是部署到 webapps 目录下对应 Context 的目录里的所有资源。因为我对 Tomcat 的资源管理部分暂时不是很感兴趣,所以资源管理相关类只是做了简单了解,并没有深入研究源代码。

resourceStart() 方法会对 StandardRoot 进行初始配置

postWorkDirectory() 用于创建对应的工作目录 $CATALINA_BASE/work/ / / , 该目录用于存放临时文件。

StardardContext 只是一个容器,而 ApplicationContext 则是一个应用程序真正的运行环境,相关类及操作会在请求处理流程看完以后进行补充。

StardardContext 触发 CONFIGURE_START_EVENT 生命周期事件,ContextConfig 开始调用 configureStart() 对应用程序进行配置。

这个过程会解析并合并 conf/web.xml & conf/ / /web.xml.default & webapps/ /WEB-INF/web.xml 中的配置。

配置配置文件中的参数到 StandardContext, 其中主要的包括 Servlet、Filter、Listener。

因为从 Servlet3.0 以后是直接支持注解的,所以服务器必须能够处理加了注解的类。Tomcat 通过分析 WEB-INF/classes/ 中的 Class 文件和 WEB-INF/lib/ 下的 jar 包将扫描到的 Servlet、Filter、Listerner 注册到 StandardContext。

setConfigured(true),是非常关键的一个操作,它标识了 Context 的成功配置,若未设置该值为 true 的话,Context 会启动失败。

Background process

image.png

后台进程的作用就是处理一下 Servlet 引擎中的周期性事件,处理周期默认是 10s。

特别的 StandardHost 的 backgroundProcess() 方法会触发 Host 的 PERIODIC_EVENT 生命周期事件。然后 HostConfig 会调用其 check() 方法对已加载并进行过重新部署的应用程序进行 reload 或对新部署的应用程序进行热部署。热部署跟之前介绍的部署步骤一致, reload() 过程只是简单的顺序调用 setPause(true)、stop()、start()、setPause(false),其中 setPause(true) 的作用是暂时停止接受请求。

How to read excellent open source projects

真正的第一次阅读开源项目源代码,收获还是很大的。让我在架构设计、面向对象思想、设计模式、Clean Code等等各个方面都有了进步。阅读优秀的开源项目其实是一件很爽的事,因为时不时的会发现一个新的设计思路,然后不由自主的感叹一声居然还可以这样!


当然了,读的时候还是会有一些痛点的,比如说碰到一个变量,但是死活就是找不到初始化的位置,有时通过 Find Usage 工具可以找到,但有些找不到的只能从头开始再过一边源码。有时碰到一个设计思路死活都想不明白为什么这样设计等等,这种情况就只能通过分析更高一层的架构来解决了等等。


下面我简单分享一下我是如何阅读开源项目源码的。


先找一些介绍该项目架构的书籍来看,项目架构是项目核心中的核心,读架构读的是高层次的设计思路,读源码读的是低层次的实现细节。有了高层次的设计思路做指导,源码读起来才会得心应手,因为读的时候心里很清楚现在在读的源码在整个项目架构中处于什么位置。我在读 Tomcat 源码之前先把 《How Tomcat works》 一书过了一边,然后又看了一下 《Tomcat 架构解析》 的第二章,对 Tomcat 的架构有了初步了解。(PS:《How Tomcat works》一书是全英文的,但读起来非常流畅,虽然它是基于 Tomcat 4 和 5 的,但 Tomcat 架构没有非常大的变化,新版的 Tomcat 只是增加了一些组件,如果你要学习 Tomcat 的话,首推这本书!)

如果实在找不到讲架构的书,那就自己动手画类图吧!一般来说,开源项目都是为了提供服务的,我们把提供服务的流程作为主线来分析源代码,这样目的性会更强一些,将该流程中涉及到的类画到类图中,最后得到的类图就是架构!不过分析之前你要先找到流程的入口点,否则分析就无从开始。以 Tomcat 为例,他的主线流程大致可以分为 3 个:启动、部署、请求处理。他们的入口点就是 Bootstrap 类和 接受请求的 Acceptor 类!

有了阅读思路我们下面来说说工具吧。我使用的阅读工具是 IntelliJ IDEA,一款十分强大的 IDE,可能比较重量级,如果你有其他更加轻量级的 Linux 平台源码阅读工具,可以推荐给我~

Structure 栏目可以自定义列出类中的域、方法,然后还可以按照继承结构对域和方法进行分组,这样就可以直接看出来域和方法是在继承结构中哪个类里定义的。当你点击方法和域时,还可以自动滚动到源代码等等。

在源代码中 点击右键 -> Diagrams -> show Diagram 可以显示类的继承结构,图中包含了该类所有的祖先和所有的接口。在该图中选择指定的父类和接口,点击右键 -> show Implementations, IDEA 会列出接口的实现类或该类的子类。

FindUsage、Go To Declaration 等等就不再多说了。

Reference

https://www.amazon.com/How-Tomcat-Works-Budi-Kurniawan/dp/097521280X 

http://product.dangdang.com/25084132.html

https://tomcat.apache.org/tomcat-9.0-doc/index.html

http://www-eu.apache.org/dist/tomcat/tomcat-9/v9.0.0.M22/src/

目录
相关文章
|
6月前
|
设计模式 安全 Java
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
104 0
|
6月前
|
消息中间件 监控 大数据
Kafka消息队列架构与应用场景探讨:面试经验与必备知识点解析
【4月更文挑战第9天】本文详尽探讨了Kafka的消息队列架构,包括Broker、Producer、Consumer、Topic和Partition等核心概念,以及消息生产和消费流程。此外,还介绍了Kafka在微服务、实时数据处理、数据管道和数据仓库等场景的应用。针对面试,文章解析了Kafka与传统消息队列的区别、实际项目挑战及解决方案,并展望了Kafka的未来发展趋势。附带Java Producer和Consumer的代码示例,帮助读者巩固技术理解,为面试做好准备。
614 0
|
3月前
|
监控 网络协议 Java
Tomcat源码解析】整体架构组成及核心组件
Tomcat,原名Catalina,是一款优雅轻盈的Web服务器,自4.x版本起扩展了JSP、EL等功能,超越了单纯的Servlet容器范畴。Servlet是Sun公司为Java编程Web应用制定的规范,Tomcat作为Servlet容器,负责构建Request与Response对象,并执行业务逻辑。
Tomcat源码解析】整体架构组成及核心组件
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
410 37
|
1月前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
2月前
|
人工智能 前端开发 Java
【Tomcat源码分析】启动过程深度解析 (二)
本文深入探讨了Tomcat启动Web应用的过程,重点解析了其加载ServletContextListener及Servlet的机制。文章从Bootstrap反射调用Catalina的start方法开始,逐步介绍了StandardServer、StandardService、StandardEngine、StandardHost、StandardContext和StandardWrapper的启动流程。每个组件通过Lifecycle接口协调启动,子容器逐层启动,直至整个服务器完全启动。此外,还详细分析了Pipeline及其Valve组件的作用,展示了Tomcat内部组件间的协作机制。
【Tomcat源码分析】启动过程深度解析 (二)
|
6月前
|
存储 运维 关系型数据库
2024年最全ceph的功能组件和架构概述(2),Linux运维工程面试问题
2024年最全ceph的功能组件和架构概述(2),Linux运维工程面试问题
2024年最全ceph的功能组件和架构概述(2),Linux运维工程面试问题
|
3月前
|
存储 NoSQL Java
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
这篇文章是关于Java面试中的分布式架构问题的笔记,包括分布式架构下的Session共享方案、RPC和RMI的理解、分布式ID生成方案、分布式锁解决方案以及分布式事务解决方案。
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
|
3月前
|
存储 消息中间件 缓存
这些年背过的面试题——架构设计篇
本文是技术人面试系架构设计篇,面试中关于架构设计都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
4月前
|
前端开发 Linux Shell
技术心得:基于AR9331(MIPS架构)分析系统启动过程(uboot)
技术心得:基于AR9331(MIPS架构)分析系统启动过程(uboot)
68 0

热门文章

最新文章