2020-heibaiying-Tomcat架构解析

Tomcat架构解析

一、Tomcat简介

Tomcat是目前主流的基于Java语言的轻量级应用服务器,它是对是Java Servlet,JavaServer Pages(JSP,Java Expression Language(EL表达式)和Java WebSocket技术的开源实现。当前Tomcat共有四个版本:

  • Tomcat 7:支持Servlet 3.0,JSP 2.2,EL 2.2WebSocket 1.1规范。
  • Tomcat 8.5:支持Servlet 3.1,JSP 2.3,EL 3.0WebSocket 1.1规范,并可以通过安装Tomcat原生库来支持HTTP/2 。当前Tomcat 8.5已经完全取代了Tomcat 8Tomcat 8已停止维护,并不再提供下载。
  • Tomcat 9:是当前主要的发行版;它建立在Tomcat 8.0.x8.5.x之上,并实现了Servlet 4.0,JSP 2.3,EL 3.0,WebSocket 1.1JASPIC 1.1规范。
  • Tomcat 10 (alpha) :是当前主要的开发版;它实现了Servlet 5.0,JSP 3.0,EL 4.0WebSocket 2.0规范。

二、Tomcat架构

Tomcat的整体架构如下:

  • Server:表示整个Servlet容器,在整个Tomcat运行环境中只有唯一一个Server实例。一个Server包含多个Service,每个Service互相独立,但共享一个JVM以及系统类库。
  • Service:一个Service负责维护多个Connector和一个Engine。其中Connector负责开启Socket并监听客户端请求,返回响应数据;Engine负责具体的请求处理。
  • Connector:连接器,用于监听并转换来自客户端Socket请求,然后将Socket请求交由Container处理,支持不同协议以及不同的I/O方式。
  • Engine:表示整个Servlet引擎,在Tomcat中,Engine为最高层级的容器对象。
  • Host:表示Engine中的虚拟机,通常与一个服务器的网络名有关,如域名等。
  • Context:表示ServletContext ,在Servlet规范中,一个ServletContext即表示一个独立的Web应用。
  • Wrapper:是对标准Servlet的封装。

以上各组件的详细介绍如下:

三、连接器

连接器的主要功能是将Socket的输入转换为Request对象,并交由容器进行处理;之后再将容器处理完成的Response对象写到输出流。连接器的内部组件如下:

3.1 ProtocolHandler

1. Endpoint

EndPoint会启动线程来监听服务器端口,并负责处理来自客户端的Socket请求,是对传输层的抽象。它支持以下IO方式:

  • BIO:即最传统的I/O方式;
  • NIO:采用Java NIO类库进行实现,Tomcat 8之后默认该I/O方式,以替换原来的BIO
  • NIO2:采用JDK 7最新的NIO2类库进行实现;
  • APR:采用APR (Apache可移植运行库)实现,APR是使用C/C++编写的本地库,需要单独进行安装。

2. Processor

负责构造RequestResponse对象,并通过Adapter提交到容器进行处理,是对应用层的抽象。它支持以下应用层协议:

  • HTTP / 1.1协议
  • HTTP / 2.0协议:自Tomcat 8.5以及9.0版本后开始支持;
  • AJP协议:即定向包协议。

3. ProtocolHandler

ProtocolHandler通过组合不同类型的EndpointProcessor来实现针对具体协议的处理功能。按照不同的协议(HTTPAJP)和不同的I/O方式(NIO,NIO2,AJP)进行组合,其有以下六个具体的实现类:

4.协议升级

可以看到上面的ProtocolHandler只有对HTTP 1.1协议进行处理的实现类,并没有对HTTP 2.0进行处理的实现类,想要对HTTP 2.0进行处理,需要使用到协议升级:当ProtocolHandler收到的是一个HTTP 2.0请求时,它会根据请求创建一个用于升级处理的令牌UpgradeToken,该令牌中包含了升级处理器HttpUpgradeHandler(接口,对于HTTP 2.0而言,其实现类是Http2UpgradeHandler

3.2 Adapter

Tomcat设计者希望连接器是一个单独的组件,能够脱离Servlet规范而独立存在,以便增加其使用场景,因此Process对输入流封装后得到的Request不是一个Servlet Request,该Request的全限定命名为:org.apache.coyote.Request 。因此在这里需要使用适配器模式(具体实现类是CoyoteAdapter)将其转换为org.apache.catalina.connector.Request,它才是标准的ServletRequest的实现:

3.3 MapperMapperListener

Socket输入流构建好标准的ServletRequest后,连接器还需要知道将Request发往哪一个容器,这需要通过Mapper来实现。Mapper维护了请求路径与容器之间的映射信息。在Tomcat 7及之前的版本中Mapper由连接器自身维护,在Tomcat 8之后的版本中,MapperService进行维护。

MapperListener实现了ContainerListenerLifecycleListener接口,用于在容器组件状态发生变更时,注册或取消对应容器的映射关系,这么做主要是为了支持Tomcat的热部署功能。

四、容器

4.1 ContainerLifecycle

Tomcat中的所有容器都实现了Container接口,它定义了容器共同的属性与方法,而Container接口则继承自Lifecycle接口。Tomcat中的大多数组件都实现了Lifecycle接口,它定义了与组件生命周期相关的公共方法,如 init()start()stop()destroy() :

4.2分层结构

Tomcat之所以采用分层的结构,主要是为了更好的灵活性和可扩展性:

  • Engine:最顶层的容器,一个Service中只有一个Engine
  • Host:代表一个虚拟主机,一个Engine可以包含多个虚拟主机;
  • Context:表示一个具体的Web应用程序,一个虚拟主机可以包含多个Context
  • Wrapper:是TomcatServlet的包装,一个Context中可以有多个Wrapper

Tomcat容器的分层结构在其conf目录下的 server.xml 配置文件中也有体现:

<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">
    </Engine>
  </Service>
</Server>

这里的appBase代表我们应用程序所在父目录,我们部署的每一个应用程序就是一个独立的Context

4.3 PipelineValve

由连接器发过来的请求会最先发送到Engine,最终逐层传递,直至我们编写的Servlet,这种传递主要通过PipelineValve来实现。每层容器都有自己的PipelinePipeline相当于处理管道;每个Pipeline中有一个Valve链,每个Valve可以看做一个独立的处理单元,用于对请求进行处理。最基础的Valve叫做Basic Valve,新增的Valve会位于已有的Valve之前。PipelineValve的接口定义如下:

public interface Pipeline extends Contained {

    public Valve getBasic();           // 获得Basic Valve
    public void setBasic(Valve valve); // 设置Basic Valve
    public void addValve(Valve valve); // 新增Valve
    public Valve[] getValves();        // 获取所有Valve
    public void removeValve(Valve valve);// 移除Valve
    public Valve getFirst(); //获取第一个 Valve

    public boolean isAsyncSupported();
    public void findNonAsyncValves(Set<String> result);
}
public interface Valve {

    public Valve getNext();
    // 每一个Valve都持有其下一个Valve,这是标准的责任链模式
    public void setNext(Valve valve);
    // 对请求进行检查、处理或增强
    public void invoke(Request request, Response response) throws IOException, ServletException;
    public void backgroundProcess();
    public boolean isAsyncSupported();
}

通过PipelineValve责任链模式,每一层容器都可以很方便地进行功能的扩展,来对请求进行检查、处理或增强。每一层处理完成后,就会传递到下一层的First Valve,由下一层进行处理。以Engine容器为例,其实现类为StandardEngine

public class StandardEngine extends ContainerBase implements Engine {
    public StandardEngine() {
        super();
        pipeline.setBasic(new StandardEngineValve());
         ....
        }
    }

StandardEngine创建时就会为其Pipeline设置上一个名为StandardEngineValveBasic ValveStandardEngineValve的实现如下:

final class StandardEngineValve extends ValveBase {

    public StandardEngineValve() {super(true);}

    @Override
    public final void invoke(Request request, Response response) throws IOException, ServletException {

        // 获取当前请求的Host
        Host host = request.getHost();
        if (host == null) {
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }

        // 将请求传递给host的Pipeline的第一个Valve
        host.getPipeline().getFirst().invoke(request, response);
    }
}

EngineBasic Valve(即最后一个Valve)在 invoke 方法中会获取到下一级容器(Host)的第一个Valve,从而完成首尾相接。

4.4 FilterChain

通过PipelineValve的传递,请求最终会传递到最内层容器WrapperBasic Valve,其实现类为StandardWrapperValveStandardWrapperValve会在 invoke 方法中为该请求创建FilterChain,依次执行请求对应的过滤器:

// 为该请求创建Filter Chain
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
.....
// 调用Filter Chain的doFilter方法
filterChain.doFilter(request.getRequest(), response.getResponse());

当到达执行链的末端后,会执行servletservice方法:

servlet.service(request, response);

以我们最常使用的HttpServlet为例,其最终的service方法如下:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                .....
                doGet(req, resp);
            } else {
               ......
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);

        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);

        } ......
    }

至此,来自客户端的请求就逐步传递到我们编写的doGet或者doPost方法中。

五、请求流程

这里对前面的连接器和容器章节进行总结,Tomcat对客户端请求的完整处理流程如下:

六、启动流程

Tomcat整体的启动流程如下图所示:

1. startup.sh & catalina.sh

startup.sh 是对 catalina.sh 的一层薄封装,主要用于检查 catalina.sh 是否存在以及调用它。 catalina.sh 负责启动一个JVM来运行Tomcat的启动类Bootstrap

2. Bootstrap

Bootstrap独立于Tomcat结构之外,它以JAR包的形式存在于 bin 目录下,主要负责初始化Tomcat的类加载器,并通过反射来创建Catalina

3. Catalina

Catalina通过Digester解析server.xml来创建所有的服务组件。Digester是一款能将XML转换为Java对象的事件驱动型工具,简而言之,它通过流读取XML文件,当识别出特定XML节点后,就会创建对应的组件。

七、类加载器

Tomcat并没有完全沿用JVM默认的类加载机制,为了保证Web应用之间的隔离性和加载的灵活性,其采用了下图所示的类加载机制:

1. Web App Class Loader

负责加载 /WEB-INF/classes 目录下的未压缩的Class和资源文件,以及 /WEB-INF/lib 目录下的Jar包。它只对当前的Web应用可见,对其它Web应用均不可见,因此它可以保证Web应用之间的彼此隔离。

2. Shared Class Loader

是所有Web应用的父类加载器,它负责加载Web应用之间共享的类,从而避免资源的重复加载。

3. Catalina Class Loader

用于加载Tomcat应用服务器的类加载器,从而保证TomcatWeb应用程序之间的隔离。

4. Common Class Loader

其作用和Shared Class Loader类似,当TomcatWeb应用程序之间存在共同依赖时,可以使用其进行加载。再往上,流程就与JVM类加载的流程一致了。

参考资料

  • 刘光瑞. Tomcat架构解析.人民邮电出版社. 2017-05