当前位置: 首页 > 科技观察

深入Tomcat架构和启动过程“包括部署”_0

时间:2023-03-17 22:13:41 科技观察

这个话题其实很大,写的时候还是很紧张,但是我尽量把过程描述清楚。因为这是看完源码后写的总结,在写的过程中可能会忽略一些前置条件。如果有不明白的,可以给我提Issue,我会尽快补充修改相关内容。.很多东西在时序图中已经很清楚了,没必要一步步介绍,所以本文主要以图为主,然后对其中的一些内容进行简单的说明。用来绘制图形的工具有PlantUML+VisualStudioCode+PlantUMLExtensiongraphicsPlantUML源文件:tomcat-architecture.putomcat-init.putomcat-start.putomcat-context-start.putomcat-background-thread.pu这个文章是关于Tomcat的介绍是基于Tomcat-9.0.0.M22。Tomcat-9.0.0.M22是Tomcat的最新版本,但是还没有发布。它实现了Servlet4.0和JSP2.3,并提供了许多新特性。需要1.8及以上版本的JDK支持等,详见Tomcat-9.0-docTomcat-9.0-dochttps://tomcat.apache.org/tomcat-9.0-doc/index.html概述使用Bootstrap作为Tomcat的启动类对外。在$CATALINA_BASE/bin目录下,它通过反射创建Catalina实例并初始化并启动它。Catalina解析$CATALINA_BASE/conf/server.xml文件,创建StandardServer、StandardService、StandardEngine、StandardHost等。StandardServer代表整个Servlet容器,其中包含一个或多个StandardServiceStandardService包含一个或多个Connector,而一个Engine、Connector和引擎是在解析conf/server.xml文件时创建的。Tomcat中Engine的标准实现是StandardEngineMapperListener,它实现了LifecycleListener和ContainerListener接口,用于监控容器事件和生命周期事件。侦听器实例侦听所有容器,包括StandardEngine、StandardHost、StandardContext和StandardWrapper。当容器发生变化时,它会向Mapper注册容器。Mapper维护着URL到容器的映射关系。当有请求到来时,它会根据Mapper中的映射信息,决定将请求映射到哪个Host、Context、Wrapper。Http11NioProtocol用于处理HTTP/1.1请求。NioEndpoint是连接的端点。该类是请求处理过程中的核心类,会被高亮显示。CoyoteAdapter用于将来自Connctor的请求传递给Container进行处理。解耦连接器和容器。StandardEngine表示用于处理Connector接受的Request的Servlet引擎。包含一台或多台Host(虚拟主机),Host的标准实现是StandardHost。StandardHost代表一个虚拟主机,用于在虚拟主机上部署应用程序。通常包含多个Context(Context代表Tomcat中的应用)。Tomcat中Context的标准实现是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方法。Tomcatinit当通过./startup.sh脚本或直接通过java命令启动Bootstrap时,Tomcat的启动过程正式开始,启动的入口是Bootstrap类的main方法。启动过程分为两步,分别是init和start。本节主要介绍init;初始化类加载器。【关于Tomcat类加载机制,可以参考我之前写的一篇文章:浅谈Java类加载机制】通过从CatalinaProperties类中获取common.loader等属性,得到类加载器的扫描仓库。CatalinaProperties类调用静态块中的loadProperties()方法,并从conf/catalina.properties文件加载属性。(也就是说,在创建类时已经加载了属性)。通过ClassLoaderFactory创建URLClassLoader的实例通过反射创建Catalina的实例,并设置parentClassLoadersetAwait(true)。将Catalina的await属性设置为true。Start阶段结束时,如果该属性为true,Tomcat会在主线程中监听SHUTDOWN命令,默认端口为8005。收到命令后,执行Catalina的stop()方法关闭Tomcat服务器.创建启动消化器()。Catalina的这个方法用于创建一个Digester实例,并添加一个解析conf/server.xml的RuleSet。Digester本来是Apache的一个开源项目,专门解析XML文件,但是我看到在Tomcat-9.0.0.M22中,这些类是直接集成到Tomcat中的,而不是jar文件。Digester工具的原理超出了本文的范围。有兴趣的可以参考TheDigester组件-Apache或者《How Tomcat works》-Digester【推荐】章节。parse()方法是Digester处理conf/server.xml创建各个组件的过程。值得一提的是,这些组件是使用反射创建的。特别是在创建Digester的时候,加入了一些特殊的规则集,创建了一些非常核心的组件。这些组件没有包含在conf/server.xml中,但是作用比较大。这里简单介绍一下。当它在Start中使用,然后详细解释:EngineConfig。LifecycleListener的实现类在Engine的生命周期事件触发后被调用。这个监听器没有什么特殊作用,只是打印日志HostConfig。LifecycleListener的实现类,在宿主生命周期事件触发后调用。这个监听器的作用是部署应用,包括conf///目录下的所有Contextxml文件和webapps目录下的应用,无论是war文件还是解压后的目录。另外,监听器还负责后台进程对应用的热部署。上下文配置。LifecycleListener的实现类,当Context的生命周期事件被触发时调用。这个监听器的作用是配置应用程序,它会读取并合并conf/web.xml和应用程序的web.xml,分析Class文件中的/WEB-INF/classes/和/WEB-INF/lib/*.jar注解,在StandardContext中配置所有的Servlet、ServletMapping、Filter、FilterMapping、Listener,以备后用。当然web.xml中还有一些其他的应用参数,它们都会一起配置在StandardContext中。reconfigureStartStopExecutor()用于重新配置启动和停止子容器的执行器。默认为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()前会初始化一次。在TomcatStart[Deployment]图中,在实际执行过程中会直接跳过StandardHostStart到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()方法启动应用。有关启动应用程序的步骤,请参阅上下文启动部分。StandardEngine和StandardContext在启动时都会调用各自的threadStart()方法,这会创建一个新的后台线程来处理容器、子容器和容器内组件的后台事件。StandardEngine会直接创建一个后台线程,StandardContext默认不创建,与StandardEngine共用一个。后台线程处理机制是周期性调用组件的backgroundProcess()方法。有关详细信息,请参阅后台进程部分。MapperListeneraddListeners(engine)方法会将侦听器添加到StandardEngine及其所有子容器。registerHost()会将所有Host及其子容器注册到Mapper中,用于后面的请求处理。当有新应用(StandardContext)被添加时,会触发宿主容器事件,然后通过MapperListener将新应用的映射注册到Mapper中。Start工作完成后,Catalina会创建一个CatalinaShutdownHook并注册到JVM中。CatalinaShutdownHook继承了Thread,是Catalina的一个内部类。它的run方法直接调用Catalina的stop()方法来关闭整个服务器。之所以将Thread注册到JVM,是为了防止用户非正常终止Tomcat,比如直接关闭命令窗口。当命令窗口直接关闭时,操作系统会向JVM发送一个终止信号,然后JVM会在退出前逐一启动注册的ShutdownHook关闭相应的资源。ContextStartStandRoot类实现了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将无法启动。后台进程后台进程的作用是处理Servlet引擎中的周期性事件,默认处理周期为10s。特别是,StandardHost的backgroundProcess()方法会触发Host的PERIODIC_EVENT生命周期事件。然后HostConfig将调用其check()方法重新加载已加载和重新部署的应用程序或热部署新部署的应用程序。热部署与之前介绍的部署步骤一致。reload()过程只是依次调用setPause(true)、stop()、start()、setPause(false),setPause(true)的作用是暂时停止接受请求。如何阅读优秀的开源项目真的是第一次阅读开源项目的源码,收获还是很大的。它让我在架构设计、面向对象思想、设计模式、CleanCode等各方面都取得了进步。阅读优秀的开源项目其实是一件很酷的事情,因为时不时会发现一个新的设计思路,然后不禁感叹还是可以的!当然在阅读的时候还是会有一些痛点,比如我遇到了一个变量,但是就是找不到初始化的位置。有时候通过FindUsage工具可以找到,但是如果找不到,就只能从头开始,翻遍源码。有时候遇到一个设计思路,想不通为什么要这样设计等等,这种情况只能通过分析更高层次的架构等等来解决。简单分享一下我是如何阅读源码的开源项目。先找一些介绍项目架构的书籍。项目架构是项目核心的核心。阅读架构阅读高级设计思想,阅读源代码阅读低级实现细节。在高层次设计思想的指引下,可以轻松阅读源码,因为在阅读的时候,我清楚地知道我现在正在阅读的源码在整个项目结构中的位置。在阅读Tomcat源码之前,我把《How Tomcat works》这本书搁置一旁,接着看了《Tomcat 架构解析》的第二章,对Tomcat的架构有了初步的了解。(PS:《How Tomcat works》是英文的,但是读起来很流畅。虽然是基于Tomcat4和5,但是Tomcat的架构并没有太大变化,新版的Tomcat只是增加了一些组件。如果你想要的话想学Tomcat,这本书先推荐!)实在找不到架构方面的书,那就自己画个类图吧!一般来说,开源项目是为了提供服务,而我们把提供服务的过程当成使用主线来分析源码,这样目的性会更强。把流程中涉及到的类画成类图,最后的类图就是架构!但是在分析之前,首先要找到进程的入口点。否则无法开始分析。以Tomcat为例,它的主要流程大致可以分为三个部分:启动、部署、请求处理。它们的入口是Bootstrap类和接受请求的Acceptor类!有了阅读思路,我们再来说说工具。我使用的阅读工具是IntelliJIDEA,一个非常强大的IDE,可能比较重量级。如果大家有其他轻量级的linux平台源码阅读工具,可以推荐给我哦~Structure栏可以自定义列在类域和方法中,然后按照继承结构对域和方法进行分组,这样您可以直接看到继承结构中定义了域和方法的类。当你点击方法和字段时,你还可以自动滚动到源代码等等。在源代码中右击->Diagrams->showDiagram,可以显示类的继承结构,包括类的所有祖先和所有接口。选中图中指定的父类和接口,右键->showImplementations,IDEA会列出接口的实现类或者类的子类。FindUsage、GoToDeclaration等就不多说了。参考《How Tomcat works》https://www.amazon.com/How-Tomcat-Works-Budi-Kurniawan/dp/097521280X《Tomcat 架构解析》–刘光瑞http://product.dangdang.com/25084132.htmlTomcat-9.0-dochttps://tomcat.apache.org/tomcat-9.0-doc/index.htmlapache-tomcat-9.0.0.M22-srchttp://www-eu.apache.org/dist/tomcat/tomcat-9/v9.0.0。M22/来源/