tomcat启动流程简要分析
tomcat启动流程简要分析
下面的流程就是跟着走了一遍代码,枯燥无味。
说明
文中大部分都是各处摘抄,做一个记录。顺序没有调整,文章的最后才去介绍了生命周期
Tomcat 框架介绍
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
详细描述了tomcat的架构和各部分组件作用
Tomcat的整体结构如下:
tomcat其实就是http服务器+servlet容器
,tomcat能解析客户端传来的http/https报文,并将其转发至对应的servlet容器去处理,处理之后的结果封装成http/https报文返回给客户端。
Server
:Tomcat服务器,一个Tomcat的实例,服务器启动会自行加载配置文件中配置好的连接器与容器。
Service
:一个Server可以运行多个Service,但是默认只有一个。
Connector
:连接器用于解析不同的协议(HTTP、HTTPS、AJP),对于Servlet来说是无法感觉到协议的不同的,以为连接器将协议解析并提供一个ServletRequest对象,Servlet只能看到ServletRequest对象,Servlet返回的内容封装到了ServletResponse对象中,由连接器转换为协议返还给客户端。一个Service中可以有多个connector,多个connector对应一个container。
为了实现这些功能,Tomcat又实现了3个组件,分别是EndPoint、Processor 和 Adapter
。网络通信的 I/O 模型是变化的, 应用层协议也是变化的,但是整体的处理逻辑是不变的,EndPoint
负责提供字节流给 Processor
,Processor
负责提供 Tomcat Request
对象给 Adapter
,Adapter
负责提供 ServletRequest
对象给容器。总结下来,连接器的三个核心组件 Endpoint
、Processor
和 Adapter
来分别做三件事情,其中 Endpoint
和 Processor
放在一起抽象成了 ProtocolHandler
组件,它们的关系如下图所示。
EndPoint
是用来实现TCP/IP
协议的数据读写的,本质调用操作系统的socket接口。在EndPoint
的具体实现类中,出现了两个重要组件:Acceptor、SocketProcessor
。其中,Acceptor
用于接听Socket连接请求。SocketProcessor
用于处理 Acceptor
接收到的 Socket
请求,它实现 Runnable
接口,在 Run
方法里调用应用层协议处理组件 Processor
进行处理。为了提高处理能力,SocketProcessor
被提交到线程池来执行。Adapter将处理后的请求转发给对应的Engine容器进行下一步处理。
Container
:Container表示容器,包括Engine、Host、Context、Wrapper。
一个Engine容器中有多个Host容器,一个Host容器中又有多个Context容器,一个Context中又有多个Wrapper容器。换种说法,Wrapper
表示一个 Servlet
,Context
表示一个 Web 应用程序,而一个 Web 程序可能有多个 Servlet
;Host
表示一个虚拟主机(内部可以搭建多个web应用),一个 Tomcat 可以配置多个站点(Host);一个站点( Host) 可以部署多个 Web 应用;Engine
代表 引擎,用于管理多个站点(Host),一个 Service 只能有 一个 Engine
。可以通过配置细致了解:
1 | <Server port="8005" shutdown="SHUTDOWN"> // 顶层组件,可包含多个 Service,代表一个 Tomcat 实例 |
一个请求如何定位到 Servlet
一个请求是如何定位到让哪个 Wrapper
的 Servlet
处理的?答案是,Tomcat 是用 Mapper 组件来完成这个任务的。
Mapper
组件的功能就是将用户请求的 URL
定位到一个 Servlet
,它的工作原理是:Mapper
组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host
容器里配置的域名、Context
容器里的 Web
应用路径,以及 Wrapper
容器里 Servlet
映射的路径,你可以想象这些配置信息就是一个多层次的 Map
。
Mapper 保存的关联关系就是通过观察者模式监听在每个组件启动的时候将对应的关联关系保存的。
当一个请求到来时,Mapper
组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet
。请你注意,一个请求 URL 最后只会定位到一个 Wrapper
容器,也就是一个 Servlet
。
假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy
,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?
- 首先根据协议和端口号确定 Service 和 Engine。Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。上面例子中的 URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。
- 根据域名选定 Host。 Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比如例子中的 URL 访问的域名是
user.shopping.com
,因此 Mapper 会找到 Host2 这个容器。 - 根据 URL 路径找到 Context 组件。 Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访问的是 /order,因此找到了 Context4 这个 Context 容器。
- 根据 URL 路径找到 Wrapper(Servlet)。 Context 确定后,Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
对于Tomcat中消息流的流转机制,4个不同级别的容器是通过管道机制进行流转的,对于每个请求都是一层一层处理的。如图所示,当客户端请求到达服务端后,请求被抽象成Request对象后向4个容器进行传递,首先经过Engine容器的管道通过若干阀门,最后通过StandardEngineValve阀门流转到Host容器的管道,处理后继续往下流转,通过StandardHostValve阀门流转到Context容器的管道,继续往下流转,通过StandardContextValve阀门流转到Wrapper容器的管道,而对Servlet的核心处理也正是在StandardWrapperValve阀门中。StandardWrapperValve阀门先由Application FilterChain组件执行过滤器,然后调用Servlet的service方法对请求进行处理,然后对客户端响应。
编写一个Servlet
indexServlet
1 | package com.web; |
web.xml
1 |
|
配置tomcat启动就行:
servlet初始化与装载流程分析
先尝试调试tomcat_embed_core
pom.xml 如下所示
1 |
|
Main.java
1 | package org.example; |
HelloServlet.java
1 | package org.example; |
Servlet初始化流程
根据提供的代码首先运行的是addWebapp(),调用流程图如下:
在java代码的addWebapp()处打上断点跟进去
在方法运行的时候,首先会运行方法参数上的方法也就是getHost()
,然后在getHost
的开始又调用了getEngine()
,之后调用了getServer()
,因为在初始化的Tomcat之后,获取连接器的时候已经创建了service和server,就与上面的流程图所示的一样,getXxxx()
会判断是否为null,如果不为null,直接返回,若为null便进行初始化。
初始化之后便进入addWebapp()
中,通过反射获取了ContextConfig
的Class对象,并构造了该类的实例对象赋给了listener,newInstance()
调用了该类的无参构造去实例化对象,在调用newInstance前,所实例化的类必须已经加载到内存中,并已完成连接阶段(验证、准备、解析),之后调用addWebapp的重构方法。
silence()
的作用是将指定的日志记录器设置为特定的日志级别,以控制日志输出的详细程度,之后就调用了createContext()
返回了一个StandardContext
的实例化对象。
之后会添加xmlListener(从全局的web.xml获取一些配置项)和LifecycleListener到context中,这些操作应该是设置一些包里自带的默认配置。这些都是程序一开始所做的初始化工作。
之后就是实例化一个Servlet,并封装在StandardWrapper.existing属性中,之后将Wrapper添加到context中。
以上分析仅仅是通过ExistingStandardWrapper
类添加Servlet
与StandardWrapper
有些不同:
- 对于
StandardWrapper
,通常是由 Tomcat 根据配置文件或者程序动态创建的,用于部署开发者编写的 Servlet。 - 而
ExistingStandardWrapper
则是开发者自己创建的,用于将现有的 Servlet 对象包装成为 Tomcat 可管理的实例。
Tomcat启动分析
下载tomcat源码进行调试,主要参考:
https://tomcat.apache.org/download-80.cgi
打开之后下载core binary zip文件和source文件。
之后新建一个java maven项目,将pom文件修改为下面的内容
1 |
|
- 将源码解压目录中的 conf、webappas 直接复制到上面新建的项目根路径下D:\javaCode\debugTomcatSrc
- 将源码解压目录中的 java、modules 直接复制到 D:\javaCode\debugTomcatSrc\src\main
- 将Binary解压木马中的 lib 直接复制到 D:\javaCode\debugTomcatSrc\lib
之后就将lib的所有依赖添加到项目中就行:
然后进行运行配置:
然后点击运行
之后就可以打断点调试了,具体的启动流程如下所示:
流程分析
在mian方法中实例化了bootstarp,之后执行bootstrap.init()
方法
在init()
方法中,在这个方法中获取了catalina.base/conf/catalina.properties
中的conmmon.loader、server.loader、shared.loader的值,并将该值所指向的目录和添加到repositorys中,之后将目录中所有的jar包用URLClassLoader进行加载,然后初始化了一个org.apache.catalina.startup.Catalina实例catalinaDaemon,并将其parentClassLoader设置为sharedLoader.
之后就是通过反射获取Catalina.load()
方法并执行
在createStartDigester中为解析\conf\server.xml
文档做准备,添加一些解析的规则,在之后的parse()方法中解析xml文档并进行初始化内容。
这个Server就对应着Server.xml的解析内容,如下所示。
这些listener在之后的生命周期状态转换时调用监听器中对应生命周期事件(生命周期状态转换和生命周期事件在下文有介绍),然后初始化Server getServer().init()
,之后在initInternal()
又会调用Service.init()
在Service.init() 方法中又会调用 initInternal()
,之后便又调用了engine.init()
下面继续初始化了Executor、mapperListener、connector,在connector的初始化里继续初始化了protocolHandler和CoyoteAdapter,CoyoteAdaptor组件是一个将Connector和Container适配起来的适配器。上面说到protocolHandler是Endpoint和processor的封装。
直到这里依然没有看到Host容器和Context容器的初始化,之后就会运行Catalina.start()
->getServer().start()
->startInternal()
在engine.start()
之后会调用LifecycleBase.startInternal()
,LifecycleBase是tomcat统一管理组件生命周期的一个基础类,所有扩展了这个类的子类,都会
各个组件会依次启动,所有扩展的子类也会重写对应抽象类中的抽象方法例如initInternal和startInternal,在每个组件初始化或者启动的时候会自然而然的启动下一个组件,就像树的节点一样。
所以到现在都没看到Host的init()方法,在standardHost
中并没有重写init()和initInternal(),但是在standardHost的父类中实现了initInternal方法,之后会被调用,只是重写了startInternal()
1 | protected void startInternal() throws LifecycleException { |
在engin.start()中会调用org.apache.catalina.core.ContainerBase#startInternal()
方法,在该方法中engine会寻找自己的children,之后会去执行child.start()和startInternal(),standardHost
也继承了ContainerBase
,而ContainerBase和StandardHost、StandardContext、StandardWrapper都实现了startInternal方法,所以他们又会在startInternal方法中进行调用,因为StandardHost还处在NEW state,所以就会调用init(),之后调用start()。
跟进去Host的init()
,之后会进入host的startInternal(),然后是startInternal(),之后会调用super.startInternal()。
进入super.startInternal(),这个children是空的,context不是在这里初始化的。
下面进入setState()
这个方法,在这个方法里其实有一个lifeCycle事件监听器,之后就触发相应的事件。
context是在HostConfig.deployDirectory()中初始化的,初始化完之后将其添加到host的child列表中
HostConfig是什么时候调用的?
HostConfig类实现了LifecycleListener接口的,在StandardHost类启动时调用的HostConfig,也就是上面说到的StandardHost.startInternal(),然后在状态变换的时候触发fireLifecycleEvent()然后调用到HostConfig.lifecycleEvent()然后又调用HostConfig.start(),之后调用deployApps(),在该方法中如果网站是以war包的形式放在webapps目录下那就在deployWARs()进行部署,如果网站是以目录形式出现在webapps目录中那就通过deployDirectories()进行部署。
在deployDirectories方法中实例化了一个线程池来添加网站部署任务,并通过for循环去将webapps目录下的网站逐一部署起来。
DeployDirectory类中实现了Runnable接口,添加到线程池中会执行该类的run方法从而执行org.apache.catalina.startup.HostConfig#deployDirectory()。
通过META-INF/context.xml去初始化一个StandardContext对象,之后会调用host.addChild(context),将context添加到host中。
初始化StandardContext之后,便调用了StandardHost.addChild()->super.addChild(),这里的父类是指 ContainerBase类,在该方法中又会调用addChildInternal(),之后就是调用child.start()
因为child就是StandardContext,之后又调用到了StandardContext.start()->LifecycleBase.start()->StandardContext.startInternal(),在该方法中又调用了fireLifecycleEvent()
然后会调用到ContextConfig中的lifecycleEvent(),和HostConfig一样,最后会初始化wrapper。
这里的Servlets就是conf/web.xml中配置的Servlet(tomcat默认所有网站都会加载的配置),默认配置中有两个Servlet:org.apache.catalina.servlets.DefaultServlet
,org.apache.jasper.servlet.JspServlet
,如果部署的网站中没有配置新的Servlet则将会按照默认的servlet去解析请求。
jspServlet顾名思义就是用来解析jsp文件的,只要文件后缀符合.jsp
或者.jspx
都会被这个jspServlet所处理,另一个defaultServlet则是处理/
根目录的请求,如果自己网站配置新的Servlet去处理url-pattern为/
的请求,则会覆盖掉默认的defaultServlet。换句话说就是conf/web.xml适用于所有网站。
在部署下一个网站的时候可以看到,Servlet的数量就不止默认的那两个了。
生命周期统一接口—–Lifecycle
Tomcat的组件众多如果一个一个启动实在麻烦,所以就有了这个生命周期统一管理的接口,即使之后动态扩展了组件,只要实现了这个接口就是可以用Lifecycle管理启动、停止、关闭。Tomcat内部架构中各个核心组件有包含与被包含的关系,例如,Server包含Service, Service包含Container和Connector,往下再一层层包含。Tomcat就是以容器的方式来组织整个系统架构的,就像数据结构的树,树的根节点没有父节点,其他节点有且仅有一个父节点,每个父节点有零个或多个子节点。鉴于此,可以通过父容器启动它的子容器,这样只要启动根容器,即可把其他所有容器都启动,达到统一启动、停止、关闭的效果。
作为统一的接口,Lifecycle把所有的启动、停止、关闭、生命周期相关的方法都组织到一起,就可以很方便地管理Tomcat各个容器组件的生命周期。下面是Lifecycle接口详细的定义。
1 | public interface Lifecycle { |
从上面可以看出,Lifecycle其实就定义了一些状态常量和几个方法,这里主要看init、start、stop三个方法,所有需要被生命周期管理的容器都要实现这个接口,并且各自被父容器的相应方法调用。例如,在初始化阶段,根容器Server组件会调用init方法,而在init方法里会调用它的子容器Service组件的init方法,以此类推。
LifecycleMBeanBase
是对LifecycleBase
的扩展,LifecycleBase
类主要做的事情就是tomcat
容器生命周期转换、监听生命周期事件。
生命周期的状态转化
在LifecycleState
类中定义了tomcat
生命周期的状态:
1 | public enum LifecycleState { |
这里拿Server.init()
举一个例子:
在进入init()
之前,生命周期的state(状态)是NEW
执行具体的initInternal()
之前,将生命周期设置为INITIALIZING
,在完成初始化之后将设置为INITIALIZED
,其他状态类似。如果在生命周期的某个阶段发生意外,则可能经历xx→DESTROYING→DESTROYED。整个生命周期的状态转化情况如下图所示:
生命周期事件监听
如果我们面对这么多状态之间的转换,我们肯定会有这样的需求:我希望在某某状态事情发生之前之后做点什么。Tomcat在这里使用了事件监听器模式来实现这样的功能。一般来说事件监听器需要三个参与者:
- 事件对象,用于封装事件的信息,在事件监听器接口的统一方法中作为参数使用,一般继承java.util.EventObject类。LifecycleEvent类就是事件对象,继承了EventObject类;LifecycleListener为事件监听器接口,里面只定义了一个方法lifecycleEvent (LifecycleEvent event)。很明显,LifecycleEvent作为这个方法的参数。
事件源,触发事件的源头,不同的事件源会触发不同的事件类型。事件源其实是各个组件,在启动初始化的时候会设置对应的状态,从而触发对应状态的事件。
setStateInternal
是事件的直接触发方法。事件监听器,负责监听事件源发出的事件,更确切地说,应该是每当发生事件时,事件源就会调用监听器的统一方法去处理。