深入理解Tomcat核心工作原理

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

前言

对于源码可以看以前文章,本文章是对Tomcat的核心逻辑进行简要说明,源码文章可能有些错误,可以指出。


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


Tomcat源码五部曲(二)、Web项目加载过程


Tomcat源码五部曲(三)、 请求处理过程


Tomcat源码五部曲(四)、类加载器


Tomcat作为一个鲜为人知的Web服务器,我们不仅要会使用,而且还要了解他的原理,Tomcat又被称为Servlet容器,这就需要我们了解什么是Servlet,Java很多时候只做规范,做数据库的时候有JDBC,那做web服务的时候就有Servlet,他规范了一些东西,要求实现厂商必须这么做,这里的实现厂商比如说是MySQL,他要实现JDBC的规范,而Tomcat就是实现了Servlet的规范,那我们可不可以自己实现Servlet呢?当然可以,但是做到一个完完全全的不是那么容易。

Tomcat做的事情就是从war文件中加载Servlet的实现类,加载后还需要调用其中的方法service方法,了解Servlet的都知道,doGet、doPost是service方法中调用的,那么这个调用的时机,就是接收到网络请求后,所以Tomcat还需要另一个任务,就是创建服务端Socket,一个做简单的Servlet容器,就可以只包含这两个部分,但是做大做强做好,还差不少。

比如Java中有很多IO工作方式,BIO、BIO、AIO、还有一个可能少为人知的叫APR,APR不是Java中的东西,但是他是性能最高的,在以后文章单独讨论。

那么Tomcat作为一只全能猫,几种方法肯定都要支持,这一点的工作量已经就不小了,但是在8.5之后去掉了BIO,脚趾头也能想到原因,太差了,那么这几种总要做个抽象,所以有了AbstractEndpoint,具体实现类有NioEndpoint、Nio2Endpoint,NIO2就是AIO,还有AprEndpoint,这几个类是Tomcat网络部分的核心之一。

除了IO模式,Tomcat还有协议部分,他除了支持HTTP,这是必须的,还支持AJP,所以Tomcat又把他抽象成AbstractProtocol,只有两个子类,AbstractHttp11Protocol和AbstractAjpProtocol,都是Abstract开头,也都是抽象类,所以还有子类,他们的子类其实就是上面说的AbstractEndpoint,比如你要求Tomcat使用HTTP协议,并且IO模式是NIO,那么Http11NioProtocol就会被使用。

这大概就是Tomcat网络部分的需要了解的几个类了。

接下来我们说容器。

容器在Tomcat中是另一个核心,所以也要做抽象,就有了Container接口,他的每一个子类都是重中之重,搞清楚他们就了解了Tomcat的大部分,容器在Tomcat中也是分子、父容器的,每个容器都实现了Container接口,而且命名都是有规律的,每个具体的容器实现类开头都是StandardXXX。

容器的结构可以直接从server.xml中看出,最上面是Server,他代表Tomcat整个体系,全局是唯一的,Server容器中主要做的就是事件监听,对不同事件进行处理、还有监听某个端口接收SHUTDOWN命令等等。他所包含的容器是Service,可以有多个。但是按照Tomcat的容器实现结构来看,Service和Server并不是容器,叫组件,管他叫啥呢,先用容器叫着。

Service代表请求从接受到处理时所有的组件集合,组件之一就是上面说的网络部分,这部分被称为Connector组件,这是请求开始的部分,那么处理的部分是Engine和Engine的子容器,共同合作处理,还有另一个组件是Executor,也就是线程池,这个没什么好说的。

Connector是比较难的一部分,要解析请求,解析参数,解析后送到容器中处理。

Engine是位于最外层的真正意义上的容器,上面的不算,所以他要实现Container接口,在Tomcat中,实现了Container接口的才是真正的容器,他是Servlet的全局引擎,他的实现没做多少代码,不看了就。

Engine的下一个容器是Host,表示主机,主机里面存放着若干Web应用,Web应用也是个容器,叫Context,我们部署的每个Web应用都会转换成Context,有多少个应用就有多少个Context,Host也是比较有用的,他是对主机的抽象,细心的人会看到server.xml中的Host节点上的两个信息,name和appBase,我猜你一定没更改过,假如有这么个场景,你有一个网站发布到了tomcat中,通过域名www.xxx.com就可以访问,但是有一天还有一个网站需要部署,这个网站和其他类型不一样,你想通过二级域名blog.xxx.com访问,你可以直接放入原来的webapps目录下,但这就有个问题,原来的网站已经占据了ROOT,访问新网站必须加项目名,这个时候Host就可以派上用场了,修改name为blog.xxx.com,appBase指定一个目录,把新网站放入这个目录下的ROOT中,就可以解决了,这样更优雅。

这其中就是通过请求头的Host属性来控制的,Tomcat解析Host头信息后,看这个值在内部有没有具体的Host容器所对应,有的话就进入这个Host下部署的项目,没有的话就进入默认的Host。

再说Host所管理的Web项目,每个项目是一个Context的实例。解析Context的时候是比较复杂的,解析有两个部分完成,第一部分在Host下,因为只有Host才知道自己所管理的项目有哪些,Host只提取一些简单的信息封装成Context,实例是StandardContext,比如告诉Context原路径名是什么,这个原路径名就是一个项目文件名,可能是war也可能是个目录,然后由Context自行完成剩余任何。

这里的剩余任务非常多,我们简单说下。

首先获取所有可能用到的jar,这里的jar,包括jdk目录下的,也包括项目本身lib目录下的,但没有其他项目下的lib,把他们整合在一个Map中,key是这个jar路径,value是WebXml,WebXml在Tomcat很重要,剩余部分就是对这些jar中的Servlet等信息提取,提取信息的地方有两部分,一个是web.xml,这个我们目前都不怎么用了吧,还有一个就是从注解,如果这个类加了@WebServlet、@WebFilter、@WebListener,那么就把他收集起来。提取类上的信息由ClassParser负责,提取后封装在上面说的WebXml中,所以说WebXml代表Web项目的配置。

这个WebXml可能有多个,每个jar都有一个,项目本身也有一个,最终项目本身的WebXml和其他所有的进行合并,这里就关键了,因为会带来这样一个好处,就是你依赖的jar中可能有一个类上也有@WebServlet,那么这个Servlet在项目启动后,你也可以进行访问,所以说,在你所有项目中,可能有一些Servlet是通用的,那么把他抽象出来,单独打包成jar,放入tomcat的共享目录下,就可以互通。

最后通过项目本身的WebXml构建出Context,其实WebXml和Context的属性大部分都一样,只有个别需要转换,这样一个完整的Context就构建好了,其中就包括路径匹配符到具体Servlet名字的一个映射。

再来说最后一个容器,Wrapper,他位于Context下,可以有多个,Wrapper是一个Servlet的容器,Servlet被Wrapper管理着,比如实例化,调用Servlet的init方法,每个Servlet对当前项目全局只有一个。

实例化Servlet的时候就要牵扯出另一个问题,类加载器。

每个项目都有一个类加载器,用来对自己的class进行加载,通常其他web服务器都一样,必须实现自己的类加载器,这是最基本的,比如两个不同项目可能会依赖同一个第三方的类库的不同版本,那么就不能要求JVM中只有一份,应该保证两个应用程序的类库可以互相独立。

可能和你了解的Tomcat类加载器不一样,有的是对8之前分析,而我们分析的是9,在9中只有一个ParallelWebappClassLoader。

上面就是Tomcat的大概流程,每一个容器准备好后都会添加到父容器,但还有一个需要了解,即生命周期,任何一个大型软件都离不开生命周期,Tomcat中的生命周期是Lifecycle接口,里面定义了init()、start()、destory()方法,Tomcat中所有组件、容器都继承他,讨论Tomcat生命周期的时候其实就是在讨论各个容器的流程,好在Tomcat的继承结构都比较简单,没有向Spring那样会有十几个父类,Tomcat只有五六个。

Tomcat流程非常简单,首先Server需要进行init()方法,那么其内部所有组件、容器都要进行init(),Server的init()会触发到Service的init()、Service的init()会接二连三的触发Connector、Executor的init(),这样以此类推。

init()流程完了之后,就开始进入start()流程,同样先是Server的start()方法,后续就和上面一样了。

还有destroy(),也是同样的流程。

而上面我们所说的启动Socket、解析jar中的Servlet,就是在这些生命周期中完成的。

比如在Host的start()阶段下,通过生命周期监听器,通知到HostConfig,这个类是个真正做实事的东西,他的start()方法进入deployApps(),开始部署app,有下面这几行代码,从之可以看到,首先加载xml中配置的项目,然后是war文件、最后是目录。

 protected void deployApps() {
     //获取xml标签上的appBase绝对路径。
     File appBase = host.getAppBaseFile();
     File configBase = host.getConfigBaseFile();
     //找到可能是web项目的目录
     String[] filteredAppPaths = filterAppPaths(appBase.list());

     deployDescriptors(configBase, configBase.list());
     deployWARs(appBase, filteredAppPaths);
     deployDirectories(appBase, filteredAppPaths);
 }

还有两个重要组件是Pipeline、Valve,Tomcat并不是收到请求后就进入Servlet的service方法,而是先经过Pipeline,做个比如,Pipeline就是一条渠,Valve是渠中的闸门,请求是水,田地是Servlet,水要流进田地,就要经过渠、闸门,闸门控制水到底进不进入田地。

在举个实战例子,你想记录所有Web项目的访问次数,怎么做,该不会是每个项目中单独统计,然后再合并把?

这样太low了,优雅的方法是向最上层容器添加一个闸门。

首先继承Tomcat中的ValveBase,在invoke中处理逻辑,这个时候我们的Servlet还没得到调用, getNext().invoke()用来调用下一个闸门,本闸门放开,不调用的话,相当于本闸门中断请求。

public class TestValve extends ValveBase {
    private static AtomicLong atomicLong = new AtomicLong();
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        atomicLong.addAndGet(1);
        Files.write(Paths.get("/home/HouXinLin/test/text.text"), atomicLong.toString().getBytes(StandardCharsets.UTF_8));
        getNext().invoke(request, response);
    }
}

然后打包成jar,放入Tomcat的lib目录下,在server.xml中的Engine节点下配置如下。

<Engine name="Catalina" defaultHost="localhost">
  <Valve className="com.valve.demo.TestValve" />
</Engine>

这样每个请求都会先经过这里,这就是Pipeline、Valve的作用。

每个容器都有一个Pipeline、Valve,Engine位于最上面,Tomcat中默认的Valve会在最后,负责把这个请求转交给下一个容器的Pipeline、Valve,比如下面这段是Engine中的最后一个Valve的逻辑。

public final void invoke(Request request, Response response)
    host.getPipeline().getFirst().invoke(request, response)
;
}

而对于Wrapper的Valve,他会先调用过滤器,之后会实例化Servlet,进入service方法。

在来讨论请求映射器,一个请求是如何调用对应的Servlet呢?这就是请求映射器需要解决的。

请求映射器的实现类是Mapper,他处理的逻辑比较复杂,就根据我们现在所知道的,Mapper是不是必须得包含所有Host信息?才能根据指定的url进行映射,事实也是如此,内部会有这么一个字段保存所有Host。

  volatile MappedHost[] hosts = new MappedHost[0];

并且还有一个addHost()方法用来添加。并i提供map()方法用来获取这个url信息需要映射到哪个Host下的Context下的Servlet,所以map()方法需要接受三个参数,请求的host、请求地址url、结果返回地址。

映射完后就进入Pipeline了。

在说说错误页是怎么被返回的,处理错误页也是个Valve,被Tomcat添到Host下的Pipeline,他首先会调用下一个Valve,因为只有在下面的Valve中无法处理的时候,他才有权处理,那么就需要一个标识,告诉这个请求我处理了,请你不要在处理,这个标识有三个,可以在ErrorReportValve的report方法下看到。

注释也很清楚。

第一不处理1xx、2xx 、3xx开头的响应状态。

第二不处理已经发送数据了的。

第三不处理没有被标出明确错误的。

// Do nothing on a 1xx, 2xx and 3xx status
// Do nothing if anything has been written already
// Do nothing if the response hasn't been explicitly marked as in error
//    and that error has not been reported.
if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
    return;
}

如果都不满足,那么接下来Tomcat会用一种非常传统的字符拼接,拼出一个html返回。

除了上面的,要完整的了解Tomcat,最开始还需要从Digester开始了解,他用来解析xml,但是对于Digester,只要了解他怎么使用就可以了,源码估计还有些费事。

除此之外,还有会话管理器,集群组件,还有他的JMX。

- END -


原文始发于微信公众号(十四个字节):深入理解Tomcat核心工作原理