2. Servlet 接口

Servlet 接口是 Java Servlet API 的核心抽象。所有 Servlet 类必须直接或间接的实现该接口,或者更通常做法是通过继承一个实现了该接口的类从而复用许多共性功能。目前有 GenericServletHttpServlet 这两个类实现了 Servlet 接口。大多数情况下,开发者只需要继承 HttpServlet 去实现自己的 Servlet 即可。

2.1 请求处理方法

Servlet 基础接口定义了用于客户端请求处理的 service 方法。当有请求到达时,该方法由 servlet 容器路由到
一个 servlet 实例。

Web 应用程序的并发请求处理通常需要 Web 开发人员去设计适合多线程执行的 Servlet,从而保证 service 方法能在一个特定时间点处理多线程并发执行。(注:即 Servlet 默认是线程不安全的,需要开发人员处理多线程问题)

通常 Web 容器对于并发请求将使用同一个 servlet 处理,并且在不同的线程中并发执行 service 方法。

2.1.1 基于 Http 规范的请求处理方法

HttpServlet 抽象子类在 Servlet 接口基础之上添加了些协议相关的方法,并且这些方法能根据 HTTP 请求类型自动的由 HttpServlet 中实现的 service 方法转发到相应的协议相关的处理方法上。这些方法是:

  • doGet 处理 HTTP GET 请求
  • doPost 处理 HTTP POST 请求
  • doPut 处理 HTTP PUT 请求
  • doDelete 处理 HTTP DELETE 请求
  • doHead 处理 HTTP HEAD 请求
  • doOptions 处理 HTTP OPTIONS 请求
  • doTrace 处理 HTTP TRACE 请求

一般情况下,当开发基于 HTTP 协议的 Servlet 时, Servlet 开发人员将仅去实现 doGetdoPost请求处理方法即可。如果开发人员想使用其他处理方法,其使用方式跟之前的是类似的,即 HTTP 编程都是类似。

2.1.2 附加的方法

doPutdoDelete 方法允许 Servlet 开发人员让支持 HTTP/1.1 的客户端使用这些功能。 HttpServlet 中的doHead 方法可以认为是 doGet 方法的一个特殊形式,它仅返回由 doGet 方法产生的 header 信息。 doOptions 方法返回当前 servlet 支持的 HTTP 方法(译者注:通过 Allow 响应头返回支持的 HTTP 操作,如 GET、POST)。

doTrace 方法返回的响应包含 TRACE 请求的所有头信息。

2.1.3 有条件 GET 支持

HttpServlet 定义了用于支持有条件 GET 操作的 getLastModified 方法。所谓的有条件 GET 操作是指客户端
通过 GET 请求获取资源时,当资源自第一次获取那个时间点发生更改后才再次发生数据,否则将使用客户
端缓存的数据。在一些适当的场合,实现此方法可以更有效的利用网络资源,减少不必要的数据发送。

2.2 实例数量

通过注解描述的(第 8 章 注解和可插拔性)或者在 Web 应用程序的部署描述符(第 14 章 部署描述符)中描述的 servlet 声明,控制着 servlet 容器如何提供 servlet 实例。

对于未托管在分布式环境中(默认)的 servlet 而言, servlet 容器对于每一个 Servlet 声明必须且只能产生一个实例。不过,如果 Servlet 实现了 SingleThreadModel 接口, servlet 容器可以选择实例化多个实例以便处理高负荷请求或者串行化请求到一个特定实例。

如果 servlet 以分布式方式进行部署,容器可以为每个虚拟机( JVM)的每个 Servlet 声明产生一个实例。但是,如果在分布式环境中 servlet 实现了 SingleThreadModel 接口,此时容器可以为每个容器的 JVM 实例化多个 Servlet 实例。

2.2.1 关于 Single Thread Model

SingleThreadModel 接口的作用是保证一个特定 servlet 实例的 service 方法在一个时刻仅能被一个线程执行,一定要注意,此保证仅适用于每一个 servlet 实例,因此容器可以选择池化这些对象。有些对象可以在同一时刻被多个 servlet 实例访问,如 HttpSession 实例,可以在一个特定的时间对多个 Servlet 可用,包括那些实现了SingleThreadModel 接口的 Servlet。

SingleThreadModel 接口已过期,不推荐使用。

2.3 Servlet 生命周期

Servlet 是按照一个严格定义的生命周期被管理,该生命周期规定了 Servlet 如何被加载、实例化、初始化、处理客户端请求,以及何时结束服务。该生命周期可以通过 javax.servlet.Servlet 接口中的 initservicedestroy 这些 API 来表示,所有 Servlet 必须直接或间接的实现 GenericServletHttpServlet 抽象类。

2.3.1 加载和实例化

Servlet 容器负责加载和实例化 Servlet。加载和实例化可以发生在容器启动时,或者延迟初始化直到容器决
定有请求需要处理时。当 Servlet 引擎启动后, servlet 容器必须定位所需要的 Servlet 类。 Servlet 容器使用
普通的 Java 类加载设施加载 Servlet 类。可以从本地文件系统或远程文件系统或者其他网络服务加载。加载
完 Servlet 类后,容器就可以实例化它并使用了。

2.3.2 初始化

一旦一个 Servlet 对象实例化完毕,容器接下来必须在处理客户端请求之前初始化该 Servlet 实例。初始化
的目的是以便 Servlet 能读取持久化配置数据,初始化一些代价高的资源(比如 JDBC API 连接),或者执
行一些一次性的动作。容器通过调用 Servlet 实例的 init 方法完成初始化, init 方法定义在 Servlet 接口中,并且提供一个唯一的 ServletConfig 接口实现的对象作为参数,该对象每个 Servlet 实例一个。

配置对象允许 Servlet 访问由 Web 应用配置信息提供的键-值对的初始化参数。该配置对象也提供给 Servlet
去访问一个 ServletContext 对象, ServletContext 描述了 Servlet 的运行时环境。请参考第 4 章,“ Servlet Context” 获取 ServletContext 接口的更多信息。

2.3.2.1 初始化时的错误条件

在初始化阶段, servlet 实现可能抛出 UnavailableExceptionServletException 异常。在这种情况下, Servlet不能放置到活动服务中,而且 Servlet 容器必须释放它。如果初始化没有成功, destroy 方法不应该被调用。在 实 例 初 始 化 失 败 后 容 器 可 能 再 实 例 化 和 初 始 化 一 个 新 的 实 例 。 此 规 则 的 例 外 是 , 当 抛 出 的 UnavailableException 表示一个不可用的最小时间,容器在创建和初始化一个新的 servlet 实例之前必须等待一段时间。

2.3.2.2 使用工具时的注意事项

当一个工具加载并内省某个 Web 应用程序时触发的静态初始化,这种用法与调用 init 初始化方法是有区别
的。在 Servlet 的 init 方法没被调用,开发人员不应该假定其处于活动的容器环境内。比如,当某个 Servlet
仅有静态方法被调用时,不应该与数据库或企业级 JavaBean( EJB)容器建立连接。

2.3.3 请求处理

Servlet 完成初始化后, Servlet 容器就可以使用它处理客户端请求了。客户端请求由 ServletRequest 类型的request 对象表示。 Servlet 封装响应并返回给请求的客户端,该响应由 ServletResponse 类型的 response 对象表示。这两个对象( request 和 response)是由容器通过参数传递到 Servlet 接口的 service 方法的。

在 HTTP 请 求 的 场 景 下 , 容 器 提 供 的 请 求 和 响 应 对 象 具 体 类 型 分 别 是 HttpServletRequest
HttpServletResponse

需要注意的是,由 Servlet 容器初始化的某个 Servlet 实例在服务期间,可以在其生命周期中不处理任何请
求。

2.3.3.1 多线程问题

Servlet 容器可以并发的发送多个请求到 Servlet 的 service 方法。为了处理这些请求, Servlet 开发者必须为
service 方法的多线程并发处理做好充足的准备。一个替代的方案是开发人员实现 SingleThreadModel 接口,
由容器保证一个 service 方法在同一个时间点仅被一个请求线程调用,但是此方案是不推荐的。 Servlet 容器
可以通过串行化访问 Servlet 的请求,或者维护一个 Servlet 实例池完成该需求。如果 Web 应用中的 Servlet
被标注为分布式的,容器应该为每一个分布式应用程序的 JVM 维护一个 Servlet 实例池。

对于那些没有实现 SingleThreadModel 接口的 Servlet,但是它的 service 方法(或者是那些 HttpServlet 中通过 service 方法分派的 doGetdoPost 等分派方法)是通过 synchronized 关键词定义的, Servlet 容器不能使用实例池方案,并且只能使用序列化请求进行处理。强烈推荐开发人员不要去通过 service 方法(或者那些由 Service 分派的方法),因为这将严重影响性能 。

2.3.3.2 请求处理时的异常

Servlet 在处理一个请求时可能抛出 ServletExceptionUnavailableException 异常。 ServletException 表示在处理请求时出现了一些错误,容器应该采取适当的措施清理掉这个请求。UnavailableException 表示 servlet 目前无法处理请求,或者临时性的或者永久性的。

如果 UnavailableException 表示的是一个永久性的不可用, Servlet 容器必须从服务中移除这个 Servlet,调用它的 destroy 方法,并释放 Servlet 实例。所有被容器拒绝的请求,都会返回一个 SC_NOT_FOUND (404) 响应。

如果 UnavailableException 表示的是一个临时性的不可用,容器可以选择在临时不可用的这段时间内路由任
何请求到 Servlet。所以在这段时间内被容器拒绝的请求,都会返回一个 SC_SERVICE_UNAVAILABLE (503)
响应状态码,且同时会返回一个 Retry-After 头指示此 Servlet 什么时候可用。容器可以选择忽略永久性和临
时性不可用的区别,并把 UnavailableExceptions 视为永久性的,从而 Servlet 抛出 UnavailableException 后需要把它从服务中移除。

2.3.3.3 异步处理

有时候, Filter 及/或 Servlet 在生成响应之前必须等待一些资源或事件以便完成请求处理。比如, Servlet 在
进行生成一个响应之前可能等待一个可用的 JDBC 连接,或者一个远程 web 服务的响应,或者一个 JMS 消
息,或者一个应用程序事件。在 Servlet 中等待是一个低效的操作,因为这是阻塞操作,从而白白占用一个
线程或其他一些受限资源。许多线程为了等待一个缓慢的资源比如数据库经常发生阻塞,可能引起线程饥
饿,且降低整个 Web 容器的服务质量。当
Servlet 3.0 引入了异步处理请求的能力,使线程可以返回到容器,从而执行更多的任务。当开始异步处理请
求时,另一个线程或回调可以或者产生响应,或者调用完成( complete)或请求分派( dispatch),这样,它
可以在容器上下文使用 AsyncContext.dispatch 方法运行。一个典型的异步处理事件顺序是:

  1. 请求被接收到,通过一系列如用于验证的等标准的 filter 之后被传递到 Servlet。
  2. servlet 处理请求参数及(或)内容体从而确定请求的类型。
  3. 该 servlet 发出请求去获取一些资源或数据,例如,发送一个远程 web 服务请求或加入一个等待 JDBC 连
    接的队列。
  4. servlet 不产生响应并返回。
  5. 过了一段时间后,所请求的资源变为可用,此时处理线程继续处理事件,要么在同一个线程,要么通过
    AsyncContext 分派到容器中的一个资源上。
代码示例
@Slf4j
@WebServlet(asyncSupported = true, urlPatterns = "/asyncServlet")
public class AsyncServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        log.info("my.servlet.AsyncServlet.doGet Start..");

        AsyncContext asyncContext = request.startAsync();
        asyncContext.start(() -> {
            log.info("asyncContext.start Start..");
            try {
                log.info("sleep for 10000 milliseconds.");
                Thread.sleep(10000);
                asyncContext.getResponse().getWriter().write("Hello World!");
                log.info("asyncContext.start End..");
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
            asyncContext.complete();
        });

        log.info("my.servlet.AsyncServlet.doGet End..");
    }
}

2.3.3.4 线程安全

除了 startAsynccomplete 方法,请求和响应对象的实现都不保证线程安全。这意味着它们应该仅在请求处理线程范围内使用或应用确保线程安全的访问请求和响应对象。

如果应用使用容器管理对象创建一个线程,例如请求或响应对象,这些对象必须在其生命周期内被访问,就像定义在 3-31 页 3.12 节的“ Request 对象的生命周期”和 5.7 节的“ Response 对象的生产周期”。 请注意,除了 startAsynccomplete 方法,请求和响应对象不是线程安全的。如果这些对象需要多线程访问,需要同步这些访问或通过包装器添加线程安全语义,比如,同步化调用访问请求属性的方法,或者在线程内为响应对象使用一个局部输出流。

2.3.3.5 升级处理

在 HTTP/1.1, Upgrade 通用头( general-header)允许客户端指定其支持和希望使用的其他通信协议。如果
服务器找到合适的切换协议,那么新的协议将在之后的通信中使用。 Servlet 容器提供了 HTTP 升级机制。

不过, Servlet 容器本身不知道任何升级协议。协议处理封装在 HttpUpgradeHandler 协议处理器。在容器和
HttpUpgradeHandler 协议处理器之间通过字节流进行数据读取或写入。

当收到一个升级( upgrade)请求, servlet 可以调用 HttpServletRequest.upgrade 方法启动升级处理。 该方法实例化给定的 HttpUpgradeHandler 类,返回的 HttpUpgradeHandler 实例可以被进一步的定制。应用准备和发送一个合适的响应到客户端。退出 servlet service 方法之后, servlet 容器完成所有过滤器的处理并标记连
接已交给 HttpUpgradeHandler 协议处理器处理。然后调用 HttpUpgradeHandler 协议处理器的 init 方法,传入一个 WebConnection 以允许 HttpUpgradeHandler 协议处理器访问数据流。

Servlet 过滤器仅处理初始的 HTTP 请求和响应,然后它们将不会再参与到后续的通信中。换句话说,一旦
请求被升级,它们将不会被调用。

协议处理器( ProtocolHandler)可以使用非阻塞 IO( non blocking IO)消费和生产消息。

当处理 HTTP 升级时,开发人员负责线程安全的访问 ServletInputStreamServletOutputStream

当升级处理已经完成,将调用 HttpUpgradeHandler.destroy 方法。

2.3.4 终止服务( End of Service)

Servlet 容器没必要保持装载的 Servlet 持续任何特定的一段时间。一个 Servlet 实例可能会在 servlet 容器内
保持活跃( active)持续一段时间(以毫秒为单位), Servlet 容器的寿命可能是几天,几个月,或几年,或
者是任何之间的时间。

当 Servlet 容器确定 servlet 应该从服务中移除时,将调用 Servlet 接口的 destroy 方法以允许 Servlet 释放它使
用的任何资源和保存任何持久化的状态。例如,当想要节省内存资源或它被关闭时,容器可以做这个。

在 servlet 容器调用 destroy 方法之前,它必须让当前正在执行 service 方法的任何线程完成执行,或者超过
了服务器定义的时间限制。

一旦调用了 servlet 实例的 destroy 方法,容器无法再路由其他请求到该 servlet 实例了。如果容器需要再次
使用该 servlet,它必须用该 servlet 类的一个新的实例。在 destroy 方法完成后, servlet 容器必须释放 servlet
实例以便被垃圾回收。