Tomcat源码解析五部曲(一)、启动过程

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

Tomcat源码确实复杂,没个几星期还真是搞不清楚(对于我来说,为此每天抽出点时间研究,足足花了两个星期左右),比如Tomcat如何初始化我们编写的Servlet,以及存放在哪里?又如何把请求映射到指定的Servlet,以及没找到对应资源又如何处理。每一个过程其实都较难,但是只要多看,多debug,一定能搞清楚的。虽说有时debug心态是崩溃了,小伙伴一定要有耐心哦。

这篇主要记录Tomcat的启动流程,以及他启动时为后续Web请求处理所做的一些铺垫。

先来了解下Tomcat整体架构,在server.xml中的结构也恰恰反应了它,我们应该很熟悉这些节点,可能唯一了解Context标签和Connector的作用。但是还是有一些细节在这个结构中反应不出来,比如Context,它代表一个项目,但是内部还有一个Wrapper存放我们的Servlet。

每个标签在源码中都有对应的实现类,如以下:

Server-------------StandardServer

Service------------StandardService 

Host----------------StandardHost 

Engine-------------StandardEngine

Context-------------StandardContext

Wrapper-----------StandardWrapper

都是以StandarXXXX开头。

Wrapper、Context、Engine、Host也称之为容器,每个容器可以包含多个子容器,最容易理解的应该是一个Host节点下可以包含多个Context,也就是一个主机下可以包含多个项目。Tomcat源码解析五部曲(一)、启动过程在这里就不说每个标签的作用了,干说起来也不容易理解,我们从代码入手,一层层看,也许会明白他的整体架构。

(上述的这些还称之为组件)

组件生命周期

Tomcat启动也就是启动各个组件,也就是Server、Service、Engine、Host、Context等,而这些组件都有生命周期,Tomcat把他定义在Lifecycle接口中,有init()、start()、stop()等这些方法。各个组件都要实现Lifecycle。但不是直接实现,而是先统一先抽象成Container,Container会继承Lifecycle。Engine、Host、Context、Wrapper都会继承ContainerTomcat源码解析五部曲(一)、启动过程但是你会发现,Server和Service没有继承Container,但是他两都实现Lifecycle接口,因为他们也有生命周期。不继承Container的原因之一是在Container中一些抽象方法在他俩中不适用。

在Container中有一个Pipeline方法,这是在Web请求处理时候用到的,会在第二章说,非常重要。还有findChildren()返回所有其子容器,addChild添加子容器。拿Host节点说,他的子容器一定是Context,如果不是,则会报错。

与Lifecycle关联的还有LifecycleListener概念,用于监听容器发出的事件,具体运用场景在下面会提到。

启动过程概述

如果是大概概述,那就是最外层的Server依次调用子组件得init()初始化、子组件也依次调用自己子组件/容器()得init()。init()流程完了之后又从最外层Server依次调用子组件start()进行启动,同样子组件也会调用自己子组件/容器start()启动。每个组件init()、start()都做了非常大工作,但也有例外。

如果要从最外面启动说起,那就到了startup.sh/bat和catalina.sh/bat了,startup也是调用catalina来启动,而catalina中噼里啪啦拿到一堆运行时必备的jar路径,最终在执行java命令时候传递过去,最后一个参数是org.apache.catalina.startup.Bootstrap start,而Bootstrap类就是Tomcat的启动类,start是给Bootstrap中main方法的参数。Tomcat源码解析五部曲(一)、启动过程

Bootstrap和Catalina

Bootstrap位于源码目录org/apache/catalina/startup下,main方法就在其中。

main方法中根据不同参数执行不同动作,但是真正的启动还不在Bootstrap中,而是在Catalina中,Bootstrap也不是直接new Catalina()后调用它的方法,而是在load()中通过反射调用。这个会点java的都可以跟踪到,以及通过反射实例化Catalina在Bootstrap的init方法中,这部分较简单。

Bootstrap启动会先load()后start(),也就是同样Catalina也会先load后start来启动。

这里的先后是指Bootstrap.load()中调用Catalina.load(),而Catalina.load()会调用Server组件的init(),这才开始了所有组件的init()旅程,各个组件做自己该做的事,该初始化ServerSocket的初始化。

init()流程完了之后折回来开始Bootstrap.start(),同样的流程调用Catalina.start(),Catalina.start()在调用Server组件的start()开始所有组件的start()旅程,各个组件开始启动。

再看Catalina.load()方法,下面是非常重要的一段,这段是解析server.xml中的内容,并实例化各个标签。Tomcat源码解析五部曲(一)、启动过程如果此时往下看有一段getServer().init();,这是就是上述所说开始组件的init()旅程,但是getServer()的返回值就是其类的实例变量server,而你翻遍全源码,也找不到server在什么地方实例化。反而只有一个setServer()方法对他赋值。

这你顿时就蒙了,这玩意在什么地方初始化?

其实答案就是在Digester中,Digester是一个把XML转换为Java对象的工具,而这个工具如果你没用过的话,八成不知道server是怎么被初始化。

而上面这段我们要看createStartDigester()方法,挑一段来看。

public class Catalina {
protected Digester createStartDigester() {
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");

digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
}
}

第一段意思是,当解析到Server标签时,创建对象org.apache.catalina.core.StandardServer。

第二段意思是,当解析到Server标签时,设置xml中的属性到某个java对象中,对于Server标签,他有两个属性,port和shutdown,而此时的对象就是第一步创建的StandardServer,也就是会通过反射给StandardServer中的这两个属性赋值。(这两个属性的意思是,在指定的端口上监听指定的消息,这个消息意为要关闭Tomcat,当接收到此消息时,就会走正常流程关闭。而我们非常喜欢直接kill)。

而第三段意思是,当解析到Server标签时候,调用某个对象的setServer方法,参数是Server,也就是StandardServer。

这也就是调用某个对象的setServer方法把上面实例化后的StandardServer传递过去。而这个对象的管理方式非常有意思,是以对象栈来管理,第一个对象其实就是Catalina自己,这是因为上面调用了入栈操作,也就是digester.push(this);

Digester的设计思想是,读取文件过程中,如果遇到一个XML节点的开始部分,则会触发处理规则创建对应实例对象,并放入栈中,当遇到节点的结束部分,这个对象会从栈中取出并清除。

所以,你明白了吗。在来看其中一段。

这一段意思是当解析到Server下的Service标签时,实例化StandardService类、设置属性。

最后一段是调用某个对象的addService,把上面实例化的StandardService传递过去。而这个对象此时就是StandardServer。查看StandardServer源码,确实存在一个public void addService(Service service) {}方法。通过把断点打到这个方法上。你会发现调用栈最上层就是 digester.parse(inputSource)。

  digester.addObjectCreate("Server/Service",
"org.apache.catalina.core.StandardService",
"className");

digester.addSetProperties("Server/Service");

digester.addSetNext("Server/Service",
"addService",
"org.apache.catalina.Service");

上面说过,不做做小练习是不懂digester的,当你做过小练习后,会发现原来这么神奇,这么nb。

也就是说,在xml解析的时候,会实例化很多类,并添加到他所在的父容器中,谁是谁的父容器,以及谁和谁同级,在server.xml已经够清楚的了。

createStartDigester()方法设置的规则非常多,推荐每条对看一遍。还有以下几个类。Tomcat源码解析五部曲(一)、启动过程

init()旅程

我们回到Catalina的load方法(),(这个名字很好记住,叫卡特琳娜,哈哈)。

Tomcat源码解析五部曲(一)、启动过程此处开始先初始化Server。上面说过了。Tomcat源码解析五部曲(一)、启动过程我们知道启动组件就是调用init()、start()方法,init()等被定义在Lifecycle中,但是有一个默认实现的类LifecycleBase,他重写的init方法会把具体的初始化交给子类,也就是那个抽象方法initInternal(),同样start方法也是交给子类实现,所以,我们跟踪源码时候,主要先关注组件的initInternal()、startInternal()方法即可。

Tomcat源码解析五部曲(一)、启动过程所以回到StandardServer的initInternal()方法下,方法中前面一些也不是很明白,无伤大雅,但是主要看下面一段,遍历所有Service,依次初始化,这些Service实例在addService()中被添加,也就是在xml解析的时候,还有从中可以看出,server节点下可以包含多个service实例。Tomcat源码解析五部曲(一)、启动过程由于Service的实现类是StandardService,所以来到StandardService的initInternal(),这里面可做了很多,里面的Engine先不管了,Executor线程池的概念也都清楚吧,还有最重要的MapperListener,他和Mapper两个为维护着url和项目的映射,也就是tomcat调用哪个Servlet处理由他管理,非常重要。当然这里MapperListener没有重写initInternal(),不去管他,还有线程池如果没有配置,是0个,暂时也不用管他。

 @Override
protected void initInternal() throws LifecycleException {
super.initInternal();
/**
* Engine初始化
*/

if (engine != null) {
engine.init();
}
// Initialize any Executors
/**
* 初始化线程池
*/

for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
/**
*/

// Initialize mapper listener
mapperListener.init();
/**
* 连接器初始化
*/

// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
connector.init();
}
}
}

所以就剩下Connector,Connector中包含这一个协议处理器ProtocolHandler,同样在xml解析时实例化,当解析到Connector节点时,根据Connector节点的protocol属性实例化不同的处理器,Tomcat9中默认是HTTP/1.1,也就是实例化Http11NioProtocol(但这不是一定,默认)。

还有ProtocolHandler中包含这着一个AbstractEndpoint,用于启动Socket监听,根据不同IO实现分类,有AprEndpoint、Nio2Endpoint、NioEndpoint,而Http11NioProtocol配合的是NioEndpoint。

(Tomcat在8.5版本后移除了BIO模式,NIO和BIO是什么就不说了,Http11NioProtocol表示基于NIO的http协议处理器)

一开始说过init()方法会调用自己所有组件/容器的init(),当然会走到AbstractEndpoint的init()下,但是他可没有实现Lifecycle接口,init()是他自己的方法,init()方法会调用bind交给子类处理(bind是抽象方法),也就是交给了NioEndpoint(对于Http11NioProtocol来说),NioEndpoint中的bind会进行ServerSocket初始化,这里只是初始化,并没有开始开启线程接收网络请求,开始接收是在startInternal()方法中,稍后在说start的流程。Tomcat源码解析五部曲(一)、启动过程

这样的话init()流程就算启动完成,但还没有细说,细说是说不玩的。

注意,在这个过程中Host和Context都没被调用init(),其实他们是在start()旅程下执行的。

start()旅程

同样在回到Catalina的start中,getServer().start启动服务。

  public void start() {
......
/**
* 启动服务
*/

getServer().start();
.....
}

同样也是一层层调用各个容器的start()方法,也就是:

server的start()遍历所有service,依次调用他的start();

service中调用engine的start(); 

service中调用线程池的start();

service中调用mapperListener的start(); 

service中调用所有Connectors的start();

这里写出各个调用过程太麻烦了,来看重点。

当Service调用Engine的start()后,Engine也没做什么事,直接交给父类处理,父类ContainerBase中做了这样一件事,获取子所有容器,依次调用他的start(),对于Engine来说,子容器就是Host节点,所以在这要调用StandardHost的start()。

public abstract class ContainerBase extends LifecycleMBeanBase
implements Containe
{
/**
* 获取所有子容器,依次调用他们的start
*/

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

private static class StartChild implements Callable<Void> {
private Container child;
public StartChild(Container child) {
this.child = child;
}
@Override
public Void call() throws LifecycleException {
//子容器启动,
child.start();
return null;
}
}
}

StandardHost的startInternal()其实做了一件非常重要的事,就是添加错误响应处理阀门,这个阀门的概念在下一章介绍。接着又交给父类处理。

还有一个概念要了解,StandardHost其实要借助HostConfig完成一些工作,StandardContext也要借助ContextConfig完成一些工作。而如StandardHost要调用HostConfig完成工作,并不是靠hostConfig.xxxx来完成,而是通过事件监听。这就是上面提到的LifecycleListener。

LifecycleListener里面定义了一个lifecycleEvent方法,如下。HostConfig、ContextConfig都会实现他。Tomcat源码解析五部曲(一)、启动过程现在就需要一个List保存并管理所有LifecycleListener实现类,那就是LifecycleBase类了,但是List是private的,所以就需要提供一个方法访问它,通过这个方法遍历List中的所有监听者,依次主动发送事件信息给监听者,监听者根据不同事件类型,做不同处理。

以下是LifecycleBase继承图。可以发现,所有容器都继承了他。Tomcat源码解析五部曲(一)、启动过程也就是拿StandardHost来说,可以调用addLifecycleListener为其添加一个监听者,或者说是事件处理者,通过某个方法遍历所有其下的监听者,调用lifecycleEvent()主动推送事件。

刚才说到接着又交给父类处理,所以来到ContainerBase的startInternal(),其中有这么一句。

setState(LifecycleState.STARTING);

最终也就是调用了这段(Ctrl+鼠标左键一层层进入),这方法也就是上面提到的遍历所有监听者。

 protected void fireLifecycleEvent(String type, Object data) {
/**
* 包装事件
*/

LifecycleEvent event = new LifecycleEvent(this, type, data);
/**
* 对于StandardHost来说,默认的一个监听者就是HostConfig
*/

for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}

注释中说了对于StandardHost默认的一个监听者就是HostConfig,这也就是说必定有一处要调用StandardHost.addLifecycleListener(HostConfig)为他添加事件监听,但是你不去看digester起初都添加了那些处理规则时,肯定找不到在什么地方添加的HostConfig,但是可以换个简单的思路,给HostConfig的构造方法下断点,不可能实例化不调用构造方法吧。Tomcat源码解析五部曲(一)、启动过程最终发现是在LifecycleListenerRule下的begin方法,这同样是在解析阶段。也就是一开始添加的规则处理,位于HostRuleSet.addRuleInstances下,当解析到Host节点时,实例化StandardHost后调用addLifecycleListener添加监听,监听者的类名为org.apache.catalina.startup.HostConfig。Tomcat源码解析五部曲(一)、启动过程从HostConfig的lifecycleEvent来看,if判断事件类型,而这里我们要看start方法().

@Override
public void lifecycleEvent(LifecycleEvent event) {
if xxxxxxx
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}

而start()调用的deployApps开始部署项目。

protected void deployApps() {
...............
/**
* 部署xml标签中context项目
*/

deployDescriptors(configBase, configBase.list());
/**
* 部署war项目
*/

deployWARs(appBase, filteredAppPaths);
/**
* 部署文件夹项目
*/

deployDirectories(appBase, filteredAppPaths);
}

其实这个过程也是非常复杂的,也会在下一篇文章中介绍,这三个方法大概都做了同一件事,也就是从不同地方加载项目并实例化成StandardContext,然后添加到Host中。

当然每个地方都可能存着多个项目,也就是server.xml中配置了多个Context、webapps下有多个war、webapp下有多个项目目录,当一个项目加载完后会记录下这个项目的唯一Key,下一个要部署时,会先判断这个key是否存在,存在则就跳过。也就是下面代码。

  /**
* 判断是否加载,存在指定可以则跳过。
*/

if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;
results.add(es.submit(new DeployDirectory(this, cn, dir)));

而创建StandardContext过程中还要为StandardContext添加监听,也就是向StandardContext中addLifecycleListener(ContextConfig),如下面代码,其中host.getConfigClass()直接返回他的变量configClass,configClass是写死的。

ContextConfig加载我们的Servlet并包装成Wrapper,在将Wrapper添加到StandardContext下,这一个过程也是巨复杂,也会在下一篇中记录。

public class HostConfig implements LifecycleListener {
protected void deployDirectory(ContextName cn, File dir) {
//获取ContextConfig的class
Class<?> clazz = Class.forName(host.getConfigClass());
//实例化
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
//添加
context.addLifecycleListener(listener);
}
}

Tomcat源码解析五部曲(一)、启动过程setState(LifecycleState.STARTING)执行完后,对于StandardHost来说,所有的子容器是你部署的项目集合。可以自己增加两段代码验证。

public abstract class ContainerBase extends LifecycleMBeanBase
implements Container
{
if (this instanceof StandardHost){
/**
* 先获取所有项目,此时会是0
*/

Container[] children1 = findChildren();
int length = children1.length;
}

setState(LifecycleState.STARTING);

if (this instanceof StandardHost){
/**
* 再次获取,此时是你部署的所有项目集合
*/

Container[] children1 = findChildren();
int length = children1.length;
}
}

而我们又忽略了一段,那项目(StandardContext)的init()、start()在那调用了?

其实不难找,上面我们说过,那三个部署项目的方法中会添加StandardContext到Host下。也就是调用他的addChild()、addChildInternal()进行添加,StandardContext的init、start方法就在这里被调用。

public abstract class ContainerBase extends LifecycleMBeanBase
implements Container
{

private void addChildInternal(Container child) {
。。。。
try {
if ((getState().isAvailable() ||
LifecycleState.STARTING_PREP.equals(getState())) &&
startChildren) {
//启动子容器
child.start();
}
} catch (LifecycleException e) {
throw new IllegalStateException(sm.getString("containerBase.child.start"), e);
}
}
}

而StandardContext的start()过程也非常复杂。其中init()也会在这个过程中调用。

mapperListener.start()



结尾的重中之重,这其实就是开头所说为后续请求所做的铺垫,也就是映射。

public class StandardService extends LifecycleMBeanBase implements Service {
protected void startInternal() throws LifecycleException {
。。。。。
mapperListener.start();
。。。。。
}
}

在执行这个过程前,Host下所有的项目已经都准备好了,现在就是为后续处理url地址到项目的映射做准备,就在这一部分中。

当这个过程完了之后。我们就可以给定一个主机地址和uri,拿到对应的Servlet去处理。注意这里Tomcat并不是直接更具请求类型去调用doGet或doPost,而是调用先service(),让service()更具请求类型去调用doGet还是doPost,或者是其他类型。

这段只是演示,getMapper返回的就是Mapper,Mapper会更具主机地址和uri去分配哪个Servlet处理,如果没有能处理的,则返回DefaultServlet处理,他会把具体的结果放入MappingData下,可以根据MappingData拿到对应的Servlet实例,

这段可以放在mapperListener.start();后执行,allocate的结果就是为我们实例化好的Servlet,但如果放入之前,是映射不到的还会报错。

  try {
MessageBytes host = MessageBytes.newInstance();
host.setString("http://localhost:8080");
MessageBytes uri = MessageBytes.newInstance();
uri.setString("/DynamicWebDemo/LoginServlet");
MappingData mappingData =new MappingData();
getMapper().map(host,uri,"",mappingData);
Servlet allocate = mappingData.wrapper.allocate();

System.out.println(allocate);
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
Tomcat源码解析五部曲(一)、启动过程

而这部分也比较多,也单独记录一篇吧。毕竟本文也比较长了,太长的话也没人看。


原文始发于微信公众号(十四个字节):Tomcat源码解析五部曲(一)、启动过程