servlet接口是Java servlet API的核心抽象。所有的servlet都直接实现此接口或者更通常是继承一个实现了此接口的类。在Java servlet API中实现了此接口的两个类是GenericServlet和HttpServlet。在大多数情况下,开发人员将扩展HttpServlet来实现他们的servlet。
1. 请求处理方法
基本的Servlet接口定义了一个service方法来处理客户端请求。该方法针对servlet容器路由到servlet实例的每个请求进行调用。
对Web应用程序并发请求的处理通常要求Web开发人员设计可以处理在特定时间内在service方法中执行的多个线程的servlet。
通常,Web容器通过在不同的线程上并发执行service方法来处理对同一个servlet的并发请求。
1.1 HTTP特定请求的处理方法
HttpServlet抽象子类将额外的方法添加到基本的Servlet接口之外,这是由HttpServlet类中的service方法自动调用的,以帮助处理基于http的请求。这些方法有:
- doGet,处理HTTP的GET请求
- doPost,处理POST请求
- duPut,处理PUT请求
- doDelete,处理DELETE请求
- doHead,处理HEAD请求
- doOptions,处理OPTIONS请求
- doTrace,处理TRACE请求
通常情况下开发基于HTTP的servlet时,一个servlet开发者将只关注doGet和doPost方法。其他方法被认为是非常熟悉HTTP编程的程序员使用的方法。
1.2 其他方法
doPut和doDelete方法允许Servlet开发人员支持使用这些特性的HTTP/1.1客户端。HttpServlet中的doHead方法是doGet方法的一种特殊形式,它只返回doGet方法生成的头。doOptions方法响应servlet支持的HTTP方法。doTrace方法生成一个响应,该响应包含跟踪请求中发送的所有消息头的实例。
CONNECT方法不受支持,因为它适用于代理,而Servlet API针对端点。
1.3 条件GET支持
HttpServlet接口定义getLastModified方法来支持条件GET操作。一个条件GET操作请求一个资源只有在指定的时间后被修改才会被发送。在适当的情况下,这种方法的实施可能有助于有效利用网络资源。
2. 实例数量
通过注解描述的(第 8 章 注解和可插拔性)或者在 Web 应用程序的部署描述符(第 14 章 部署描述符)
中描述的 servlet 声明,控制着 servlet 容器如何提供 servlet 实例。
对于不驻留在分布式环境中的servlet(默认),servlet容器必须仅使用一个servlet声明的实例。然而,对于实现SingleThreadModel接口的servlet, servlet容器可以实例化多个实例来处理大量请求负载并将请求序列化到特定实例。
In the case where a servlet was deployed as part of an application marked in the deployment descriptor as distributable, a container may have only one instance per servlet declaration per Java Virtual Machine (JVM™)1. However, if the servlet in a distributable application implements the SingleThreadModel interface, the container may instantiate multiple instances of that servlet in each JVM of the container.
如果servlet被部署的应用程序在部署描述符中标记为分布式,一个容器可能只有每个JVM每个servlet声明一个实例。但是,如果分布式应用程序中的servlet实现了SingleThreadModel接口,那么容器可以在容器的每个JVM中实例化该servlet的多个实例。
2.1 Single Thread Model(单线程模型)的注意事项
使用SingleThreadModel接口保证在给定的servlet实例的service
方法中,每次只执行一个线程(意思是此时的service方法是同步方法吗,Single Thread Model接口保证不会有多个请求线程同时访问单个servlet实例?)。需要注意的是,这个保证只适用于每个servlet实例,因此容器可以选择将这些对象池化(It is important to note that this guarantee only applies to each servlet instance, since the container may choose to pool such objects.)。有可以同时被多个servlet实例访问的对象,例如HttpSession的实例,可以在任何特定时间用于多个servlet,包括实现SingleThreadModel的servlet。
建议开发人员采用其他方法来解决这些问题,而不是实现该接口,例如避免使用实例变量或同步代码块访问这些资源。在这个版本的规范中,单线程模型接口被弃用了。
3 servlet生命周期
servlet是通过一个定义良好的生命周期来管理的,它定义了如何加载和实例化、初始化、处理来自客户机的请求,以及被取消服务。这个生命周期是在API中通过javax.servlet的init、service和destory 的方法来表示的。所有Servlet必须直接或间接实现GenericServlet或HttpServlet抽象类。
3.1 加载和实例化
servlet容器负责加载和实例化servlet。加载和实例化可以发生在容器启动时,或者延迟到容器内的servlet需要处理一个请求时。在启动servlet引擎时,必须通过servlet容器找到所需的servlet类。servlet容器使用普通的Java类装载工具加载servlet类。加载可能来自本地文件系统、远程文件系统或其他网络服务。
加载完Servlet类之后,容器实例化servlet以使用。
3.2 初始化
在servlet对象实例化之后,容器必须在它可以处理来自客户端的请求之前初始化servlet。初始化提供了这样一个servlet可以阅读持久化配置数据,初始化代价高昂的资源(如JDBC™APIbased连接),并执行其他一次性活动。容器通过调用Servlet接口的init方法使用一个唯一的(每个servlet声明)的对象来初始化servlet,这个对象实现了ServletConfig接口。
这个配置对象允许servlet从Web应用程序的配置信息中访问名称-值初始化参数。配置对象还允许servlet访问描述servlet运行时环境的对象(实现ServletContext接口)。有关ServletContext接口的更多信息,请参见第4章“Servlet上下文”。
3.2.1 初始化的错误情况
在初始化过程中,servlet试了可以抛出一个 UnavailableException或ServletException。在这种情况下,次servlet禁止被放进一个活动的服务中并且必须被servlet容器释放。在不成功初始化的情况下,destory方法不会被调用。
在初始化失败后,容器可能实例化并初始化一个新实例。这个规则的例外是,当一个UnavailableException指示一个不可用的最小时间时,容器必须等待这段时间才能创建和初始化一个新的servlet实例。
3.2.2 工具注意事项
当工具加载和内省Web应用程序时,静态初始化方法的触发将与init方法的调用区别开来。在调用servlet接口的init方法之前,开发人员不应该假定servlet处于活动容器运行时。举个例子,一个servlet不应在只有静态(类)初始化方法被调用后尝试建立连接数据库或Enterprise javabean™容器。
3.3 请求的处理
servlet被正确初始化之后,servlet容器可以使用它来处理客户端请求。请求由类型ServletRequest的请求对象表示。servlet通过调用ServletResponse类型提供的对象的方法来填充对请求的响应。这些对象作为参数传递给Servlet接口的service方法。
在一个HTTP请求的情况,容器提供的对象类型为HttpServletRequest和HttpServletResponse。
需要注意的是,被servlet容器放置到服务中的一个servlet实例,有可能在其生命周期中不会处理任何请求。
3.3.1 多线程问题
servlet容器可以通过servlet的service方法发送并发请求。为了处理请求,Servlet开发人员必须为在service方法中使用多个线程并发处理提供足够的规定。
尽管不推荐,但是开发人员的另一种选择是实现SingleThreadModel接口,该接口要求容器确保在service方法中每次只有一个请求线程。servlet容器可以通过在servlet上序列化请求,或者通过维护一个servlet实例池来满足这一需求。如果servlet是被标记为可分发的Web应用程序的一部分,那么容器可以在每个JVM中维护应用程序分布的一个servlet实例池。
对于servlet不实现SingleThreadModel接口的情况,如果service方法(或是被HttpServlet抽象类的service方法分发的如doGet方法或doPost)定义了synchronized的关键字,servlet容器不能使用实例池的方法,并且只能使用序列化请求进行处理。强烈建议开发人员不要在这些情况下同步service方法(或其分发的方法),因为这会对性能造成不利影响。
3.3.2 请求处理时的异常
servlet可以在请求的服务期间抛出ServletException或UnavailableException。一个ServletException信号表明在处理请求过程中发生了一些错误,并且容器应该采取适当的措施来清理请求。
一个UnavailableException信号,servlet无法处理请求,或者暂时或永久地处理请求。
如果一个永久的不可用性是由UnavailableException指示的,那么servlet容器必须调用它的destroy方法从服务中删除servlet,并释放servlet实例。任何由容器拒绝的请求都必须返回一个SC_NOT_FOUND(404)响应。
如果临时的不可用性是由UnavailableException指示的,那么容器可以选择在临时不可用的时间段内不通过servlet路由任何请求。在此期间,容器所拒绝的任何请求都必须返回SC_SERVICE_UNAVAILABLE(503)响应状态,以及Retry-After的消息头,指示不可用性何时终止。
容器可以选择忽略永久性和临时不可用性的区别,并将所有不可用的异常视为永久的,从而删除从服务中抛出UnavailableException的servlet。
3.3.3 异步处理
有些时候,过滤器和/或servlet在生成响应之前没有等待一个资源或事件而无法完成请求的处理。例如,一个servlet可能需要等待一个可用的JDBC连接、一个远程web服务的响应、一个JMS消息,或者一个应用程序事件,然后才开始生成响应。在servlet内等待是一种低效的操作,因为它是一个阻塞操作,它消耗线程和其他有限的资源。通常,像数据库这样的慢资源可能有许多线程阻塞等待访问,并可能导致线程饥饿和整个web容器的服务质量差。
引入请求的异步处理,允许线程返回到容器并执行其他任务。当异步处理从请求开始时,另一个线程或回调可能会生成响应并调用complete或dispatch请求,以便它可以使用AsyncContext.dispatch方法在容器的上下文中运行。异步处理的典型事件序列是:
1.请求被接收,并通过普通过滤器传递给servlet。
2.servlet处理请求参数和/或内容,以确定请求的性质。
3.servlet向资源或数据发出请求,例如发送远程web服务请求或连接等待JDBC连接的队列。
4.servlet返回而不产生响应。
5.经过一段时间之后,请求的资源变得可用,处理该事件的线程在同一个线程中继续处理,或者使用AsyncContext分派到容器中的资源。
Java 企业版的功能,如第 15.2.2 节,在第 15-178 页的“Web 应用环境”和第 15.3.1 节,在第 15-180页的“EJB 调用的安全标识传播”,仅提初始化请求的线程执行,或者请求经过 AsyncContext.dispatch方法被分派到容器。 Java 企业版的功能可能支持由 AsyncContext.start(Runnable)方法使用其他线程直接操作响应对象。
在第8章中描述的@WebServlet和@WebFilter注释有一个属性- asyncSupported,它是一个具有默认值false的布尔值。当将asyncsupport设置为true时,应用程序可以通过调用startAsync(参见下文)在一个单独的线程中启动异步处理,将它传递给请求和响应对象,然后从原始线程的容器中退出。这意味着响应将遍历(相反的顺序)与进入时相同的过滤器(或过滤器链)。直到 AsyncContext 调用complete (见下文)时响应才会被提交。如果异步任务在容器启动的分派之前执行,且调用了 startAsync并返回给容器,此时应用需负责处理请求和响应对象的并发访问。
从一个 Servlet 分派时,把 asyncSupported=true 设置为 false 是允许的。这种情况下,当 servlet 的 service方法不支持异步退出时,响应将被提交,且容器负责调用 AsyncContext 的 complete,以便所有感兴趣的 AsyncListener 得到触发知。过滤器作为清理要完成的异步任务持有的资源的一种机制,也应该使用 AsyncListener. onComplete 触发的结果。
从一个同步 Servlet 分派到另一个异步 Servlet 是非法的。不过与该点不同的是当应用调用 startAsync时将抛出 IllegalStateException。这将允许 servlet 只能作为同步的或异步的 Servlet。
应用在一个与初始请求所用的不同的线程中等待异步任务直到可以直接写响应,这个线程不知道任何过滤器。如果过滤器想处理新线程中的响应,那就必须在处理进入时的初始请求时包装 response,并且把包装的 response 传递给链中的下一个过滤器,并最终交给 Servlet。因此,如果响应是包装的(可能被包装多次,每一个过滤器一次),并且应用处理请求并直接写响应,这将只写响应的包装对象,即任何输出的响应都会由响应的包装对象处理。当应用在一个单独的线程中读请求时,写内容到响应的包装对象,这其实是从请求的包装对象读取,并写到响应的包装对象,因此对包装对象操作的所有输入及(或)输出将继续存在。
如果应用程序选择这样做,则可以使用AsyncContext将请求从新线程发送到容器中的资源。这将允许在容器的范围内使用像JSP这样的内容生成技术。
除了注释属性之外,我们还添加了以下方法/类:
ServletRequest
API省略。
一篇servlet异步处理的示例
3.3.4 线程安全
除了startAsync和complete方法之外,请求和响应对象的实现不能保证是线程安全的。这意味着它们只能在请求处理线程的范围内使用,或者应用程序必须确保对请求和响应对象的访问是线程安全的。
如果应用程序创建的一个线程使用容器管理的对象,如请求或响应对象,这些对象必须只在3.13节中定义的对象的生命周期被访问。请注意,除了startAsync和complete的方法之外,请求和响应对象不是线程安全的。如果这些对象是在多个线程中访问的,那么访问应该是同步的,或者通过包装器来添加线程安全,例如,同步调用方法的调用来访问请求属性,或者在线程中使用本地输出流作为响应对象。
3.3.5 升级处理
在HTTP/1.1中,升级的通用头允许客户机指定它支持的附加通信协议并愿意使用。如果服务器发现它适合切换协议,那么新的协议将在随后的通信中使用。
servlet容器提供了一个HTTP升级机制。然而,servlet容器本身并不了解升级的协议。协议处理封装在HttpUpgradeHandler中。servlet容器和HttpUpgradeHandler之间的数据是在字节流中读取或写入的。
当收到升级请求时,servlet可以调用HttpServletRequest.upgrade方法,启动升级过程。该方法实例化给定的HttpUpgradeHandler类。返回的HttpUpgradeHandler实例可以进一步定制。应用程序准备并向客户机发送适当的响应。退出servlet的service方法之后,servlet容器完成了所有过滤器的处理,并标记了由HttpUpgradeHandler处理的连接。然后它调用HttpUpgradeHandler的init方法,通过一个WebConnection来允许协议处理程序访问数据流。
servlet过滤器只处理初始HTTP请求和响应。他们没有参与后续的交流。换句话说,当请求升级后,它们不会被调用。
HttpUpgradeHandler可以使用非阻塞IO来消费和生成消息。
开发人员负责在处理HTTP升级时线程安全访问ServletInputStream和ServletOutputStream。
当升级处理完成,HttpUpgradeHandler.destroy将被调用。
3.4 服务的终止
servlet容器不需要在任何特定的时间内保存一个servlet。servlet实例可以在servlet容器中保持活跃,以毫秒为单位,以实现servlet容器的生命周期(可能是若干天、几个月或几年),或者在两者之间的任何时间。
当servlet容器确定应该从服务中删除servlet时,它调用servlet接口的destroy方法,以允许servlet释放它所使用的任何资源,并保存任何持久状态。例如,当容器想要节省内存资源时,或者当它被关闭时,它可能这样做。
在servlet容器调用destroy方法之前,它必须允许当前运行在servlet服务方法中的任何线程完成执行,或者超过一个服务器定义的时间限制。
一旦在servlet实例上调用了destroy方法,容器可能不会将其他请求路由到该servlet实例。如果容器需要再次启用servlet,它必须使用servlet类的新实例来实现。
destory方法完成后,servlet容器必须释放servlet实例,这样它才有资格进行垃圾收集。