Tomcat&Jetty

Tomcat&Jetty

Tomcat & Jetty

全文是对文档的记录。如果感兴趣,可以前往阅读。

[TOC]

Servlet

浏览器发给服务端的是一个 HTTP 格式的请求,HTTP 服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的 Java 类,一般来说不同的请求需要由不同的 Java 类来处理。

但这样做明显有问题,因为 HTTP 服务器的代码跟业务逻辑耦合在一起了,如果新加一个业务方法还要改 HTTP 服务器的代码。于是Servlet规范规定,各种业务类都必须实现一个接口,这个接口就叫 Servlet 接口,有时我们也把实现了 Servlet 接口的业务类叫作 Servlet。

Servlet 容器用来加载和管理业务类。HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器去处理,Servlet 容器会将请求转发到具体的 Servlet,如果这个 Servlet 还没创建,就加载并实例化这个 Servlet,然后调用这个 Servlet 的接口方法。因此 Servlet 接口其实是Servlet 容器跟具体业务类之间的接口。下面我们通过一张图来加深理解。

image-20210630100900111

Tomcat 和 Jetty 都按照 Servlet 规范的要求实现了 Servlet 容器,同时它们也具有 HTTP 服务器的功能。作为 Java 程序员,如果我们要实现新的业务功能,只需要实现一个 Servlet,并把它注册到 Tomcat(Servlet 容器)中,剩下的事情就由 Tomcat 帮我们处理了。

Servlet 接口

public interface Servlet {
    void init(ServletConfig config) throws ServletException;
    
    ServletConfig getServletConfig();
    
    void service(ServletRequest req, ServletResponse resthrows ServletException, IOException;
    
    String getServletInfo();
    
    void destroy();
}

其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:ServletRequest 和 ServletResponse。ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。

你还会注意到 ServletConfig 这个类,ServletConfig 的作用就是封装 Servlet 的初始化参数。你可以在 web.xml 给 Servlet 配置参数,并在程序里通过 getServletConfig 方法拿到这些参数。

Servlet 容器

HTTP 服务器不直接调用 Servlet,而是把请求交给 Servlet 容器来处理,那 Servlet 容器又是怎么工作的呢?接下来介绍 Servlet 容器大体的工作流程:Web 应用的目录格式是什么样的,以及我该怎样扩展和定制化 Servlet 容器的功能

工作流程

HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端。

![image-20210630101646436](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630101646436.png)

Web应用

Web 应用程序有一定的目录结构,在这个目录下分别放置了 Servlet 的类文件、配置文件以及静态资源,Servlet 容器通过读取配置文件,就能找到并加载 Servlet。Web 应用的目录结构大概是下面这样的:

| -  MyWebApp
      | -  WEB-INF/web.xml        -- 配置文件,用来配置 Servlet 等
      | -  WEB-INF/lib/           -- 存放 Web 应用所需各种 JAR 包
      | -  WEB-INF/classes/       -- 存放你的应用类,比如 Servlet 类
      | -  META-INF/              -- 目录存放工程的一些信息

Servlet 规范里定义了ServletContext这个接口来对应一个 Web 应用。Web 应用部署好后,Servlet 容器在启动时会加载 Web 应用,并为每个 Web 应用创建唯一的 ServletContext 对象。你可以把 ServletContext 看成是一个全局对象,一个 Web 应用可能有多个 Servlet,这些 Servlet 可以通过全局的 ServletContext 来共享数据,这些数据包括 Web 应用的初始化参数、Web 应用目录下的文件资源等。由于 ServletContext 持有所有 Servlet 实例,你还可以通过它来实现 Servlet 请求的转发。

Tomcat&Jetty在启动时给每个Web应用创建一个全局的上下文环境,这个上下文就是ServletContext,其为后面的Spring容器提供宿主环境。

Tomcat&Jetty在启动过程中触发容器初始化事件,Spring的ContextLoaderListener会监听到这个事件,它的contextInitialized方法会被调用,在这个方法中,Spring会初始化全局的Spring根容器,这个就是Spring的IoC容器,IoC容器初始化完毕后,Spring将其存储到ServletContext中,便于以后来获取。

Tomcat&Jetty在启动过程中还会扫描Servlet,一个Web应用中的Servlet可以有多个,以SpringMVC中的DispatcherServlet为例,这个Servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个Servlet请求。

Servlet一般会延迟加载,当第一个请求达到时,Tomcat&Jetty发现DispatcherServlet还没有被实例化,就调用DispatcherServlet的init方法,DispatcherServlet在初始化的时候会建立自己的容器,叫做SpringMVC 容器,用来持有Spring MVC相关的Bean。同时,Spring MVC还会通过ServletContext拿到Spring根容器,并将Spring根容器设为SpringMVC容器的父容器,请注意,Spring MVC容器可以访问父容器中的Bean,但是父容器不能访问子容器的Bean, 也就是说Spring根容器不能访问SpringMVC容器里的Bean。说的通俗点就是,在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。

Tomcat 总体架构

Tomcat 要实现 2 个核心功能:

  • 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
  • 加载和管理 Servlet,以及具体处理 Request 请求。

连接器

连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。

我们可以把连接器的功能需求进一步细化,比如:

  • 监听网络端口。
  • 接受网络连接请求。
  • 读取请求网络字节流。
  • 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
  • 将 Tomcat Request 对象转成标准的 ServletRequest。
  • 调用 Servlet 容器,得到 ServletResponse。
  • 将 ServletResponse 转成 Tomcat Response 对象。
  • 将 Tomcat Response 转成网络字节流。
  • 将响应字节流写回给浏览器。

通过分析连接器的详细功能列表,我们发现连接器需要完成 3 个高内聚的功能:

  • 网络通信。
  • 应用层协议解析。
  • Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。

因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter。

网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。

但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。

![image-20210630193417902](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630193417902.png)

ProtocolHandler 组件

连接器用 ProtocolHandler 来处理网络连接和应用层协议,包含了 2 个重要部件:EndPoint 和 Processor,下面我来详细介绍它们的工作原理。

EndPoint

EndPoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint 是用来实现 TCP/IP 协议的。

EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。

其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。

Processor

如果说 EndPoint 是用来实现 TCP/IP 协议的,那么 Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。

Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。

Adapter 组件

我在前面说过,由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来“存放”这些请求信息。

但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service 方法。

为什么Tomcat不把连接器替换成Netty呢?

第一个原因是Tomcat的连接器性能已经足够好了,同样是Java NIO编程,套路都差不多。

第二个原因是Tomcat做为Web容器,需要考虑到Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的,因此即使用到了Netty,也不能充分发挥它的优势。所以Netty一般用在非HTTP协议和Servlet的场景下。

高级特性

Tomcat APR提高I/O性能

APR(Apache Portable Runtime Libraries)是 Apache 可移植运行时库,它是用 C 语言实现的,其目的是向上层应用程序提供一个跨平台的操作系统接口库。Tomcat 可以用它来处理包括文件和网络 I/O,从而提升性能。Tomcat 支持的连接器有 NIO、NIO.2 和 APR。跟 NioEndpoint 一样,AprEndpoint 也实现了非阻塞 I/O,它们的区别是:NioEndpoint 通过调用 Java 的 NIO API 来实现非阻塞 I/O,而 AprEndpoint 是通过 JNI 调用 APR 本地库而实现非阻塞 I/O 的。

某些场景下,比如需要频繁与操作系统进行交互,Socket 网络通信就是这样一个场景,特别是如果你的 Web 应用使用了 TLS 来加密传输,我们知道 TLS 协议在握手过程中有多次网络交互,在这种情况下 Java 跟 C 语言程序相比还是有一定的差距,而这正是 APR 的强项。

AprEndpoint 工作过程

![image-20210630195523296](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630195523296.png)

Acceptor

Accpetor 的功能就是监听连接,接收并建立连接。它的本质就是调用了四个操作系统 API:socket、bind、listen 和 accept。

先封装一个 Java 类,在里面定义一堆用native 关键字修饰的方法,像下面这样:

public class Socket {
  ...
  // 用 native 修饰这个方法,表明这个函数是 C 语言实现
  public static native long create(int family, int type,
                                 int protocol, long cont)
                                 
  public static native int bind(long sock, long sa);
  
  public static native int listen(long sock, int backlog);
  
  public static native long accept(long sock)
}

接着用 C 代码实现这些方法,比如 bind 函数就是这样实现的:

// 注意函数的名字要符合 JNI 规范的要求
JNIEXPORT jint JNICALL 
Java_org_apache_tomcat_jni_Socket_bind(JNIEnv *e, jlong sock,jlong sa)
	{
	    jint rv = APR_SUCCESS;
	    tcn_socket_t *s = (tcn_socket_t *sock;
	    apr_sockaddr_t *a = (apr_sockaddr_t *) sa;
	
        // 调用 APR 库自己实现的 bind 函数
	    rv = (jint)apr_socket_bind(s->sock, a);
	    return rv;
	}

AprEndpoint 的 Acceptor 组件就是调用了 APR 实现的四个 API。

Poller

AprEndpoint 接收到一个新的 Socket 连接后,通过 JNI 调用 APR 中的 poll 方法,而 APR 的poll方法又是调用了操作系统的 epoll API 来实现的。

这里有个特别的地方是在 AprEndpoint 中,我们可以配置一个叫deferAccept的参数,它对应的是 TCP 协议中的TCP_DEFER_ACCEPT,设置这个参数后,当 TCP 客户端有新的连接请求到达时,TCP 服务端先不建立连接,而是再等等,直到客户端有请求数据发过来时再建立连接。这样的好处是服务端不需要用 Selector 去反复查询请求数据是否就绪。

APR 提升性能的秘密

JVM 堆 VS 本地内存

JVM 内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。从 JVM 的角度看,JVM 内存之外的部分叫作本地内存,C 程序代码在运行过程中用到的内存就是本地内存中分配的。

![image-20210630200306067](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630200306067.png)

Tomcat 的 Endpoint 组件在接收网络数据时需要预先分配好一块 Buffer,所谓的 Buffer 就是字节数组byte[],Java 通过 JNI 调用把这块 Buffer 的地址传给 C 代码,C 代码通过操作系统 API 读取 Socket 并把数据填充到这块 Buffer。Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer,下面的代码演示了如何创建两种 Buffer。

  • HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组byte[]也是在 JVM 堆上分配。但是如果用HeapByteBuffer来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆,而不是直接从内核拷贝到 JVM 堆上。
  • DirectByteBuffer 对象本身在 JVM 堆上,但是它持有的字节数组不是从 JVM 堆上分配的,而是从本地内存分配的。DirectByteBuffer 对象中有个 long 类型字段 address,记录着本地内存的地址,这样在接收数据的时候,直接把这个本地内存地址传递给 C 程序,C 程序会将网络数据从内核拷贝到这个本地内存,JVM 可以直接读取这个本地内存,这种方式比 HeapByteBuffer 少了一次拷贝,因此一般来说它的速度会比 HeapByteBuffer 快好几倍。

Tomcat 中的 AprEndpoint 就是通过 DirectByteBuffer 来接收数据的,而 NioEndpoint 和 Nio2Endpoint 是通过 HeapByteBuffer 来接收数据的。你可能会问,NioEndpoint 和 Nio2Endpoint 为什么不用 DirectByteBuffer 呢?这是因为本地内存不好管理,发生内存泄漏难以定位,从稳定性考虑,NioEndpoint 和 Nio2Endpoint 没有去冒这个险。

Sendfile

我们再来考虑另一个网络通信的场景,也就是静态文件的处理。浏览器通过 Tomcat 来获取一个 HTML 文件,而 Tomcat 的处理逻辑无非是两步:

  1. 从磁盘读取 HTML 到内存。
  2. 将这段内存的内容通过 Socket 发送出去。

如果是传统IO方式,会经过多次数据copy。

而 Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。

它带有两个关键参数:Socket 和文件句柄。将文件从磁盘写入 Socket 的过程只有两步:

第一步:将文件内容读取到内核缓冲区。

第二步:数据并没有从内核缓冲区复制到 Socket 关联的缓冲区,只有记录数据位置和长度的描述符被添加到 Socket 缓冲区中;接着把数据直接从内核缓冲区传递给网卡。这个过程你可以看下面的图。

Servlet 容器

容器,顾名思义就是用来装载东西的器具,在 Tomcat 里,容器就是用来装载 Servlet 的。

Tomcat 有两个核心组件:连接器和容器,其中连接器负责外部交流,容器负责内部处理。具体来说就是,连接器处理 Socket 通信和应用层协议的解析,得到 Servlet 请求;而容器则负责处理 Servlet 请求。

![image-20210630174248455](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630174248455.png)

容器的层次结构

Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。

![image-20210630174350037](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630174350037.png)

设计成这么多层次的容器,这不是增加了复杂度吗?其实这背后的考虑是,Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。

Context 表示一个 Web 应用程序,Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet。Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序,Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。

Tomcat 采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照一定的格式要求配置在这个顶层容器中。

![image-20210630175118004](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630175118004.png)

**容器具有父子关系,形成一个树形结构,Tomcat 用组合模式来管理这些容器的。**具体实现方法是,所有容器组件都实现了 Container 接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的 Wrapper,组合容器对象指的是上面的 Context、Host 或者 Engine。Container 接口定义如下:

public interface Container extends Lifecycle {
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public void removeChild(Container child);
    public Container findChild(String name);
}

请求定位 Servlet 的过程

Mapper 组件的功能就是将用户请求的 URL 定位到一个 Servlet,它的工作原理是:Mapper 组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host 容器里配置的域名、Context 容器里的 Web 应用路径,以及 Wrapper 容器里 Servlet 映射的路径,这些配置信息就是一个多层次的 Map。

当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。请注意,一个请求 URL 最后只会定位到一个 Wrapper 容器,也就是一个 Servlet。

![image-20210630175525533](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630175525533.png)

并不是说只有 Servlet 才会去处理请求,实际上这个查找路径上的父子容器都会对请求做一些处理。连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。这个调用过程具体是使用 Pipeline-Valve 管道来完成的。

Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。

Pipeline-Value

每一个容器都有一个 Pipeline 对象。

public interface Pipeline extends Contained {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。

public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

Valve 表示一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有 getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。

只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到。但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。

这是因为 Pipeline 中还有个 getBasic 方法。这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。

![image-20210630192406971](/Users/temperlee/Library/Application Support/typora-user-images/image-20210630192406971.png)

整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve:

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Wrapper 容器的最后一个 Valve 会创建一个 Filter 链,并调用 doFilter() 方法,最终会调到 Servlet 的 service 方法。

你可能会问,前面我们不是讲到了 Filter,似乎也有相似的功能,那 Valve 和 Filter 有什么区别吗?它们的区别是:

  • Valve 是 Tomcat 的私有机制,与 Tomcat 的基础架构 /API 是紧耦合的Servlet API 是公有的标准,所有的 Web 容器包括 Jetty 都支持 Filter 机制。
  • 另一个重要的区别是 Valve 工作在 Web 容器级别,拦截所有应用的请求;而 Servlet Filter 工作在应用级别,只能拦截某个 Web 应用的所有请求。如果想做整个 Web 容器的拦截器,必须通过 Valve 来实现。

Tomcat实现Servlet规范

Servlet 管理

Tomcat 是用 Wrapper 容器来管理 Servlet 的,那 Wrapper 容器具体长什么样子呢?我们先来看看它里面有哪些关键的成员变量:

protected volatile Servlet instance = null;

毫无悬念,它拥有一个 Servlet 实例,并且 Wrapper 通过 loadServlet 方法来实例化 Servlet。代码简化之后的样子如下:

public synchronized Servlet loadServlet() throws ServletException {
    Servlet servlet;
  
    //1. 创建一个 Servlet 实例
    servlet = (Servlet) instanceManager.newInstance(servletClass);    
    
    //2. 调用了 Servlet 的 init 方法,这是 Servlet 规范要求的
    initServlet(servlet);
    
    return servlet;
}

其实 loadServlet 主要做了两件事:创建 Servlet 的实例,并且调用 Servlet 的 init 方法,因为这是 Servlet 规范要求的。

为了加快系统的启动速度,系统一般会采取资源延迟加载的策略,Tomcat 也不例外,默认情况下 Tomcat 在启动时不会加载Servlet,除非把 Servlet 的loadOnStartup参数设置为true。这里还需要注意的是,虽然 Tomcat 在启动时不会创建 Servlet 实例,但是会创建 Wrapper 容器,当有请求来访问某个 Servlet 时,这个 Servlet 的实例才会被创建。

每个容器组件都有自己的 Pipeline,每个 Pipeline 中有一个 Valve 链,并且每个容器组件有一个 BasicValve)。Wrapper 作为一个容器组件,它也有自己的 Pipeline 和 BasicValve,Wrapper 的 BasicValve 叫StandardWrapperValve

当请求到来时,Context 容器的 BasicValve 会调用 Wrapper 容器中 Pipeline 中的第一个 Valve,然后会调用到 StandardWrapperValve。先来看看它的 invoke 方法是如何实现的,简化了代码之后的样子大致是:

public final void invoke(Request request, Response response) {
    //1. 实例化 Servlet. allocate()方法内部会调用:org.apache.catalina.core.StandardWrapper#loadServlet
    servlet = wrapper.allocate();
   
    //2. 给当前请求创建一个 Filter 链
    ApplicationFilterChain filterChain =
        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
 
   //3. 调用这个 Filter 链,Filter 链中的最后一个 Filter 会调用 Servlet
   filterChain.doFilter(request.getRequest(), response.getResponse());
}

StandardWrapperValve 的 invoke 方法比较复杂,去掉其他异常处理的一些细节,本质上就是三步:

  • 第一步,创建 Servlet 实例;
  • 第二步,给当前请求创建一个 Filter 链;
  • 第三步,调用这个 Filter 链。

为什么需要给每个请求创建一个 Filter 链?这是因为每个请求的请求路径都不一样,而 Filter 都有相应的路径映射,因此不是所有的 Filter 都需要来处理当前的请求,我们需要根据请求的路径来选择特定的一些 Filter 来处理。

为什么没有看到调到 Servlet 的 service 方法?这是因为 Filter 链的 doFilter 方法会负责调用 Servlet,具体来说就是 Filter 链中的最后一个 Filter 会负责调用 Servlet。

所以,换句话来说,当调用到pipeline的地方,就会实例化内部的servlet对象。

Filter 管理

我们知道,跟 Servlet 一样,Filter 也可以在web.xml文件里进行配置,不同的是,Filter 的作用域是整个 Web 应用,因此 Filter 的实例是在 Context 容器中进行管理的,Context 容器用 Map 集合来保存 Filter。

Filter 链的存活期很短,它是跟每个请求对应的。一个新的请求来了,就动态创建一个 FIlter 链,请求处理完了,Filter 链也就被回收了。

public final class ApplicationFilterChain implements FilterChain {
  
  //Filter 链中有 Filter 数组,这个好理解
  private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
    
  //Filter 链中的当前的调用位置
  private int pos = 0;
    
  // 总共有多少了 Filter
  private int n = 0;
 
  // 每个 Filter 链对应一个 Servlet,也就是它要调用的 Servlet
  private Servlet servlet = null;
  
  public void doFilter(ServletRequest req, ServletResponse res) {
        internalDoFilter(request,response);
  }
   
  private void internalDoFilter(ServletRequest req,
                                ServletResponse res){
 
    // 每个 Filter 链在内部维护了一个 Filter 数组
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        Filter filter = filterConfig.getFilter();
 
        filter.doFilter(request, response, this);
        return;
    }
 
    servlet.service(request, response);
   
}
  1. Filter 链中除了有 Filter 对象的数组,还有一个整数变量 pos,这个变量用来记录当前被调用的 Filter 在数组中的位置。
  2. Filter 链中有个 Servlet 实例,这个好理解,因为上面提到了,每个 Filter 链最后都会调到一个 Servlet。
  3. Filter 链本身也实现了 doFilter 方法,直接调用了一个内部方法 internalDoFilter。
  4. internalDoFilter 方法的实现比较有意思,它做了一个判断,如果当前 Filter 的位置小于 Filter 数组的长度,也就是说 Filter 还没调完,就从 Filter 数组拿下一个 Filter,调用它的 doFilter 方法。否则,意味着所有 Filter 都调到了,就调用 Servlet 的 service 方法。

Listener 管理

我们接着聊 Servlet 规范里 Listener。跟 Filter 一样,Listener 也是一种扩展机制,你可以监听容器内部发生的事件,主要有两类事件:

  • 第一类是生命状态的变化,比如 Context 容器启动和停止、Session 的创建和销毁。
  • 第二类是属性的变化,比如 Context 容器某个属性值变了、Session 的某个属性值变了以及新的请求来了等。

可以在web.xml配置或者通过注解的方式来添加监听器,在监听器里实现业务逻辑。对于 Tomcat 来说,它需要读取配置文件,拿到监听器类的名字,实例化这些类,并且在合适的时机调用这些监听器的方法。

**Tomcat 是通过 Context 容器来管理这些监听器的。**Context 容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器:

// 监听属性值变化的监听器
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
 
// 监听生命事件的监听器
private Object applicationLifecycleListenersObjects[] = new Object[0];

剩下的事情就是触发监听器了,比如在 Context 容器的启动方法里,就触发了所有的 ServletContextListener:

//1. 拿到所有的生命周期监听器
Object instances[] = getApplicationLifecycleListeners();
 
for (int i = 0; i < instances.length; i++) {
   //2. 判断 Listener 的类型是不是 ServletContextListener
   if (!(instances[i] instanceof ServletContextListener))
      continue;
 
   //3. 触发 Listener 的方法
   ServletContextListener lr = (ServletContextListener) instances[i];
   lr.contextInitialized(event);
}

需要注意的是,这里的 ServletContextListener 接口是一种留给用户的扩展机制,用户可以实现这个接口来定义自己的监听器,监听 Context 容器的启停事件。Spring 就是这么做的。ServletContextListener 跟 Tomcat 自己的生命周期事件 LifecycleListener 是不同的。LifecycleListener 定义在生命周期管理组件中,由基类 LifeCycleBase 统一管理。

下面就以Springboot为例讲一下,Spring是怎么实现Listener接口的。

SpringBoot支持嵌入式容器管理

我们知道 Tomcat 和 Jetty 是组件化的设计,要启动 Tomcat 或者 Jetty 其实就是启动这些组件。在 Tomcat 独立部署的模式下,我们通过 startup 脚本来启动 Tomcat,Tomcat 中的 Bootstrap 和 Catalina 会负责初始化类加载器,并解析server.xml和启动这些组件。

在内嵌式的模式下,Bootstrap 和 Catalina 的工作就由 Spring Boot 来做了,Spring Boot 调用了 Tomcat 和 Jetty 的 API 来启动这些组件。

Spring Boot 中 Web 容器相关的接口

既然要支持多种 Web 容器,Spring Boot 对内嵌式 Web 容器进行了抽象,定义了WebServer接口:

public interface WebServer {
    void start() throws WebServerException;
    void stop() throws WebServerException;
    int getPort();
}

Spring Boot 还定义了一个工厂ServletWebServerFactory来创建 Web 容器,返回的对象就是上面提到的 WebServer。

public interface ServletWebServerFactory {
    WebServer getWebServer(ServletContextInitializer... initializers);
}

可以看到 getWebServer 有个参数,类型是ServletContextInitializer。它表示 ServletContext 的初始化器,用于 ServletContext 中的一些配置:

public interface ServletContextInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}

请注意,上面提到的 getWebServer 方法会调用 ServletContextInitializer 的 onStartup 方法,也就是说如果你想在 Servlet 容器启动时做一些事情,比如注册你自己的 Servlet,可以实现一个 ServletContextInitializer,在 Web 容器启动时,Spring Boot 会把所有实现了 ServletContextInitializer 接口的类收集起来,统一调它们的 onStartup 方法。

内嵌式 Web 容器的创建和启动

再来看看 Spring Boot 是如何实例化和启动一个 Web 容器的。我们知道,Spring 的核心是一个 ApplicationContext,它的抽象实现类 AbstractApplicationContext实现了著名的refresh方法,它用来新建或者刷新一个 ApplicationContext,在 refresh 方法中会调用 onRefresh 方法,AbstractApplicationContext 的子类可以重写这个方法 onRefresh 方法,来实现特定 Context 的刷新逻辑,因此 ServletWebServerApplicationContext 就是通过重写 onRefresh 方法来创建内嵌式的 Web 容器,具体创建过程是这样的:

@Override
protected void onRefresh() {
     super.onRefresh();
     try {
        // 重写 onRefresh 方法,调用 createWebServer 创建和启动 Tomcat
        createWebServer();
     }
     catch (Throwable ex) {
     }
}
 
//createWebServer 的具体实现
private void createWebServer() {
    // 这里 WebServer 是 Spring Boot 抽象出来的接口,具体实现类就是不同的 Web 容器
    WebServer webServer = this.webServer;
    ServletContext servletContext = this.getServletContext();
    
    // 如果 Web 容器还没创建
    if (webServer == null && servletContext == null) {
        // 通过 Web 容器工厂来创建
        ServletWebServerFactory factory = this.getWebServerFactory();
        // 注意传入了一个 "SelfInitializer"
      
      	// 注意这里,这里做了子类的抽象!!!!!! 
        this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
        
    } else if (servletContext != null) {
        try {
            this.getSelfInitializer().onStartup(servletContext);
        } catch (ServletException var4) {
          ...
        }
    }
 
    this.initPropertySources();
}

ServletWebServerFactory是一个接口,底层的实现如果大家有时间可以看一下,目前可选的实现方式有:

![image-20210701002623026](/Users/temperlee/Library/Application Support/typora-user-images/image-20210701002623026.png)

再来看看 getWebSever 具体做了什么,以 Tomcat 为例,主要调用 Tomcat 的 API 去创建各种组件:

public WebServer getWebServer(ServletContextInitializer... initializers) {
    //1. 实例化一个 Tomcat,可以理解为 Server 组件。
    Tomcat tomcat = new Tomcat();
    
    //2. 创建一个临时目录
    File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    
    //3. 初始化各种组件
    Connector connector = new Connector(this.protocol);
    tomcat.getService().addConnector(connector);
    this.customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    this.configureEngine(tomcat.getEngine());
    
    //4. 创建定制版的 "Context" 组件。
    this.prepareContext(tomcat.getHost(), initializers);
    return this.getTomcatWebServer(tomcat);
}

这里的 Context 是指Tomcat 中的 Context 组件,为了方便控制 Context 组件的行为,Spring Boot 定义了自己的 TomcatEmbeddedContext,它扩展了 Tomcat 的 StandardContext。

注册 Servlet 的三种方式

Servlet 注解

在 Spring Boot 启动类上加上 @ServletComponentScan 注解后,使用 @WebServlet、@WebFilter、@WebListener 标记的 Servlet、Filter、Listener 就可以自动注册到 Servlet 容器中,无需其他代码,我们通过下面的代码示例来理解一下。

@SpringBootApplication
@ServletComponentScan
public class xxxApplication
{}
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {}

在 Web 应用的入口类上加上 @ServletComponentScan, 并且在 Servlet 类上加上 @WebServlet,这样 SpringBoot 会负责将 Servlet 注册到内嵌的 Tomcat 中。

ServletRegistrationBean

同时 Spring Boot 也提供了 ServletRegistrationBean、FilterRegistrationBean 和 ServletListenerRegistrationBean 这三个类分别用来注册 Servlet、Filter、Listener。假如要注册一个 Servlet,可以这样做:

@Bean
public ServletRegistrationBean servletRegistrationBean() {
    return new ServletRegistrationBean(new HelloServlet(),"/hello");
}

这段代码实现的方法返回一个 ServletRegistrationBean,并将它当作 Bean 注册到 Spring 中,因此你需要把这段代码放到 Spring Boot 自动扫描的目录中,或者放到 @Configuration 标识的类中。

动态注册

你还可以创建一个类去实现前面提到的 ServletContextInitializer 接口,并把它注册为一个 Bean,Spring Boot 会负责调用这个接口的 onStartup 方法。

@Component
public class MyServletRegister implements ServletContextInitializer {
 
    @Override
    public void onStartup(ServletContext servletContext) {
    
        //Servlet 3.0 规范新的 API
        ServletRegistration myServlet = servletContext
                .addServlet("HelloServlet", HelloServlet.class);
                
        myServlet.addMapping("/hello");
        
        myServlet.setInitParameter("name", "Hello Servlet");
    }
 
}

这里请注意两点:

  • ServletRegistrationBean 其实也是通过 ServletContextInitializer 来实现的,它实现了 ServletContextInitializer 接口。
  • 注意到 onStartup 方法的参数是我们熟悉的 ServletContext,可以通过调用它的 addServlet 方法来动态注册新的 Servlet,这是 Servlet 3.0 以后才有的功能。