Servlet是基于Java的web组件,用于生成动态内容。像其他基于Java的组件技术一样,Servlet也是基于平台无关的Java类格式,被编译为平台无关的字节码,可以被基于Java技术的web server动态加载并运行。
Servlet 容器是 web server 或 application server 的一部分,提供基于请求/响应发送模型的网络服务,解码基于 MIME 的请求,并且格式化基于 MIME 的响应。Servlet 容器也包含了管理 Servlet 生命周期。Servlet 容器可以嵌入到宿主的 web server 中,或者通过 Web Server 的本地扩展 API 单独作为附加组件安。Servelt 容器也可能内嵌或安装到包含 web 功能的 application server 中。所有 Servlet 容器必须支持基于 HTTP 协议的请求/响应模型,比如像基于 HTTPS(HTTP over SSL)协议的请求/应答模型可以选择性的支持。容器必须实现的 HTTP 协议版本包含 HTTP/1.0 和 HTTP/1.1。因为容器或许支持 RFC2616 (HTTP/1.1)描述的缓存机制,缓存机制可能在将客户端请求交给 Servlet 处理之前修改它们,也可能在将 Servlet 生成的响应发送给客户端之前修改它们,或者可能根据 RFC2616 规范直接对请求作出响应而不交给 Servlet 进行处理。Servlet 容器应该使 Servlet 执行在一个安全限制的环境中。在 Java 平台标准版(J2SE, v.1.3 或更高) 或者Java 平台企业版(Java EE, v.1.3 或更高) 的环境下,这些限制应该被放置在 Java 平台定义的安全许可架构中。比如,高端的 application server 为了保证容器的其他组件不受到负面影响可能会限制 Thread 对象的创建。Java SE 6 是构建 Servlet 容器最低的 Java 平台版本。
以下是一个典型的事件序列:
1、客户端(如 web 浏览器)发送一个 HTTP 请求到 web 服务器;
2、Web 服务器接收到请求并且交给 servlet 容器处理,servlet 容器可以运行在与宿主 web 服务器同一个进
程中,也可以是同一主机的不同进程,或者位于不同的主机的 web 服务器中,对请求进行处理。
3、servlet 容器根据 servlet 配置选择相应的 servlet,并使用代表请求和响应对象的参数进行调用。
4、servlet 通过请求对象得到远程用户,HTTP POST 参数和其他有关数据可能作为请求的一部分随请求一
起发送过来。Servlet 执行我们编写的任意的逻辑,然后动态产生响应内容发送回客户端。发送数据到客户
端是通过响应对象完成的。
5、一旦 servlet 完成请求的处理,servlet 容器必须确保响应正确的刷出,并且将控制权还给宿主 Web 服务器。
从功能上看,servlet 位于公共网关接口(CGI)程序和私有的 server 扩展如 Netscape Server API(NSAPI) 或 Apache Modules 这两者之间。相对于其他 server 扩展机制 Servlet 有如下优势:
它们通常比 CGI 脚本更快,因为采用不同的处理模型。它们采用标准的 API 从而支持更多的 Web Server。
它们拥有 Java 编程语言的所有优势,包括容易开发和平台无关。它们可以访问 Java 平台提供的大量API。
Java Servlet API 3.1 版本是 Java 平台企业版 7 版本(http://java.sun.com/javaee/)必须的 API。Servlet 容器和servlet 被部署到平台中,为了能在 Java EE 环境中执行,必须满足 JavaEE 规范中描述的额外的一些要求。
该规范之前发布的版本,监听器以随机顺序被调用。从 Servlet3.0 开始,监听器调用顺序定义在第『8 76』页的“web.xml 装配描述符,web- fragment .xml 和注解”部分。
在 servlet 2.5 中不存在 web-fragments 的概念,因此在 servlet2.5 中 metadata-complete 仅影响部署时的注解扫描。然而,在 servlet 3.0 和后来版本中,在运行时,metadata-complete 将影响扫描指定部署信息的所有注解和 web-fragments。在一个 web 应用程序中,描述符的版本不能影响你扫描哪些注解。规范的一个典型版本实现必须扫描配置中支持的所有注解,除非 metadata-complete 被指定。
Servlet 接口是 Java Servlet API 的核心抽象。所有 Servlet 类必须直接或间接的实现该接口,或者更通常做法是通过继承一个实现了该接口的类从而复用许多共性功能。目前有 GenericServlet 和 HttpServlet 这两个类实现了 Servlet 接口。大多数情况下,开发者只需要继承 HttpServlet 去实现自己的 Servlet 即可。
Servlet 基础接口定义了用于客户端请求处理的 service 方法。当有请求到达时,该方法由 servlet 容器路由到一个 servlet 实例。
Web 应用程序的并发请求处理通常需要 Web 开发人员去设计适合多线程执行的 Servlet,从而保证 service
方法能在一个特定时间点处理多线程并发执行。(注:即 Servlet 默认是线程不安全的,需要开发人员处理
多线程问题)通常 Web 容器对于并发请求将使用同一个 servlet 处理,并且在不同的线程中并发执行service 方法。
HttpServlet 抽象子类在 Servlet 接口基础之上添加了些协议相关的方法,并且这些方法能根据 HTTP 请求类型自动的由 HttpServlet 中实现的 service 方法转发到相应的协议相关的处理方法上。这些方法是:
doPut和doDelete方法允许Servlet开发人员让支持HTTP/1.1的客户端使用这些功能。HttpServlet中的doHead方法可以认为是 doGet 方法的一个特殊形式,它仅返回由 doGet 方法产生的 header 信息。doOptions 方法返回当前 servlet 支持的 HTTP 方法(译者注:通过 Allow 响应头返回支持的 HTTP 操作,如 GET、POST)。doTrace 方法返回的响应包含 TRACE 请求的所有头信息。
HttpServlet 定义了用于支持有条件 GET 操作的 getLastModified 方法。所谓的有条件 GET 操作是指客户端通过 GET 请求获取资源时,当资源自第一次获取那个时间点发生更改后才再次发生数据,否则将使用客户端缓存的数据。在一些适当的场合,实现此方法可以更有效的利用网络资源,减少不必要的数据发送。
通过注解描述的(第 8 章 注解和可插拔性)或者在 Web 应用程序的部署描述符(第 14 章 部署描述符)
中描述的 servlet 声明,控制着 servlet 容器如何提供 servlet 实例。
对于未托管在分布式环境中(默认)的 servlet 而言,servlet 容器对于每一个 Servlet 声明必须且只能产生一
个实例。不过,如果 Servlet 实现了 SingleThreadModel 接口,servlet 容器可以选择实例化多个实例以便处
理高负荷请求或者串行化请求到一个特定实例。
如果 servlet 以分布式方式进行部署,容器可以为每个虚拟机(JVM)的每个 Servlet 声明产生一个实例。但
是,如果在分布式环境中 servlet 实现了 SingleThreadModel 接口,此时容器可以为每个容器的 JVM 实例化
多个 Servlet 实例。
SingleThreadModel 接口的作用是保证一个特定servlet 实例的service 方法在一个时刻仅能被一个线程执行,一定要注意,此保证仅适用于每一个 servlet 实例,因此容器可以选择池化这些对象。有些对象可以在同一时刻被多个 servlet 实例访问,如 HttpSession 实例,可以在一个特定的时间对多个 Servlet 可用,包括那些实现了 SingleThreadModel 接口的 Servlet
Servlet 是按照一个严格定义的生命周期被管理,该生命周期规定了 Servlet 如何被加载、实例化、初始化、处理客户端请求,以及何时结束服务。该声明周期可以通过 javax.servlet.Servlet 接口中的 init、service 和destroy 这些 API 来表示,所有 Servlet 必须直接或间接的实现 GenericServlet 或 HttpServlet 抽象类。
Servlet 容器负责加载和实例化 Servlet。加载和实例化可以发生在容器启动时,或者延迟初始化直到容器决定有请求需要处理时。当 Servlet 引擎启动后,servlet 容器必须定位所需要的 Servlet 类。Servlet 容器使用普通的 Java 类加载设施加载 Servlet 类。可以从本地文件系统或远程文件系统或者其他网络服务加载。加载完 Servlet 类后,容器就可以实例化它并使用了。
一旦一个 Servlet 对象实例化完毕,容器接下来必须在处理客户端请求之前初始化该 Servlet 实例。初始化的目的是以便 Servlet 能读取持久化配置数据,初始化一些代价高的资源(比如 JDBC API 连接),或者执行一些一次性的动作。容器通过调用 Servlet 实例的 init 方法完成初始化,init 方法定义在 Servlet 接口中,并且提供一个唯一的 ServletConfig 接口实现的对象作为参数,该对象每个 Servlet 实例一个。配置对象允许 Servlet 访问由 Web 应用配置信息提供的键-值对的初始化参数。该配置对象也提供给 Servlet去访问一个ServletContext对象,ServletContext描述了Servlet的运行时环境。请参考第4章,“Servlet Context”获取 ServletContext 接口的更多信息。
在初始化阶段,servlet 实现可能抛出 UnavailableException 或 ServletException 异常。在这种情况下,Servlet不能放置到活动服务中,而且 Servlet 容器必须释放它。如果初始化没有成功,destroy 方法不应该被调用。在实例初始化失败后容器可能再实例化和初始化一个新的实例。此规则的例外是,当抛出的UnavailableException 表示一个不可用的最小时间,容器在创建和初始化一个新的 servlet 实例之前必须等待一段时间。
当一个工具加载并内省某个 Web 应用程序时触发的静态初始化,这种用法与调用 init 初始化方法是有区别的。在 Servlet 的 init 方法没被调用,开发人员不应该假定其处于活动的容器环境内。比如,当某个 Servlet仅有静态方法被调用时,不应该与数据库或企业级 JavaBean(EJB)容器建立连接。
Servlet 完成初始化后,Servlet 容器就可以使用它处理客户端请求了。客户端请求由 ServletRequest 类型的request 对象表示。Servlet 封装响应并返回给请求的客户端,该响应由 ServletResponse 类型的 response 对象表示。这两个对象(request 和 response)是由容器通过参数传递到 Servlet 接口的 service 方法的。在 HTTP 请求的场景下,容器提供的请求和响应对象具体类型分别是 HttpServletRequest 和HttpServletResponse。需要注意的是,由 Servlet 容器初始化的某个 Servlet 实例在服务期间,可以在其生命周期中不处理任何请求。
Servlet 容器可以并发的发送多个请求到 Servlet 的 service 方法。为了处理这些请求,Servlet 开发者必须为service 方法的多线程并发处理做好充足的准备。一个替代的方案是开发人员实现 SingleThreadModel 接口,由容器保证一个 service 方法在同一个时间点仅被一个请求线程调用,但是此方案是不推荐的。Servlet 容器可以通过串行化访问 Servlet 的请求,或者维护一个 Servlet 实例池完成该需求。如果 Web 应用中的 Servlet被标注为分布式的,容器应该为每一个分布式应用程序的 JVM 维护一个 Servlet 实例池。对于那些没有实现 SingleThreadModel 接口的 Servlet,但是它的 service 方法(或者是那HttpServlet 中通过 service 方法分派的 doGet、doPost 等分派方法)是通过 synchronized 关键词定义的,Servlet 容器不能使用实例池方案,并且只能使用序列化请求进行处理。强烈推荐开发人员不要去通过 service 方法(或者那些由 Service 分派的方法),因为这将严重影响性能。
Servlet 在处理一个请求时可能抛出 ServletException 或 UnavailableException 异常。ServletException 表示在
处理请求时出现了一些错误,容器应该采取适当的措施清理掉这个请求。UnavailableException 表示 servlet 目前无法处理请求,或者临时性的或者永久性的。如果 UnavailableException 表示的是一个永久性的不可用,Servlet 容器必须从服务中移除这个 Servlet,调用它的 destroy 方法,并释放 Servlet 实例。所有被容器拒绝的请求,都会返回一个 SC_NOT_FOUND (404) 响应。如果 UnavailableException 表示的是一个临时性的不可用,容器可以选择在临时不可用的这段时间内路由任何请求到 Servlet。所以在这段时间内被容器拒绝的请求,都会返回一个 SC_SERVICE_UNAVAILABLE (503)响应状态码,且同时会返回一个 Retry-After 头指示此 Servlet 什么时候可用。容器可以选择忽略永久性和临时性不可用的区别,并把UnavailableExceptions 视为永久性的,从而 Servlet 抛出 UnavailableException 后需要把它从服务中移除。
有时候,Filter 及/或 Servlet 在生成响应之前必须等待一些资源或事件以便完成请求处理。比如,Servlet 在进行生成一个响应之前可能等待一个可用的 JDBC 连接,或者一个远程 web 服务的响应,或者一个 JMS 消息,或者一个应用程序事件。在 Servlet 中等待是一个低效的操作,因为这是阻塞操作,从而白白占用一个线程或其他一些受限资源。许多线程为了等待一个缓慢的资源比如数据库经常发生阻塞,可能引起线程饥饿,且降低整个 Web 容器的服务质量。当Servlet 3.0 引入了异步处理请求的能力,使线程可以返回到容器,从而执行更多的任务。当开始异步处理请求时,另一个线程或回调可以或者产生响应,或者调用完成(complete)或请求分派(dispatch),这样,它可以在容器上下文使用 AsyncContext.dispatch 方法运行。
一个典型的异步处理事件顺序是:
请求被接收到,通过一系列如用于验证的等标准的 filter 之后被传递到 Servlet。
servlet 处理请求参数及(或)内容体从而确定请求的类型。
该 servlet 发出请求去获取一些资源或数据,例如,发送一个远程 web 服务请求或加入一个等待 JDBC 连接的队列。
servlet 不产生响应并返回。
过了一段时间后,所请求的资源变为可用,此时处理线程继续处理事件,要么在同一个线程,要么通过
AsyncContext 分派到容器中的一个资源上。
Java 企业版的功能,如第 15.2.2 节,在第 15-178 页的“Web 应用环境”和第 15.3.1 节,在第 15-180页的“EJB 调用的安全标识传播”,仅提初始化请求的线程执行,或者请求经过 AsyncContext.dispatch方法被分派到容器。Java 企业版的功能可能支持由 AsyncContext.start(Runnable)方法使用其他线程直接操作响应对象。
第八章描述的@WebServlet 和@WebFilter 注解有一个属性——asyncSupported,boolean 类型默认值为 false。当asyncSupported 设置为 true,应用通过执行 startAsync(见下文)可以启动一个单独的线程中进行异步处理,并把请求和响应的引用传递给这个线程,然后退出原始线程所在的容器。这意味着响应将遍历(相反的顺序)与进入时相同的过滤器(或过滤器链)。直到 AsyncContext 调用complete(见下文)时响应才会被提交。如果异步任务在容器启动的分派之前执行,且调用了 startAsync并返回给容器,此时应用需负责处理请求和响应对象的并发访问。
从一个 Servlet 分派时,把 asyncSupported=true 设置为 false 是允许的。这种情况下,当 servlet 的 service方法不支持异步退出时,响应将被提交,且容器负责调用 AsyncContext 的 complete,以便所有感兴趣的 AsyncListener 得到触发知。过滤器作为清理要完成的异步任务持有的资源的一种机制,也应该使用 AsyncListener. onComplete 触发的结果。从一个同步 Servlet 分派到另一个异步 Servlet 是非法的。不过与该点不同的是当应用调用 startAsyn时将抛出 IllegalStateException。这将允许 servlet 只能作为同步的或异步的 Servlet。应用在一个与初始请求所用的不同的线程中等待异步任务直到可以直接写响应,这个线程不知道任何过滤器。如果过滤器想处理新线程中的响应,那就必须在处理进入时的初始请求时包装 response,并且把包装的 response 传递给链中的下一个过滤器,并最终交给 Servlet。因此,如果响应是包装的(可能被包装多次,每一个过滤器次),并且应用处理请求并直接写响应,这将只写响应的包装对象,即任何输出的响应都会由响应的包装对象处理。当应用在一个单独的线程中读请求时,写内容到响应的包装对象,这其实是从请求的包装对象读取,并写到响应的包装对象,因此对包装对象操作的所有输入及(或)输出将继续存在。
如果应用选择这样做的话,它将可以使用 AsyncContext 从一个新线程发起到容器资源的分派请求。这将允
许在容器范围内使用像 JSP 这种内容生成技术。
除了注解属性外,我们还添加了如下方法/类:
ServletRequest
public AsyncContext getAsyncContext() – 返回由 startAsync 调用创建的或初始化的 AsyncContext。如果请求已经被设置为异步模式,调用 getAsyncContext 是非法的。
public boolean isAsyncSupported() – 如果请求支持异常处理则返回 true,否则返回 false。一旦请求传
给了过滤器或 servlet 不支持异步处理(通过指定的注解或声明),异步支持将被禁用。
public boolean isAsyncStarted() – 如果请求的异步处理已经开始将返回 true,否则返回 false。如果这个请求自从被设置为异步模式后已经使用任意一个 AsyncContext.dispatch 方法分派,或者成功调用了AsynContext.complete 方法,这个方法将返回 false。public DispatcherType getDispatcherType() – 返回请求的分派器(dispatcher)类型。容器使用请求的分派器类型来选择需要应用到请求的过滤器。只有匹配分派器类型和 url 模式(url pattern)的过滤器才会被应用。允许一个过滤器配置多个分派器类型,过滤器可以根据请求的不同分派器类型处理请求。请求的初始分派器类型定义为 DispatcherType.REQUEST 。
使用RequestDispatcher.forward(ServletRequest, ServletResponse) 或 RequestDispatcher.include(ServletRequest,ServletResponse) 分派时,它们的请求的分派器类型分别是 DispatcherType.FORWARD 或DispatcherType.INCLUDE ,当一个异步请求使用任意一个 AsyncContext.dispatch 方法分派时该请求的分派器类型是 DispatcherType.ASYNC。最后,由容器的错误处理机制分派到错误页面的分派器类型是DispatcherType.ERROR 。
AsyncContext – 该类表示在 ServletRequest 启动的异步操作执行上下文,AsyncContext 由之前描述ServletRequest.startAsync 创建并初始化。AsyncContext 的方法:
代码示例 2-1:
// 请求到 /url/A
AsyncContext ac = request.startAsync();
...
ac.dispatch(); // 异步分派到 /url/A
代码示例 2-2 :
// 请求到 /url/A
// 转发到 /url/B
request.getRequestDispatcher(“/url/B”).forward(request, response);
// 从 FORWARD 的目标内启动异步操作
AsyncContext ac = request.startAsync();
ac.dispatch(); // 异步分派到 /url/A
代码示例 2-3:
// 请求到 /url/A
// 转发到 /url/B
request.getRequestDispatcher(“/url/B”).forward(request, response);
// 从 FORWARD 的目标内启动异步操作
AsyncContext ac = request.startAsync(request, response);
ac.dispatch(); //异步分派到 /url/B
概览
1.1什么是Servlet