Servlet 本章要点: ? Servlet的工作原理 ? Servlet的基本编程技能,包括请求处理、响应生成和参数配置等 ? Servlet的高级编程技能,包括会话管理、上下文和Servlet间协作 ? Servlet Filter的工作原理和编程方法 ? Servlet Listener的工作原理和编程方法      本章首先讲解Servlet的定义和工作原理,随后通过示例讲解Servlet编程基础包括请求处理、响应生成和参数配置等;在此基础上,对会话管理、Servlet上下文、Servlet间协作等高级编程技巧进行深入讲解;最后介绍Servlet编程中的两类高级功能组件Filter和Listener。 3.1 Web应用模型   Java EE企业应用最常见的场景就是处理Web请求并生成动态响应。因此Java EE学习之旅的第一站自然从Java EE的Web组件Servlet开始。不过在学习Servlet 编程之前,开发人员应该首先了解Web应用是如何工作的。   所谓Web应用,指的是可通过Web访问的应用程序,如门户网站等。区别于在计算机本地运行的桌面应用如Word、Excel等,Web应用由客户端和服务器两部分组成,二者通过HTTP协议进行交互,如图3-1所示。 图3-1 Web应用模型   HTTP是Web应用最常用的协议。最广泛使用的HTTP版本是1.1,它工作在请求响应模式下,一次请求处理流程包含如下四个步骤。      (1)客户端向服务器发送一个请求,请求头部包含请求的方法、URI、协议版本,以及包含请求修饰符、客户端信息和内容的类似MIME的消息结果。   (2)服务器接收到请求信息后建立与客户端的连接。   (3)服务器对客户端提交的请求信息进行处理,并最终返回一个响应,内容包括消息协议的版本、成功或失败编码加上包含服务器信息、实体元信息以及其他内容。   (4)服务器断开与客户端的连接。   如果客户端需要再次向服务器请求信息,则进入如上所示新一轮的处理流程。   对于HTTP 1.1协议有以下两个特性开发人员必须要牢记:   (1)HTTP协议是无状态的。服务器并不会记录和保存客户端的任何信息。也就是说,同一用户在第二次访问同一服务器上的页面时,服务器的响应过程与第一次被访问时相同。至于服务器如何处理来自同一客户端的请求,将在3.7节进行深入讲解。   (2)HTTP是无连接的。服务器并不会保持与客户端的永久性连接。服务器只是在收到客户端的请求后才会与客户端建立起连接,一旦服务器生成响应并返回客户端,服务器就将断开与客户端的连接。如果客户端需要请求服务器上另外一个资源,则需要重新建立与服务器的连接。 3.2 Servlet基础 3.2.1 Servlet定义   Servlet是服务器端的Java应用程序,它用来扩展服务器的功能,可以生成动态的Web页面。Servlet与传统Java应用程序最大的不同在于:它不是从命令行启动的,而是由包含Java虚拟机的Web服务器进行加载。   Applet是运行于客户端浏览器的Java应用程序,Servlet与Applet相比较,有以下特点。   1.相似之处   (1)它们不是独立的应用程序,没有main方法。   (2)它们不是由用户调用,而是由另外一个应用程序(容器)调用。   (3)它们都有一个生命周期,包含init和destroy方法。   2.不同之处   (1)Applet运行在客户端,具有丰富的图形界面。   (2)Servlet运行在服务器端,没有图形界面。   造成这种差别的原因在于它们所肩负的使命不同。Applet目的是为了实现浏览器与客户的强大交互,因此需要丰富多样的图形交互界面;Servlet用于扩展服务器端的功能,实现复杂的业务逻辑,它不直接同客户交互,因此不需要图形界面。   Servlet 最大的用途是通过动态响应客户端请求来扩展服务器功能。 3.2.2 Servlet工作流程   Servlet运行在Web服务器上的Web容器里。Web容器负责管理Servlet。它装入并初始化Servlet,管理Servlet的多个实例,并充当请求调度器,将客户端的请求传递到Servlet,并将Servlet的响应返回给客户端。Web容器在 Servlet 的使用期限结束时终结该 Servlet。服务器关闭时,Web容器会从内存中卸载和除去 Servlet。   Servlet的基本工作流程如下:   (1)客户端将请求发送到服务器。   (2)服务器上的Web容器实例化(装入)Servlet,并为Servlet进程创建线程。请注意,Servlet是在出现第一个请求时装入的,在服务器关闭之前不会卸载它。      注意:Servlet也可以配置为Web应用程序启动时自动装载。关于如何配置Servlet将在3.6节详细讲解。      (3)Web容器将请求信息发送到Servlet。   (4)Servlet 创建一个响应,并将其返回到Web容器。Servlet使用客户端请求中的信息以及服务器可以访问的其他信息资源(如资源文件和数据库等)来动态构造响应。   (5)Web容器将响应返回客户端。   (6)服务器关闭或Servlet空闲时间超过一定限度时,调用destroy方法退出。   从上面Servlet的工作基本流程可以看出,客户端与Servlet间没有直接的交互。无论是客户端对Servlet的请求还是Servlet对客户端的响应,都是通过Web容器来实现的,这就大大提高了Servlet组件的可移植性。   下面对Servlet的工作基本流程进行详细说明。   1.Servlet装入和初始化   第一次请求Servlet时,服务器将动态装入并实例化 Servlet。开发人员可以通过Web配置文件将Servlet配置成在Web服务器初始化时直接装入和实例化。Servlet 调用 init方法执行初始化。init方法只在 Servlet创建时被调用,所以,它常被用来作为一次性初始化的工作,如装入初始化参数或获取数据库连接。   init方法有两个版本:一个没有参数,一个以 ServletConfig 对象作为参数。   2.调用Servlet   每个Servlet都对应一个URL地址。Servlet 可以作为显式 URL 引用调用,或者嵌入在HTML中并从Web应用程序调用。   Servlet和其他资源文件(如JSP文件、静态HTML文本等)打包作为一个Web应用存放在Web服务器上。对于每个Web应用,都可以存在一个配置文件web.xml。关于Servlet的名称、对应的Java类文件、URL地址映射等信息都存放在配置文件web.xml中。当Web服务器接收到对URL地址的请求信息时,会根据配置文件中URL地址与Servlet之间的映射关系将请求转发到指定的Servlet来处理。      说明:自Java EE 6版本以来,Java EE规范推荐使用注解来配置Web组件,而不是使用配置文件web.xml。注解是内嵌在Java代码中的一种特殊标记,关于注解的使用本书后面的示例中会反复讲到。因此,在Java EE 6版本以上的 Web应用中,也允许没有配置文件web.xml存在。      3.处理请求   当Web容器接收到对 Servlet 的请求,Web容器会产生一个新的线程来调用Servlet的service方法。service方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),然后相应地调用Servlet组件的 doGet、doPost、doPut、doDelete等方法。如果Servlet 处理各种请求的方式相同,也可以尝试覆盖service方法。GET请求类型与POST请求类型的区别在于:如果以GET方式发送请求,所带参数附加在请求URL后直接传给服务器,并可从服务器端的QUERY_STRING这个环境变量中读取;如果以POST方式发送请求,则参数会被打包在数据包中传送给服务器。   4.多个请求的处理   Servlet由Web容器装入,一个Servlet同一时刻只有一个实例,并且它在 Servlet 的使用期间将一直保留。当同时有多个请求发送到同一个Servlet时,服务器将会为每个请求创建一个新的线程来处理客户端的请求。   如图3-2所示,有两个客户端浏览器同时请求同一个 Servlet服务,服务器会根据Servlet实例对象为每个请求创建一个处理线程。每个线程都可以访问Servlet装入时的初始化变量。每个线程处理它自己的请求。Web容器将不同的响应返回各自的客户端。 图3-2 Servlet对多个请求的处理   上述说明意味着Servlet的doGet方法和doPost方法必须注意共享数据和领域的同步访问问题,因为多个线程可能会同时尝试访问同一块数据或代码。如果想避免多线程的并发访问,可以设置Servlet实现SingleThreadModel接口,如下所示:    public class YourServlet extends HttpServlet implements SingleThreadModel { ... }      注意:使用SingleThreadModel接口虽然避免了多请求条件下的线程同步问题,但是单线程模式将对应用的性能造成重大影响,因此在使用时要特别慎重。      5.退出   如果Web应用程序关闭或者Servlet已经空闲了很长时间,Web 容器会将Servlet实例从内存移除。移除之前Web 容器会调用Servlet的destroy方法。Servlet可以使用这个方法关闭数据库连接、中断后台线程、向磁盘写入Cookie列表及执行其他清理动作。      注意:当Web容器出现意外而被关闭,则不能够保证destroy方法被调用。      通过上面Servlet工作流程的基本描述,对于Web容器的职责,可以归纳为以下两点:一是管理Servlet组件的生命周期,包括Servlet组件的初始化、销毁等;二是作为客户端与Servlet之间的中介,负责封装客户端对Servlet的请求,并将请求映射到对应的Servlet,以及将Servlet产生的响应返回给客户端。 3.2.3 Servlet编程接口   Java EE标准定义了Java Servlet API,用于规范Web容器和Servlet 组件之间的标准接口。Java Servlet API是一组接口和类,主要由两个包组成:javax.servlet 包含了支持协议无关的 Servlet 的类和接口;javax.servlet.http 包括了对 HTTP 协议的特别支持的类和接口。如果希望详细了解Java Servlet API,可访问http://www.oracle.com/technetwork/java/ index-jsp-135475.html下载Java Servlet API的详细文档。   所有的Servlet都必须实现通用Servlet 接口或HttpServlet 接口。通用Servlet 接口类javax.servlet.GenericServlet定义了管理 Servlet 及它与客户端通信的方法;HttpServlet 接口类javax.servlet.http.HttpServlet是继承了通用Servlet 接口类的一个抽象子类。要编写在Web上使用的 HTTP协议下的Servlet,通常采用继承 HttpServlet 接口的形式。下面以HttpServlet 接口为中心,介绍与Servlet编程密切相关的几个接口,如图3-3所示。 图3-3 Servlet编程相关接口示意图 * HttpServletRequest代表发送到HttpServlet 的请求。这个接口封装了从客户端到服务器的通信。它可以包含关于客户端环境的信息和任何要从客户端发送到Servlet的数据。 * HttpServletResponse代表从HttpServlet返回客户端的响应。它通常是根据请求和 Servlet 访问的其他来源中的数据动态创建生成的响应,如HTML页面。 * ServletConfig代表Servlet的配置信息。Servlet在发布到服务器上的时候,在Web应用配置文件中对应一段配置信息。Servlet根据配置信息进行初始化。配置信息的好处在于在Servlet发布时可以通过配置信息灵活地调整Servlet而不需要重新改动、编译代码。 * ServletContext代表Servlet的运行环境信息。Servlet是运行在服务器上的程序。为了与服务器及服务器上运行的其他程序进行交互,有必要获得服务器的环境信息。 * ServletException代表Servlet运行过程中抛出的意外对象。 * HttpSession用来在无状态的HTTP协议下跨越多个请求页面来维持状态和识别用户。维护HttpSession的方法有Cookie或URL重写。 * RequestDispatcher:请求转发器,可以将客户端请求从一个Servlet转发到其他的服务器资源,如其他Servlet、静态HTML页面等。   Java EE服务器必须声明支持的Java Servlet API的版本级别。随着Java EE技术的不 断进步,Java Servlet API的版本也在不断更新,在Java EE 8标准规范中包含的Java Servlet API的版本为4.0。 3.3 第一个Servlet   在了解了Servlet的基础知识后,现在开始编写第一个Servlet组件。   编写响应HTTP请求的Servlet只需要两步:   (1)创建一个扩展了javax.servlet.http.HttpServlet接口的类。javax.servlet.http.HttpServlet接口是javax.servlet.GenericServlet的扩展接口,它包含了分析HTTP 请求Header和将客户端信息打包到javax.servlet.http.HttpServletRequest类中的相关代码。   (2)重写Servlet组件的doGet或doPost方法实现对HTTP请求信息的动态响应。这些方法是Servlet实际完成工作的地方。HTTP 1.1支持七种请求方法: GET、POST、 HEAD、 OPTIONS、PUT、DELETE和TRACE。GET和POST是Web应用程序中最常用的两个方法。根据请求是通过GET还是POST发送,覆盖doGet、doPost方法之一或全部。doGet和doPost方法都有两个参数,分别为HttpServletRequest 接口和HttpServletResponse接口。HttpServletRequest提供访问有关客户端请求信息的方法,包括表单数据、请求Header等。HttpServletResponse除了提供用于指定HTTP应答状态(200、404等)、应答头部信息(Content-Type、Set-Cookie等)的方法之外,最重要的是它提供了一个用于向客户端发送数据的输出流对象。这个输出流对象可以是字节流或二进制数据流。对于Servlet开发来说,它的大部分工作是操作此输出流并返回给客户端。      提示:doGet和doPost这两个方法是由service方法调用的,有时可能需要直接覆盖service方法,例如Servlet要对GET和POST两种请求采用同样的处理方式。但不推荐那样做。      Servlet也可以重写init和destroy方法以实现Servlet定制化的初始化和析构。重写init和destroy方法的典型场景是在init方法中建立数据库连接并在destroy方法中断开它。   下面开始创建Servlet。Servlet作为一个Web组件,必须包含在某个Web应用程序中,因此,首先创建Web应用程序Chapter3。      注:本章中所有的示例都包含在此Web应用程序中。      打开NetBeans开发环境,单击“文件”菜单的“新建项目”选项,弹出如图3-4所示的“新建项目”对话框。 图3-4 创建Web应用项目Chapter3   在“类别”列表框中选中Java Web选项,在“项目”列表框中选中“Web应用程序”。单击对话框底部的“下一步”按钮,进入下一页面,如图3-5所示。 图3-5 设置Web应用项目名称和位置   在“项目名称”文本框输入Chapter3。单击“项目位置”右侧的“浏览”按钮可选择项目的位置。选中“设置为主项目”复选框将当前项目设置为主项目。单击底部的“下一步”按钮,进入下一页面,如图3-6所示。 图3-6 设置Web应用服务器   Web应用程序必须发布到Java EE Web服务器上才能够运行。在这里从“服务器”下拉列表框中选择NetBeans内置的服务器“GlassFish Server 3.1.1”,默认其他选项设置,单击“完成”按钮,则Web应用程序创建完毕。   下面为Web应用创建一个Servlet。在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→Servlet命令,弹出如图3-7所示对话框。 图3-7 “新建Servlet”对话框   在“类名”文本框中输入Servlet实现类的名称First,在“包”文本框中输入Servlet实现类所在的包名com.servlet,单击“下一步”按钮,进入下一页面,如图3-8所示。 图3-8 配置Servlet 部署信息   这一步主要完成Servlet组件的部署配置,主要工作是设置Servlet的名称以及对应的URL模式名称。所谓URL模式,就是代表客户端请求的一个字符串,Web容器总是将匹配此字符串内容的请求转发到此Servlet组件来处理以便返回动态响应。“Servlet名称”文本框中的内容为Servlet的显示名称,并不要求等于前面定义的类名。在“Servlet名称”文本框中输入First。在“URL模式”文本框输入Servlet所对应的请求URL模式“/First”。选中“将信息添加到部署描述符(web.xml)”复选框,单击“完成”按钮,则一个名为First的Servlet组件创建完毕。NetBeans将在编辑器中自动打开Servlet的源代码。   在这个Servlet中,只要求Servlet在接收到请求后向客户端返回“Hello, World!”的提示信息。完整代码如程序3-1所示。      说明:为节省篇幅,代码中的一些注释信息被省略,完整的代码请到清华大学出版社的网站下载。另外,有些注释信息是NetBeans自动生成,由于机器环境的不同,读者所生成的注释信息可能与本书配套资源中的不完全一致,如 create date、author 信息等,这完全正常。      程序3-1:First.java    package com.servlet; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; public class First extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { out.println(""); out.println(""); out.println("Servlet First"); out.println(""); out.println(""); out.println("

Hello World!

"); out.println(""); out.println(""); } finally { out.close(); } } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } public String getServletInfo() { return "Short description"; } }      程序说明:NetBeans自动生成了Servlet的框架代码,其中方法doGet和doPost分别用来响应客户端发出的GET和POST请求,开发人员可以分别编写代码覆盖上述两个方法以实现对GET和POST请求的处理,在这里它们都默认调用方法processRequest(),这是NetBeans推荐的一种编程实践。方法processRequest有两个输入参数:request是代表客户端发出的请求信息的HttpServletRequest接口对象;response是代表Servlet返回客户端的响应的HttpServletResponse接口对象。在方法processRequest中,首先调用response方法setContentType("text/html;charset=UTF-8") 设置响应返回类型为HTML文件,编码类型为UTF-8,然后调用response的getWriter方法获取响应对应的PrintWriter对象,最后利用PrintWriter对象的out方法在客户端打印信息“Hello World!”。      注意:方法doGet、doPost的声明中必须包含抛出两个异常(ServletException、IOException)。      为访问编写的Servlet,必须将其打包并发布到Java EE 服务器上,然后启动服务器来访问Servlet。在前面创建Web 应用程序的时候已经设置Java EE Web服务器为NetBeans内置的服务器GlassFish Server 5。   在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“生成项目”命令,则Web应用被自动打包。   重新选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“部署项目”命令,则Web应用被部署到Web服务器。再一次选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“运行项目”命令,则Web服务器被启动且Web应用被加载运行。      说明:在执行“运行项目”的操作时,如果Web服务器已经处于运行状态,则服务器只执行加载Web应用的操作。      打开IE浏览器,在地址栏中输入http://localhost:8080/Chapter3 /First,程序运行结果如图3-9所示。 图3-9 Servlet运行结果页面   读到这里可能会产生疑问:Web服务器是如何将浏览器中输入的地址http://localhost: 8080/Chapter3/First自动映射到Servlet组件First的呢?   这个秘密在于:每个Web应用程序都对应一个称为上下文信息的字符串,表示此Web应用所对应的URL请求地址。在“项目”视图选中Web项目Chapter3,右击,在弹出的快捷菜单中选中“属性”命令,弹出如图3-10所示对话框。在左侧的“类别”栏目中选中“运行”,则在右侧“上下文路径”文本框中可查看并修改Web应用上下文信息。 图3-10 查看Web应用上下文路径   当Web应用程序部署到服务器上时,服务器根据此信息便知道将上下文信息为“/Chapter3”请求映射到此Web应用。   另外,为描述Web应用内部信息,每个Web应用还通常包含一个配置文件web.xml来对自身包含的Web组件的信息进行说明。Servlet添加到Web应用后,在Web应用的配置文件web.xml中包含Servlet及其URL映射信息。Java EE 服务器正是根据web.xml中的配置信息将客户端的请求转发给Web应用中适当的Web组件。   web.xml的详细内容如程序3-2所示,其中的节点指明Servlet名称与Servlet实现类之间的对应关系。节点指明Servlet名称与请求URL之间的对应关系。再回头看在浏览器地址栏中输入的请求地址。地址最前面的部分“http://localhost: 8080”将请求导向本机安装的Java EE 服务器GlassFish Server 5。其中localhost代表本机,8080代表Java EE 服务器程序的端口号。那么对于请求地址剩余信息的解析就由Java EE 服务器来接管。Java EE服务器根据请求地址中的“/Chapter3”和服务器上Web应用的上下文信息确定请求由Web应用Chapter3 处理响应。Java EE 服务器在Chapter3 Web应用的配置文件web.xml中查找请求地址中的“/First”对应的Servlet映射信息,最终确定请求由名为First的Servlet处理响应,此Servlet对应的类文件就是刚才编写的com.servlet.First。   程序3-2:web.xml    First com.servlet.First First /First 30      web.xml对于一个Web应用是如此的重要,因此在这里不得不对它多说几句。web.xml的作用就是作为Web容器与Web应用交互的场所,它一定位于应用的WEB-INF子目录下。它包含了Web应用的重要的描述信息,以本例来说,节点用来指明Servlet逻辑名称与Java 实现类之间的对应关系;用来指明Servlet逻辑名称与URL模式之间的对应关系。当然还有其他节点用来描述Web应用其他方面的信息,详细信息可以查阅DTD(Document Type Definitions,文档类型定义)文档。Web容器正是根据web.xml文件描述的信息来操作Web应用的。   可以将程序3-2中的代码片段:    First /First    改为    First /FirstServlet      将程序3-2保存,重新发布Web应用并启动浏览器,在地址栏中输入http://localhost: 8080/Chapter3/First,则将得到如图3-11所示的运行结果页面。   404错误代码表示文件无法定位的错误类型。产生错误的原因在于此时Servlet组件First对应的请求URL不再是“/First”,而是“/FirstServlet”。在地址栏中输入http://localhost:8080 / Chapter3 /FirstServlet,看看又会得到什么运行结果页面。   值得一提的是,自Java EE 5 规范以后,推荐使用注解来代替编写复杂的配置文件。下面修改程序3-1,代码如程序3-3所示。 图3-11 修改Web应用配置后的运行结果   注:为节省篇幅,书中代码主要显示编程实践中的重点内容,完整代码请参考本书的源代码包。      程序3-3:First.java    package com.servlet; … @WebServlet(name="First", urlPatterns={"/First"}) public class First extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { out.println(""); out.println(""); out.println("Servlet First"); out.println(""); out.println(""); out.println("

Hello World!

"); out.println(""); out.println(""); out.close(); } finally { out.close(); } } … }      程序说明:与程序3-1相比,在类的定义前添加了一个注解@WebServlet,它包含两个属性name和urlPatterns,分别用来定义Servlet组件的名称和URL模式。当部署此组件时,Web容器将根据此注解自动完成对此Servlet组件的配置。还要注意的是,因为使用了注解@WebServlet,因此要在代码中添加对javax.servlet.annotation.WebServlet的引用。   在“项目”视图的“配置文件”目录下删除掉web.xml,重新发布应用,打开浏览器重新输入请求地址http://localhost:8080/Chapter3/First,看看会不会得到如图3-9所示的运行结果。      说明:由于新的Java EE规范推荐采用注解的方式,因此在以后的示例中,本书将尽可能采用注解的方式来部署Web组件。但配置文件web.xml在有些情况下还是必需的,如设置Web应用的安全属性等,因此,注解并不能完全取代web.xml,它只是使得web.xml更加简洁。      如果Web组件中既采用了注解来配置组件,在配置文件web.xml中又包含了此组件配置信息,那么Web容器在进行URL解析映射时该如何进行呢?下面还是亲自动手实验一下吧。   在“项目”视图的“配置文件”目录下重新添加配置文件web.xml,内容如程序3-4所示。   程序3-4:web.xml    First com.servlet.First First /FirstServlet 30      程序说明:注意在程序3-4中servlet对应的URL模式(/FirstServlet)与程序3-3中注解@WebServlet对应的URL模式(/First)是不一致的。重新发布Web应用,在浏览器的地址栏输入http://localhost:8080/Chapter3/FirstServlet,将得到如图3-9所示的运行界面,而在浏览器的地址栏输入http://localhost:8080/Chapter3/First却得到一个错误提示信息。这就证明,在Web部署配置文件和注解都对Servlet进行配置的情形下,Web容器将以Web部署配置文件中的信息为准。 3.4 处 理 请 求   在3.3节学习了如何创建、部署和运行一个Servlet组件,掌握了如何利用部署配置文件或者使用注解来配置Servlet,演示了如何编写了一个简单的Servlet来显示静态提示信息。Servlet编程的核心工作便是处理客户端提交的请求信息,生成动态响应信息返回到客户端。本节将深入研究在Servlet中如何处理客户端提交的请求信息。 3.4.1 请求参数   客户端提交的信息中最常见也是最重要的一类信息便是用户提交的请求参数。   在Web程序设计中,客户端以表单方式向服务器提交数据是最常见的方法。表单数据的提交方法有两种:Post方法和Get方法。Post方法一般用于更新服务器上的资源,当使用Post方法时,提交的数据包含在HTTP实体数据内。而Get方法一般用于查询服务器上的数据,当使用Get方法时,提交的数据附加在请求地址的后面,在浏览器的地址栏中可以看到。Servlet会自动将以上两种方法得到的数据进行处理,对于Post方法或Get方法提交的数据,Servlet的处理方法是一样的,用户只要简单地调用HttpServletRequest的getParameter方法,给出变量名称即可取得该变量的值。需要注意的是,变量的名称是大小写敏感的。当请求的变量不存在时,将会返回NULL。   下面演示Servlet如何处理客户端提交的信息。首先生成提交客户端信息的页面。在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→“HTML”命令来生成提交数据的HTML页面login.html。页面模拟一个系统登录页面,用户名和密码信息通过表单提交到后台的Servlet处理,页面代码如程序3-5所示。保存并重新发布Web应用,打开IE浏览器,在地址栏中输入http://localhost:8080/Chapter3 /login.html,页面显示如图3-12所示。 图3-12 提交表单数据的页面   程序3-5:login.html    提交表单数据

欢迎登录系统

用户名:
密 码:

     下面生成处理客户端请求的Servlet。在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→Servlet命令,弹出“新建Servlet”对话框。在包com.servlet中创建一个名为GetPostData的Servlet。Servlet的URL为“/GetPostData”。注意,URL必须和login.html页面中form对象的action的属性值一致,表单提交的信息才能发送到Servlet来处理。   下面要做的是为Servlet添加处理表单提交信息的代码。代码如程序3-6所示。   程序3-6:GetPostData.java    package com.servlet; … @WebServlet(name = "GetPostData", urlPatterns = {"/GetPostData"}) public class GetPostData extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { out.println( "\n" + "

" + "get post data " + "

\n" + "
    \n" + "
  • username: " + request.getParameter("username") + "\n" + "
  • password: " + request.getParameter("password") + "\n" + "
\n" + ""); } finally { out.close(); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } … }      程序说明:doPost方法调用processRequest方法来处理客户提交的请求。方法processRequest的输入参数request是HttpServletRequest接口的实例,代表对Servlet发出的客户端请求。通过调用request的方法getParameter可以很方便地获取客户端请求参数。getParameter的参数为客户端请求参数名称。通过调用HttpServletResponse的方法getWriter可获得客户端显示对象PrintWriter,最后调用PrintWriter的println方法来显示获取的客户端请求参数。   保存程序后,重新发布Web应用。打开浏览器,在地址栏中输入http://localhost:8080/ Chapter3/login.html,得到登录页面login.html。在“用户名”文本框中输入john,在“密码”文本框中输入123,单击“提交”按钮,表单数据被提交到Servlet GetPostData。返回的运行结果页面如图3-13所示,可以看到Servlet已经正确地获取到客户端提交的用户参数。 图3-13 显示获取的客户端的输入信息   Servlet获取客户端提交的信息就是这么简单。但不要高兴得太早,单击浏览器的“后退”按钮,回到如图3-12所示的提交表单数据页面。在“用户名”文本框中输入“张三”,在“密码”文本框中输入123,单击“提交”按钮,得到如图3-14所示的页面。页面中本来应该显示的汉字信息全部显示为乱码。 图3-14 提交汉字信息后的错误显示   造成这种现象的原因是客户端提交的参数值为汉字。不同于西文字母编码,每个汉字编码占2字节,而利用getParameter方法获取客户端请求变量,默认的编码方式是西文,如此得到的只是半个汉字,显示为乱码自然就不奇怪了。解决问题的方法很简单,根据程序3-5,开发人员知道页面的编码字符集为UTF-8,因此只需要在程序3-6的processRequest方法中第一行位置添加如下代码:    request.setCharacterEncoding("UTF-8");    其中,调用setCharacterEncoding方法确保参数信息以UTF-8编码方式提取。   重新发布Web应用,打开浏览器,在地址栏中输入http://localhost:8080/Chapter3/ login.html,调出提交表单数据页面。在“用户名”文本框中输入“张三”,在“密码”文本框中输入123,提交,得到结果如图3-15所示。可以看到汉字信息已经正确显示了。 图3-15 显示获取的客户端的汉字信息   表单提交的数据中的有些参数的值可能不止一个,如复选框对应的参数。如果参数有多个值,这时应该调用getParameterValues方法,这个方法将会返回一个字符串数组。   下面通过一个调查问卷的示例来说明如何获取请求中的多值变量。在Web应用项目中新建HTML页面multiChoice.html。页面包含进行调查问卷的一个复选框,如图3-16所示。页面完整代码如程序3-7所示。 图3-16 包含复选框的输入页面   程序3-7:multiChoice.html    多值变量提交 选出你喜欢吃的水果:
苹果 西瓜 桃子 葡萄
     下面创建处理客户端请求的Servlet。Servlet名称MultiChoiceServlet,所在的Java包为com.servlet,对应的URL模式为“/multichoice”。注意, URL必须和multiChoice.html页面中form对象的action的属性值一致,表单提交的信息才能发送到Servlet来处理。主要代码如程序3-8所示。   程序3-8:MultiChoiceServlet.java    package com.servlet; … @WebServlet(name = "MultiChoiceServlet", urlPatterns = {"/multichoice"}) public class MultiChoiceServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8");//解决编码问题 PrintWriter out = response.getWriter(); try { String[] paramValues = request.getParameterValues("checkbox1"); Stringtemp = new String(""); for (int i = 0; i < paramValues.length; i++) { temp += paramValues[i] + " "; } out.println("你喜欢吃的水果有:" + temp + "。"); } finally { out.close(); } } … }      程序说明:由于复选框对应的请求参数有多个值,程序调用request.getParameterValues ("checkbox1")获得参数值数组,然后采用遍历的方式,将其中的每个值取出显示。程序运行结果如图3-17所示。 图3-17 获取复选框提交的信息 3.4.2 Header   在接收到的请求信息中,除了用户提交的参数外,还有一类重要的信息称为Header,它代表客户端发出的请求的头部信息,相当于客户端和浏览器之间通信的控制信息,用来表示与请求相关的一些特定信息(如浏览器类型、客户端操作系统等)以及对服务器返回的响应的一些特殊要求(如可以接受的内容类型、编码格式等)。通过这些Header,Servlet将可以更加灵活地生成适应客户端需求的各种响应。   常见的Header信息如表3-1所示。 表3-1 常见Header说明 Header名称 用 途 Accept 浏览器可接受的MIME类型 Accept-Charset 浏览器支持的字符编码 Accept-Encoding 浏览器知道如何解码的数据编码类型(如gzip) Accept-Language 浏览器支持的语言 Connection 是否使用持续连接。使用持续连接可以使保护很多小文件的页面的下载时间减少 Content-Length 使用POST方法提交时,传递数据的字节数 Authorization 认证信息,一般是对服务器发出的WWW-Authenticate头的回应 Cookie 用户实现客户会话跟踪的信息文件 Host (主机和端口) User-Agent 客户端的类型,一般用来区分不同的浏览器和客户端操作系统      在Servlet中读取请求 Header的值是很简单的,只要调用HttpServletRequest的getHeader方法就可以了,方法的参数为Header的名称,返回值为String类型的Header内容。如果指定的Header不存在,则返回Null。另外,某些Header如Accept-Charset、Accept-Language等可能对应多个值,此时可调用getHeaderNames将返回一个Enumeration,它代表指定名称的Header的所有值。   下面创建一个Servlet来显示请求中的各种Header信息,代码如程序3-9所示。   程序3-9:ShowRequestHeader.java    package com.servlet; … @WebServlet(name = "ShowRequestHeader", urlPatterns = {"/ShowRequestHeader"}) public class ShowRequestHeader extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { out.println( "\n" + "" + "显示Header信息" + "" + "\n" + "\n" + "

" + "显示Header信息" + "

\n" + "\n" +"\n" + "
Header NameHeader Value"); Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = (String) headerNames.nextElement(); out.println("
" + headerName); out.println(" " + request.getHeader(headerName)); } out.println("
\n"); } finally { out.close(); } } … }      程序说明:在上面的代码中,调用HttpServletRequest 的getHeaderNames获取当前请求的所有Header信息,返回值是一个Enumeration,程序中对它进行了遍历并通过表格来显示。   运行结果如图3-18所示。 图3-18 显示请求Header信息   从图3-18中可以看到,请求的Header中包含了主机名称、端口、cookie、客户端浏览器类型以及客户端接受的MIME类型和语言属性等。Servlet组件可以根据上述信息进行特殊的处理,如根据浏览器的类型来决定生成何种页面代码。   在使用Header信息时需要注意的是,在HTTP 1.1支持的所有Header中,只有Host是必需的,因此在调用getHeader(headerName)来获取Header信息的过程中,特别要注意返回的信息是否为Null。 3.4.3 上传文件   文件上传一直是Web应用中一个常见的功能需求, Servlet对文件上传提供了强大支持。HttpServletRequest提供了两个方法用于从请求中解析出上传的文件: * Part getPart(String name) * CollectiongetParts()   前者用于获取请求中指定name的文件(注意 name指上传组件的名称而不是被上传文件的文件名),后者用于获取所有的上传文件。每一个文件用一个javax.servlet.http.Part实例来表示,它代表了上传的表单数据的一部分,可以有自己的数据实体、Header信息等。Part接口提供了处理文件的简易方法,比如write、delete等。因此,利用HttpServletRequest和Part来保存上传的文件变得非常简单,如下所示:    Part photo=request.getPart("photo"); photo.write("/tmp/photo.jpg");      下面通过一个示例来演示如何利用Part来实现文件上传。首先创建上传信息提交页面,如程序3-10所示。   程序3-10:upload.html   
文件1 文件2
     程序说明:页面主要提供一个上传界面,应注意form标记的三个属性,其中,action代表表单提交的URL地址;enctype代表表单内容的编码格式,要实现文件上传,必须设为multipart/form-data;同时method属性也必须设为post。   下面创建一个Servlet组件来处理上传文件信息,代码如程序3-11所示。   程序3-11:UploadServlet    package com.servlet; … @WebServlet(name = "UpLoadServlet", urlPatterns = {"/UpLoad"}) @MultipartConfig(location = "c:\\td") public class UploadServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { response.setContentType("text/html;charset=UTF-8"); for (Part p : request.getParts()) { if (p.getContentType().contains("image")) { String fname = getFileName(p); p.write(fname); System.out.println(fname); } } } catch (Exception e) { System.out.println(e.toString()); } finally { out.close(); } } private String getFileName(Part part) { String header = part.getHeader("Content-Disposition"); String fileName = header.substring(header.indexOf("filename=\"") + 10, header.lastIndexOf("\"")); fileName = fileName.substring(fileName.lastIndexOf("\\") + 1); return fileName; } … }      程序说明:在上面的Servlet中,除了注解@WebServlet外,还增加了注解@MultipartConfig,用来声明Servlet可以处理multipart/form-data格式的请求,它的属性location用来设置上传文件的存放路径。注意这个路径必须是已经存在的,否则将抛出异常。   在processRequest方法中,调用HttpServletRequest接口的getParts方法获得请求上传的Part,然后利用Part的write方法将其写入服务器。为了获得上传文件名称,程序自定义了方法getFileName,其中上传文件的名称包含在Part实例的名为content-disposition的Header中。   值得一提的是,在上面的示例中,还通过调用getContentType方法对上传附件的类型进行判断,实现只允许保存图像类型的附件。开发人员还需要注意,在调用Part的write方法后,Part实例将被释放。还有一点需要注意:如果请求的MIME类型不是multipart/form-data,若调用HttpServletRequest的getPart(Stringname)或getParts方法,将会有异常抛出。 3.4.4 异步请求处理   由3.2节的内容可知,对于Servlet组件接收到的每个请求,都会产生一个线程来处理请求并返回响应。这就产生了一个问题:如果客户端的请求处理是一项比较耗时的过程,例如,需要访问Web服务或者后台数据库,则当有大量用户请求此Servlet组件时,在Web容器中将会产生大量的线程,导致Web容器性能急剧恶化。   为了解决这一问题,Servlet规范中提供了对请求的异步处理支持。在异步处理模式下,客户端请求的处理流程变为:当Servlet接收到请求之后,首先需要对请求携带的数据进行一些预处理;接着,Servlet线程将请求转交给一个异步线程来执行业务处理,Servlet线程本身返回至容器并可处理其他客户端的请求,注意此时Servlet还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有HttpServletRequest和HttpServletResponse对象的引用),或者将请求继续转发给其他Servlet。如此一来,Servlet线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即 返回。   异步处理特性可以应用于Servlet和Filter两种组件,关于Filter将在3.10节进行详细讨论。由于异步处理的工作模式和普通工作模式在实现上有着本质的区别,因此默认情况下,Servlet和Filter并没有开启异步处理特性,如果希望使用该特性,则必须按照如下的方式配置:   对于使用传统的部署描述文件(web.xml)的配置方式,Servlet为标签增加了子标签,该标签的默认取值为false;如果需要启用异步处理支持,则将其设为true即可。以Servlet为例,其配置方式如下所示:    MyServlet com.demo.MyServlet true      对于使用@WebServlet和@WebFilter的情况,这两个注解都提供了asyncSupported属性,默认该属性的取值为false,要启用异步处理支持,只需将该属性设置为true即可。以@WebServlet为例,其配置方式如下所示:    @WebServlet(name = "AsyncServlet", urlPatterns = {"/AsyncServlet"}, asyncSupported = true)      下面通过一个示例来演示Servlet组件的异步处理特性。代码如程序3-12所示。   程序3-12:AsyncServlet.java    package com.servlet; … @WebServlet(name = "AsyncServlet", urlPatterns = {"/AsyncServlet"}, asyncSupported = true) public class AsyncServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("进入Servlet的时间:" + new Date() + "."); out.flush(); //在子线程中执行业务调用,并由其负责输出响应,主线程退出 AsyncContext ctx = request.startAsync(); new Thread(new Executor(ctx)).start(); out.println("
"); out.println("结束Servlet的时间:" + new Date() + "."); out.flush(); } public class Executor implements Runnable { private AsyncContext ctx = null; public Executor(AsyncContext ctx) { this.ctx = ctx; } public void run() { try { //等待30秒钟,以模拟业务方法的执行 Thread.sleep(30000); PrintWriter out = ctx.getResponse().getWriter(); out.println("
"); out.println("业务处理完毕的时间:" + new Date() + "."); out.flush(); ctx.complete(); } catch (Exception e) { e.printStackTrace(); } } } … }      程序说明:在processRequest方法中,首先调用HttpServletRequest的startAsync方法获得一个AsyncContext 实例,它代表对当前请求处理的上下文环境的封装,然后将此实例作为参数传递到一个单独的线程中执行。   在新的线程中,通过调用AsyncContext的getResponse()可以获得对此请求处理响应对象,调用getRequest方法获得请求信息,因此可以与在Servlet中处理请求一样来进行业务逻辑操作。   运行程序,将得到如图3-19所示的运行结果。 图3-19 异步处理提示信息   从上面的运行页面可以看出,Servlet处理线程结束后,业务处理由异步线程托管依然在继续,最后才由异步线程将结果信息输出到用户界面。 3.4.5 异步IO处理   Servlet 3.0 开始支持异步请求处理,但却只允许使用传统IO操作方式,如下面的代码片段所示:    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { ServletInputStream input = request.getInputStream(); byte[] b = new byte[1024]; int len = -1; while ((len = input.read(b)) != -1) { //… } }      如果读取数据阻塞或者读取数据的速度很慢,那么Servlet的线程将会一直等待读取数据。这种情况在写数据的时候也会遇到。这一IO操作的局限将会限制Web容器的可扩 展性。   Servlet 3.1 开始加入对非阻塞IO(Nonblocking IO)的支持。非阻塞IO允许开发人员只在数据准备好的时候再进行读写操作。此功能不仅增加了服务器的扩展性,还增加了服务器可处理的连接数。但是需要注意的是,非阻塞IO只允许在异步 Servlet和异步 Filter 中使用。   为支持非阻塞IO,Servlet 3.1引入了两个新的接口:ReadListener 和 WriteListener。   ReadListener 有三个回调方法: * onDataAvailable——在数据没有阻塞、已经完全准备好可以读取的时候调用。 * onAllDataRead——所有数据读取完成后调用。 * onError——处理中发生错误的时候调用。   WriteListener 有两个回调方法: * onWritePossible——当数据准备好进行无阻塞写出的时候调用。 * onError——当操作出错的时候调用。   Servlet中可对ServletInputStream调用setReadListener方法或者对ServletOutputStream调用setWriterListener方法来使用非阻塞IO取代传统IO。ServletInputStream的isFinished 方法可以用于检查非阻塞读取的状态。注意ServletInputStream只允许注册一个ReadListener。   ServletOutputStream的canWrite 方法可用于检测数据是否已经准备好进行非阻塞写出,同样,ServletOutputStream也只允许注册一个 WriteListener。   下面通过一个示例来演示如何实现非阻塞IO的Servlet。该示例由两个Servlet和一个ReadListener来实现,分别如程序3-13、程序3-14和程序3-15所示。   程序3-13:ClientServlet    package chapt3; … @WebServlet(name = "ClientServlet", urlPatterns = {"/ClientServlet"}) public class ClientServlet extends HttpServlet { OutputStream output = null; InputStream input = null; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println(""); out.println(""); out.println("非阻塞IO演示"); out.println(""); out.println(""); String urlPath = "http://" + request.getServerName() + ":" + request.getLocalPort() //default http port is 8080 + request.getContextPath() + "/ServiceServlet"; URL url = new URL(urlPath); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setChunkedStreamingMode(2); conn.setRequestProperty("Content-Type", "text/plain"); conn.connect(); try { output = conn.getOutputStream(); // 发送第一部分信息 String firstPart = "hello..."; out.println("Sending to server: " + firstPart + "
"); writeData(output, firstPart); Thread.sleep(2000); // 发送第二部分信息 String secondPart = "World..."; out.println("Sending to server: " + secondPart + "

"); out.flush(); writeData(output, secondPart); Thread.sleep(2000); // 发送第三部分信息 String thirdPart = "The End..."; out.println("Sending to server: " + thirdPart + "

"); out.flush(); writeData(output, thirdPart); // 从服务器返回信息 input = conn.getInputStream(); printEchoData(out, input); out.println("Please check server log for detail"); out.flush(); } catch (IOException ioException) { Logger.getLogger(ReadListenerImpl.class.getName()).log (Level.SEVERE, "Please check the connection or url path", ioException); } catch (InterruptedException interruptedException) { Logger.getLogger(ReadListenerImpl.class.getName()).log (Level.SEVERE, "Thread sleeping error", interruptedException); } finally { if (input != null) { try { input.close(); } catch (Exception ex) { } } if (output != null) { try { output.close(); } catch (Exception ex) { } } } out.println(""); out.println(""); } … protected void writeData(OutputStream output,String data) throws IOException { if (data != null && !data.equals("") && output != null) { output.write(data.getBytes()); output.flush(); } } protected void printEchoData(PrintWriter out, InputStream input) throws IOException { while (input.available() > 0 && input != null && out != null) { out.print((char) input.read()); } out.println("
"); } }      程序说明:程序用来模拟对服务器组件ServiceServlet发起IO请求操作。在processRequest方法中,首先创建一个与ServiceServlet的连接conn,并通过调用conn的setChunkedStreamingMode(2)方法设置此连接为分块传输模式,之后调用连接对象conn的getOutputStream方法获得ClientServlet的输出流(即ServiceServlet的输入流),就可以对输出流进行操作了。在发送完三段信息后,调用连接对象的getInputStream来获得ClientServlet的输入流并将输入结果显示出来。其中方法writeData和printEchoData是操作输入输出流的辅助方法。   程序3-14:ServiceServlet.java    package chapt3; … @WebServlet(name = "ServiceServlet", urlPatterns = {"/ServiceServlet"} , asyncSupported = true) public class ServiceServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { final AsyncContext context = request.startAsync(); final ServletInputStream input = request.getInputStream(); final ServletOutputStream output = response.getOutputStream(); input.setReadListener(new ReadListenerImpl(input, output, context)); System.out.println("ServiceServlet returned"); } … }      程序说明:程序用来实现非阻塞IO操作。具体步骤是在processRequest方法中首先获得请求对象的ServletInputStream、ServletOutputStream和 AsyncContext,并以此为参数来构造一个javax.servlet.ReadListener的实例对象,之后调用ServletInputStream的setReadListener并将新建的ReadListener的实例对象来实现异步IO。注意Servlet的注解中要加上属性asyncSupported 并设置为 true,因为非阻塞IO是在异步Servlet的基础上实 现的。   程序3-15:ReadListenerImpl.java    package chapt3; import java.io.IOException; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; public class ReadListenerImpl implements ReadListener { private ServletInputStream input; private ServletOutputStream output; private AsyncContext context; private StringBuilder sb = new StringBuilder(); ReadListenerImpl(ServletInputStream input, ServletOutputStream output, AsyncContext context) { this.input = input; this.output = output; this.context = context; } @Override public void onDataAvailable() throws IOException { System.out.println("Data is available"); while (input.isReady() && !input.isFinished()) { sb.append((char) input.read()); } sb.append(" "); } @Override public void onAllDataRead() throws IOException { try { output.print("Total Received Bytes: " + sb.length() + "
"); output.print("Received Contents: " + sb.toString() + "
"); output.flush(); } finally { context.complete(); } System.out.println("Data is all read"); } @Override public void onError(Throwable t) { context.complete(); System.out.println("--> onError"); } }      程序说明:程序实现了ReadListener接口,并通过重载onDataAvailable、onAllDataRead和onError方法来对IO事件进行响应。   运行程序3-13,将得到如图3-20所示的运行结果,显示ClientServlet和ServiceServlet之间的交互已经成功完成。   查看NetBeans的服务器日志窗口,如图3-21所示,可以看到在处理完第一部分数据请求后,ServiceServlet的主线程已经返回,而对Servlet的IO操作却在后台一直异步运行,直到所有数据全部读取完毕。 图3-20 程序3-13运行结果 图3-21 异步IO操作在服务器后台输出的日志信息 3.5 生 成 响 应   Servlet的核心职责就是根据客户端的请求来生成动态响应。在ServletResponse接口中定义了一系列与生成响应结果相关的方法,如表3-2所示。 表3-2 ServletResponse接口的主要方法 方 法 描 述 信 息 setCharacterEncoding(String charset) 设置响应正文的字符编码。响应正文的默认字符编码为ISO-8859-1 setContentLength(int len) 设置响应正文的长度 setContentType(String type) 设置响应正文的MIME类型 getCharacterEncoding() 返回响应正文的字符编码 getContentType() 返回响应正文的MIME类型 setBufferSize(int size): 设置用于存放响应正文数据的缓冲区的大小 getBufferSize() 获得用于存放响应正文数据的缓冲区的大小 reset() 清空缓冲区内的正文数据,并且清空响应状态代码及响应头 resetBuffer() 仅仅清空缓冲区内的正文数据,不清空响应状态代码及响应头 flushBuffer() 强制性地把缓冲区内的响应正文数据发送到客户端 isCommitted() 返回一个boolean类型的值。如果为true,表示缓冲区内的数据已经提交给客户,即数据已经发送到客户端 getOutputStream() 返回一个ServletOutputStream对象,Servlet用它来输出二进制的正文数据 getWriter() 返回一个PrintWriter对象,Servlet用它来输出字符串形式的正文数据    3.5.1 编码类型   ServletResponse中响应正文的默认MIME类型为text/plain,即纯文本类型;而HttpServletResponse中响应正文的默认MIME类型为text/html,即HTML文档类型。可以通过调用getContentType方法获得当前响应正文的MIME类型,或者通过调用setContentType(String type)来设置当前响应正文的MIME类型。      说明:MIME意为多媒体Internet邮件扩展,它设计的最初目的是为了在发送电子邮件时附加多媒体数据,让邮件客户程序能根据其类型进行处理。在最早的HTTP协议中,并没有附加的数据类型信息,所有传送的数据都被客户程序解释为超文本标记语言HTML 文档,而随着Internet应用的不断扩展,为了支持多媒体数据类型,HTTP协议中就使用了附加在文档之前的MIME数据类型信息来标识数据类型。      Web浏览器使用MIME类型来识别非HTML文档,并决定如何显示该文档内的数据。如果浏览器中安装了与MIME类型对应的插件(plug-in),则当Web浏览器下载MIME类型指示的文档时,就能够启动相应插件处理此文档。某些MIME类型还可以与外部程序结合使用,浏览器下载文档后会启动相应的外部程序。有时候浏览器不能识别文档的MIME类型,通常这是由于没有安装这些文档需要的插件而导致的。在这种情况下,浏览器会弹出一个对话框,询问用户是否需要打开该文件或是将它保存到本地磁盘上。   通过调用setContentType(String type),Servlet可以向浏览器返回非HTML文件,比如 Adobe PDF和Microsoft Word。使用正确的MIME类型能够保证这些非HTML文件被正确的插件或外部程序处理显示。   PDF 文件的MIME类型是application/pdf。如果需要Servlet返回PDF文档,则需要将 response 对象中header的content类型设置成application/pdf。代码如下:    res.setContentType("application/pdf");      若要返回一个Microsoft Word文档,就要将response对象的content类型设置成 application/msword。代码如下:    res.setContentType("application/msword");      如果是一个Excel文档,则使用MIME类型application/vnd.ms-excel。其中vnd表示该应用程序的制造者,必须将它包含在MIME类型里才能够打开该类型的文档。代码如下:    res.setContentType("application/vnd.ms-excel"); 3.5.2 流操作   在Servlet与客户的请求应答的过程中,底层是通过输入输出流来实现的。Servlet支持两种格式的输入输出流。一种是字符输入输出流。ServletResponse的getWriter方法返回一个PrintWriter对象,Servlet可以利用PrintWriter来输出字符流形式的正文数据。另外一种是字节输入输出流。ServletResponse的getOutputStream方法返回一个 ServletOutputStream对象,Servlet可以利用ServletOutputStream来输出二进制的正文数据。   为了提高输出数据的效率,ServletOutputStream和PrintWriter先把数据写到缓冲区内。当缓冲区内的数据被提交给客户后,ServletResponse的isCommitted方法返回true。在以下几种情况下,缓冲区内的数据会被提交给客户,即数据被发送到客户端: * 当缓冲区内的数据已满时,ServletOutputStream或PrintWriter会自动把缓冲区内的数据发送给客户端,并且清空缓冲区。 * 调用ServletResponse对象的flushBuffer方法。 * 调用ServletOutputStream或PrintWriter对象的flush方法或close方法。   为了确保ServletOutputStream或PrintWriter输出的所有数据都会被提交给客户,比较安全的做法是在所有数据都输出完毕后,调用 ServletOutputStream或PrintWriter的close方法。   下面编写一个返回PDF文件的Servlet来说明Servlet如何实现向客户端发送非HTML文档,同时演示Servlet对输入输出流的操作。代码如程序3-16所示。   程序3-16:PDFServlet.java    package com.servlet; … @WebServlet(name = "PDFServlet", urlPatterns = {"/pdfshow"}) public class PDFServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("application/pdf"); ServletOutputStream out = response.getOutputStream(); File pdf = null; // BufferedInputStream buf = null; byte[] buffer = new byte[1024 * 1024]; FileInputStream input = null; try { pdf = new File("c:\\sample.pdf");//为演示PDF文件发送而保存的一个文件 response.setContentLength((int) pdf.length()); input = new FileInputStream(pdf); int readBytes = -1; while ((readBytes = input.read(buffer, 0, 1024 * 1024)) != -1) { out.write(buffer, 0, 1024 * 1024); } } catch (IOException e) { System.out.println("file not found!"); } finally { if (out != null) { out.close(); } if (input != null) { input.close(); } } } } … }      程序说明:首先调用HttpServletResponse接口的setContentType("application/pdf")将响应内容类型设置为PDF类型,然后调用getOutputStream()获取Servlet输出流对象。为使PDF以流的形式输出到客户端,先创建一个File对象,根据File对象得到一个文件输入流对象。通过将文件输入流中的信息写到Servlet输出流中实现PDF文件的发送。为了防止下载的数据量过大,代码中使用了一个容量为1MB的缓冲区。   为运行示例程序,需要首先在“C:”盘根目录下放置一个PDF文件并将其命名为sample.pdf。打开浏览器,在地址栏中输入http://localhost:8080/Chapter3/pdfshow,如果读者的机器装有Acrobat Reader,那么,Servlet发送到客户端的PDF文件sample.pdf将在浏览器内被打开。如果没有安装Acrobat Reader,则浏览器将提示保存文件。   如果读者的机器装有Acrobat Reader,但只想通过这种方式传送到客户端,而不想在浏览器内部打开,那么怎么办呢?一种叫作content-disposition的HTTP response header(响应头部)允许将文档指定成单独打开(而不是在浏览器中打开),还可以为该文档建议一个保存时的文件名。   在程序3-16的ServletOutputStream out =res.getOutputStream()一行下面添加如下代码:    res.setHeader("Content-disposition","attachment;filename=Example.pdf");      打开浏览器,以同样的方式重新调用Servlet,则出现如图3-22所示的提示对话框,PDF被单独保存而不是在浏览器内打开。 图3-22 单独保存PDF的提示对话框 3.5.3 重定向   ServletResponse接口还提供了一个重要的方法sendRedirect,该方法允许将当前请求定位到其他Web组件上,这个组件甚至可以是其他主机上的Web组件。在将请求重新定位之前,Servlet可以对当前的请求或响应对象通过调用SetAttribute方法来添加属性信息。重定向相当于通知客户端重新发起一个新的请求,因此重定向后在浏览器地址栏中会出现重定向页面的URL。   下面修改程序3-16中的Servlet组件,使它重定向到3.3节创建的Servlet First,修改后的代码如程序3-17所示   程序3-17:PDFServlet.java    package com.servlet; … @WebServlet(name = "PDFServlet", urlPatterns = {"/pdfshow"}) public class PDFServlet extends HttpServlet { … @Override protected void doGet(HttpServletRequest request, HttpServletResponse res) throws ServletException, IOException { res.sendRedirect("First"); return; } … }      程序说明:在doGet方法中,不再调用processRequest方法,而是调用sendRedirect方法实现请求重定向,并且之后立即调用了return语句。这是因为请求已经重定向,若再继续操作HttpServletRequest 和HttpServletResponse,将会抛出异常。   运行程序3-17,结果如图3-23所示。特别要注意图3-23中浏览器中的地址栏信息,看看是否已经发生变化。 图3-23 重定向导致浏览器地址栏变化   还需要注意的是,在调用sendRedirect方法前不允许有任何信息输出到客户端,因为Web容器在Servlet组件已经有信息输出到客户端的情形下,是不允许进行重定向的。 3.5.4 服务器推送   3.1节已经简单介绍了HTTP 1.1协议。随着互联网的快速发展,HTTP 1.1协议得到了迅猛发展,但当一个页面包含了数十个请求时,HTTP 1.1协议的局限性便暴露了出来: * 每个请求需要单独建立与服务器的连接,浪费资源。 * 每个请求与响应都需要添加完整的头信息,应用数据传输效率较低。 * 默认没有进行加密,数据在传输过程中容易被监听与篡改。   HTTP 2正是为了解决HTTP 1.1暴露出来的问题而诞生的。HTTP 2最大的特点是:不会改动HTTP 的语义、HTTP方法、状态码、URI及首部字段等核心概念,而是致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。一些知名的网站如www.baidu.com已经开始全面支持HTTP 2。   HTTP 1.1协议传输的主要是文本信息,而HTTP 2把HTTP协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息,并行地在同一个TCP连接上双向交换消息。例如,客户端使用HTTP 2协议请求页面http://www.163.com,则页面上所有的资源请求都是通过客户端与服务器之间的一条TCP连接完成请求和响应的。   另外HTTP 2新增的一个强大的功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,服务器除了对最初请求的响应外,还可以额外向客户端推送资源,而无须客户端明确地请求。例如,当客户端浏览器请求一个HTML文件,服务器已经能够知道客户端接下来要请求页面中链接的其他资源(如logo图片、css文件等)了,因此将自动推送这些资源给客户端而不需要等待浏览器得到HTML文件后解析页面再发送资源请求。服务器推送有一个很大的优势便是可实现客户端缓存。对于相同的资源,客户端将可以直接在本地缓存中读取。由于HTTP 2可主动向服务器端推送数据,目前各大浏览器出于安全考虑,仅支持安全连接下的HTTP 2,因此HTTP 2目前在实际使用中,只用于HTTPS协议场景下。   在最新的Servlet 4.0中,也提供了对HTTP 2的推送资源(push)特性的支持。   下面通过一个示例来演示如何在HTTP 2下向客户端推送资源。代码如程序3-18所示。   程序3-18:TestServlet.java    @WebServlet(name = "TestServlet", urlPatterns = {"/TestServlet"}) @ServletSecurity(httpMethodConstraints={ @HttpMethodConstraint(value="GET", transportGuarantee=CONFIDENTIAL) }) public class TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { PushBuilder pushBuilder = req.newPushBuilder().path("my.css"); pushBuilder.push(); res.getWriter().println("HTTP2 Test Hello"); } }      程序说明:调用HttpServletRequest 的newPushBuilder获得请求的PushBuilder对象,并调用path方法进行填充,最后调用PushBuilder的push方法将资源对象输出到客户端。注意Servlet组件多了注解@ServletSecurity,表示Servlet仅运行在HTTPS协议下且仅支持Get方法。   注意在运行程序之前需要首先在服务器端准备推送的资源my.css。代码如程序3-19 所示。   程序3-19:my.css    body { color: blue; }      运行程序3-18,由于服务器端的Push需要运行在HTTPS协议下,NetBeans配置的GlassFish Server 5并没有配置相应的数字证书,因此浏览器会弹出如图3-24所示的警告提示信息。单击“转到此网页(不推荐)”,将得到如图3-25所示的运行结果。可以看到由于应用了服务器端Push来的my.css,结果页面中的文本已经变成蓝色。 图3-24 浏览器弹出的安全提示 图3-25 程序3-18运行结果 3.6 Servlet配置 3.6.1 初始化参数   Servlet除了从请求对象中获取信息以外,还可以从配置文件中获取配置参数信息。与请求中的动态信息不同,配置文件中的参数信息与具体的请求无关,而是Servlet初始化时调用的。通过配置信息来初始化Servlet可以有效避免硬编码,提高Servlet的可移植性。   Servlet配置参数保存在ServletConfig对象中。在Servlet被实例化后,ServletConfig对象对任何客户端在任何时候的访问都有效,但一个Servlet的ServletConfig对象不能被其他Servlet访问。   在Servlet中调用getServletConfig方法可直接获取ServletConfig对象。   在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→Servlet命令,弹出New Servlet对话框。在“类名”文本框中输入Servlet名称InitParamServlet。在“包”文本框中输入Servlet类所在的java包名称com.servlet。单击“下一步”按钮,得到如图3-26所示的对话框。选中“将信息添加到部署描述符(web.xml)”复选框。 图3-26 配置Servlet的初始化参数   单击“新建”按钮,在“初始化参数”列表中将新增一项。在“名称”单元格中输入Servlet初始化参数名称FileType,在“值”单元格中输入初始化参数的值image。默认其他选项设置,单击“完成”按钮,NetBeans自动生成InitParamServlet的框架源文件。主要代码如程序3-20所示。   程序3-20:InitParamServlet.java    package com.servlet; … public class InitParamServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { response.setContentType("text/html;charset=UTF-8"); for (Part p : request.getParts()) { String ftype=this.getInitParameter("FileType"); if (p.getContentType().contains(ftype)) { String fname = getFileName(p); p.write(fname); System.out.println(fname); System.out.println(p.getContentType()); } } } catch (Exception e) { System.out.println(e.toString()); } finally { out.close(); } } private String getFileName(Part part) { String header = part.getHeader("Content-Disposition"); String fileName = header.substring(header.indexOf("filename=\"") + 10, header.lastIndexOf("\"")); fileName = fileName.substring(fileName.lastIndexOf("\\") + 1); return fileName; } … }      程序说明:首先需要注意的是,由于示例选择将在web.xml中保存配置信息,因此,代码中便没有了注解@WebServlet。程序中通过调用Servlet的getInitParameter方法可以方便地获得Servlet的初始化参数。若Servlet有多个初始化参数,则可调用getInitParameter获得代表初始化参数列表的枚举。   Servlet在web.xml中的配置信息如程序3-21所示。   程序3-21:web.xml(片段)    InitParamServlet com.servlet.InitParamServlet FileType image InitParamServlet /InitParamServlet      修改程序3-11,将文件上传表单的action属性指向Servlet组件InitParamServlet,重新运行程序3-11,看看会得到什么结果。      说明:Servlet的初始化参数也可以通过注解@WebInitParam在Servlet实现代码中进行配置,但是将Servlet的初始化参数配置在部署描述文件中,如果在部署Servlet组件时需要调整初始化参数,可直接编辑部署描述文件,而不需要重新编译代码,从而大大提高了程序部署的灵活性。    3.6.2 URL模式   在Servlet配置中,除了初始化参数外,还有一个重要的工作便是配置Servlet对应的URL地址信息,又称为URL模式。这里之所以称之为URL模式而不是URL地址,是因为同一个Servlet可以被映射到多个URL地址上。   另外,在Servlet映射到URL中也可以使用*通配符,但是只能有两种固定的格式:一种格式是“*.扩展名”,另一种格式是以正斜杠(/)开头并以“/*”结尾。如下所示:    First /First/* First *.do    3.6.3 默认Servlet   特别值得一提的是,如果某个Servlet的映射路径仅仅为一个正斜杠(/),那么这个Servlet就成为当前Web应用程序的默认Servlet。   凡是在当前Web应用上下文找不到匹配的组件的URL,它们的访问请求都将交给默认Servlet处理,也就是说,默认Servlet负责处理所有其他Servlet都不处理的访问请求。 3.7 会 话 管 理   3.1节讲过HTTP协议是一种无状态的协议,客户端每次打开一个Web 页面,它就会与服务器建立一个新的连接,发送一个新的请求到服务器,服务器处理客户端的请求,返回响应到客户端,并关闭与客户端建立的连接。当客户端发起新的请求,那么它重新与服务器建立连接,因此服务器并不记录关于客户的任何信息。但是对于许多Web应用而言,服务器往往需要记录特定客户端与服务器之间的一系列请求响应之间的特定信息。例如,一个在线网上商店需要记录在线客户的个人信息、添加到购物车中的商品信息等。如果顾客每打开一个新的页面都需要重新输入登录信息确认身份,那么这个网上商店可能只能关门大吉了。从特定客户端到服务器的一系列请求称为会话。在Web服务器看来,一个会话是由在一次浏览过程中所发出的全部HTTP请求组成的。换句话说,一次会话是从客户打开浏览器开始到关闭浏览器结束。记录会话信息的技术称为会话跟踪,对于开发人员而言会话跟踪不是容易解决的问题。会话跟踪的第一个障碍是如何唯一标识每一个客户会话。这只能通过为每一个客户分配一个某种标识,并将这些标识保存在客户端上,以后客户端发给服务器的每一个HTTP请求都提供这些标识来实现。那么为什么不能用客户端的IP地址作为标识呢?这是因为在一台客户端上可能同时发出多个不同的客户请求,而且,如 果多个不同客户请求还可能是通过同一个代理服务器发出的,因此IP地址不能作为唯一 标识。   常见会话跟踪技术有Cookie和URL重写等。 3.7.1 Cookie   Cookie是一小块可以嵌入到HTTP请求和响应中的数据。典型情况下,Web服务器将Cookie值嵌入到响应的Header,而浏览器则在其以后的请求中都将携带同样的Cookie。Cookie的信息中可以有一部分用来存储会话ID,这个ID被服务器用来将某些HTTP请求绑定在会话中。Cookie由浏览器保存在客户端,通常保存为一个文本文件。Cookie还含有一些其他属性,诸如可选的注释、版本号及最长生命期。   为加深对Cookie的理解,下面创建一个Servlet来显示Cookie的相关信息。代码如程序3-22所示。   程序3-22:CookieServlet.java    package com.servlet; … @WebServlet(name=" CookieServlet ", urlPatterns={"/cookie"}) public class CookieServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Cookie cookie = null; //获取请求相关的cookie Cookie[] cookies = request.getCookies( ); boolean newCookie = false; //判断Cookie ServletStudy是否存在 if (cookies != null){ for (int i = 0; i < cookies.length; i++){ if (cookies[i].getName( ).equals("Chapter3")){ cookie= cookies[i]; } }//end for }//end if if (cookie == null){ newCookie=true; int maxAge=10000; //生成cookie对象 cookie= new Cookie("Chapter3","create by hyl"); cookie.setPath(request.getContextPath( )); cookie.setMaxAge(maxAge); response.addCookie(cookie); }//end if // 显示信息 response.setContentType("text/html"); java.io.PrintWriter out = response.getWriter( ); out.println(""); out.println(""); out.println("Cookie Info"); out.println(""); out.println(""); out.println( "

Information about the cookie named \"Chapter3\"

"); out.println("Cookie value: "+cookie.getValue( )+"
"); if (newCookie){ out.println("Cookie Max-Age: "+cookie.getMaxAge( )+"
"); out.println("Cookie Path: "+cookie.getPath( )+"
"); } out.println(""); out.println(""); } }      程序说明:HttpServletRequest对象有一个getCookies方法,它可以返回当前请求中的Cookie对象的一个数组。程序首先调用getCookies方法获得request对象中的所有Cookie,然后寻找是否有名为Chapter3的Cookie。如果有,则调用Cookie对象的getValue、getName等方法显示其信息;如果没有,则创建一个新的Cookie对象,并调用response.addCookie方法将其加入到response对象并返回到客户端。以后客户端对服务器的任何访问都会在其头部携带此Cookie。可以通过刷新页面来查看Cookie的信息,可以看到显示的Cookie信息是不变的。   重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/ cookie,得到如图3-27所示的运行结果页面。 图3-27 显示Cookie信息   由于同一客户端对服务器的请求都会携带Cookie,因此可以通过在Cookie中添加与会话相关的信息以达到会话跟踪的目的。下面通过创建一个Servlet来演示如何通过Cookie实现会话跟踪。代码如程序3-23所示。   程序3-23:CookieTrackServlet.java    … @WebServlet(name=" CookieTrackServlet ", urlPatterns={"/ cookietrack"}) public class CookieTrackServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Cookie cookie=null; //获取请求相关的Cookie Cookie[] cookies=request.getCookies(); //判断Cookie VisitTimes是否存在,如果存在,其值加1 if(cookies!=null){ boolean flag=false; for(int i=0; (i"); out.println(""); out.println("Cookie跟踪会话"); out.println(""); out.println(""); out.println("

您好!

"); out.println("欢迎您第"+cookie.getValue()+"次访问本页面
"); out.println(""); out.println(""); } … }      程序说明:程序使用Cookie来实现会话的跟踪,在本示例中跟踪的是会话中页面的访问次数。程序通过将页面访问的次数写入一个名为VisitTimes的Cookie中。由于对页面的请求每次都包含了这个Cookie,因此通过每次将Cookie的值取出来显示页面的访问次数,同时又将更新过的值写回到Cookie来达到会话跟踪的目的。   重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/ cookietrack,得到如图3-28所示的运行结果页面,不停地刷新页面,页面中显示的值也不停地刷新,可以看到服务器可以准确地跟踪客户端的访问次数。 图3-28 用Cookie实现会话跟踪 3.7.2 URL重写   关于是否应当使用Cookie有很多的争论,因为一些人认为Cookie可能会造成对隐私权的侵犯。有鉴于此,大部分浏览器允许用户关闭Cookie功能,这使得跟踪会话变得更加困难。如果不能依赖Cookie的支持又该怎么办呢?那将导致不得不使用另外一种会话跟踪方法——URL重写。   URL重写通过在URL地址后面增加一个包含会话信息的字符串来记录会话信息。URL地址与会话信息的字符串之间用“?”隔开。如果请求还包含多个参数,则参数与会话信息以及参数间用“&”隔开。   下面通过编写一个Servlet URLRewrite1来演示如何利用URL重写来向服务器端传递会话信息。这里假设客户端向服务器端传递的会话信息是用户的身份信息:姓名和年龄。代码如程序3-24所示。   程序3-24:URLRewrite1.java    package com.servlet; … @WebServlet(name=" URLRewrite1", urlPatterns={"/ url1 "}) public class URLRewrite1 extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); java.io.PrintWriter out = response.getWriter( ); String contextPath = request.getContextPath( ); String encodedUrl = response.encodeURL(contextPath + "/url2?name=张三&age=27"); out.println(""); out.println(""); out.println("URL Rewriter"); out.println(""); out.println(""); out.println( "

URL重写演示:发送参数

"); out.println("转到URL2here."); out.println(""); out.println(""); out.close(); } … }      程序说明:程序首先调用response的encodeURL生成URL字符串。其中request的getContextPath用来获取请求上下文路径。URL字符串包含的会话信息为两个参数:name和age,其值分别为“张三”和27。   下面通过在Web应用Chapter3中创建一个名为URLRewrite2的Servlet来演示服务器端如何获取通过URL重写方式传递来的会话信息。代码如程序3-25所示。   程序3-25:URLRewrite2.java    package com.servlet; … @WebServlet(name=" URLRewrite2", urlPatterns={"/ url2 "}) public class URLRewrite2 extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); request.setCharacterEncoding("UTF-8"); java.io.PrintWriter out = response.getWriter( ); String contextPath = request.getContextPath( ); out.println(""); out.println(""); out.println("URL Rewriter"); out.println(""); out.println(""); out.println( "

URL重写演示:接收参数

"); out.println("下面是接收的参数:
"); out.println("name="+request.getParameter("name")); out.println("age="+request.getParameter("age")); out.println(""); out.println(""); out.close(); } … }      程序说明:对于利用URL重写技术传递来会话信息,可以调用request.getParameter来获取,就像获取表单提取的参数信息一样。实际上,通过表单向服务器端提交数据就是通过URL重写的方式。注意,如果传递的是汉字编码的信息,在提取参数前,别忘了通过“request.setCharacterEncoding("UTF-8");”来设置请求编码格式,否则得到的将会是乱码。   重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/url1,得到如图3-29所示的运行结果页面。单击页面中的链接here,则浏览器被导向地址http://localhost:8080/Chapter3/url2,得到如图3-30所示的运行结果页面,可以看到,客户端通过URL重写的会话信息已经传递到Servlet组件URLRewrite2并被正确解析。 图3-29 URL重写:发送参数 图3-30 接收URL重写的参数信息 3.7.3 HttpSession   为消除代码中手工管理会话信息的需要(无论使用什么会话跟踪方式),Servlet规范定义了HttpSession接口以方便Servlet容器进行会话跟踪。这个高级接口实际上是建立在Cookie和URL重写这两种会话跟踪技术之上的,只不过由Web容器自动实现了关于会话跟踪的底层机制,不再需要开发人员了解具体细节。HttpSession 接口允许 Servlet查看和管理关于会话的信息,确保信息持续跨越多个用户连接等。   使用 HttpSession接口进行程序开发的基本步骤如下:   (1)获取HttpSession对象。   (2)对HttpSession对象进行读或写。   (3)手工终止HttpSession,或者什么也不做,让它自动终止。每个HttpSession对象都有一定的生命周期,超过这个周期,容器自动将HttpSession对象中止。   程序开发中经常使用的HttpSession 接口方法有以下几个:   (1)isNew()。如果客户端还不知道会话,则返回 true。如果客户端已经禁用了Cookie,则会话在每个请求上都是新的。   (2)getId()。返回包含分配给这个会话的唯一标识的字符串。在使用URL改写已标识会话时比较有用。   (3)setAttribute()。使用指定的名称将对象绑定到会话。   (4)getAttribute()。返回绑定到此会话的指定名称的对象。   (5)setMaxInactiveInterval()。指定在 Servlet 使该会话无效之前客户端请求间的时间。负的时间表示会话永远不会超时。   (6)invalidate()。终止当前会话,并解开与它绑定的对象。   下面通过一个示例来演示如何用HttpSession来存储当前会话中用户访问站点的次数。   在项目中创建Servlet HitCounter,代码如程序3-26所示。   程序3-26:HitCounter    package com.Servlet; … @WebServlet(name=" HitCounter ", urlPatterns={"/hitcounter "}) public class HitCounter extends HttpServlet { static final String COUNTER_KEY = "Counter"; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取会话对象 HttpSession session = request.getSession(true); response.setContentType("text/html;charset=gb2312"); java.io.PrintWriter out = response.getWriter(); //从会话中获取属性 int count = 1; Integer i = (Integer) session.getAttribute(COUNTER_KEY); if (i != null) { count = i.intValue() + 1; } //将属性信息存入会话 session.setAttribute(COUNTER_KEY, new Integer(count)); Date lastAccessed = new Date(session.getLastAccessedTime( )); Date sessionCreated=new Date(session.getCreationTime()); DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); //输出会话信息 out.println(""); out.println(""); out.println("会话计数器"); out.println(""); out.println(""); out.println("你的会话ID: " +session.getId()+ "
"); out.println("会话创建时间:"+formatter.format(sessionCreated) + "
"); out.println("会话上次访问时间:"+formatter.format(lastAccessed) + "
"); out.println("
会话期间你对页面发起 " + count + " 次请求"); out.println("
"); out.println(""); out.println("
"); out.println(""); out.println(""); out.flush(); out.close(); } … }      程序说明:Servlet中使用HttpServletRequest对象的getSession方法来取得当前的用户会话。GetSession的参数决定了如果会话不存在,是否创建一个新会话(还有一个版本的getSession没有任何参数,它将默认创建一个新会话)。一旦获得了会话对象,就可以像操作哈希表一样使用一个唯一的键,在会话对象中加入或者获取任何对象。通过调用setAttribute将用户访问次数信息存入会话,通过调用getAttribute来获取会话中存储的信息。      注意:由于会话数据是由Web容器维护存储的,在为这些键赋值时一定要注意维护它的唯一性。一个比较好的方法是为每个会话属性的名称定义一个static final类型的String变量。      重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3 /hitcounter,得到如图3-31所示的运行结果页面。用户第一次打开HitCounter Servlet的时候,如果会话还不存在,就会创建一个新的会话(一定要注意,其他Servlet可能已经建立了这个用户的会话对象)。通过一个唯一键从会话对象中取得一个整数,如果这个整数不存在,就使用初始值1,否则每次给这个整数加1。最后,新的值被写回会话对象。一个简单的HTML页被返回给浏览器显示,它显示了会话ID及用户通过单击“再次单击”按钮获取这一页的访问次数。 图3-31 利用会话存储页面访问次数 3.8 Servlet上下文   服务器上的每个Web应用都会有一个背景环境对象,称为上下文,Web应用中的所有资源包括Servlet、JSP、JavaBean和静态HTML页面等共享此上下文对象,因此上下文对象提供了一个同一Web应用内的不同资源间共享信息的场所。Javax.Servlet. ServletContext接口提供正在运行的Servlet所处的Web应用程序的上下文对象的视图,可以通过getServletContext方法得到该Servlet运行的上下文对象。在创建Web应用程序时,通过Servlet 上下文可以实现以下功能:   (1)访问Web应用程序资源。ServletContext可以通过getResource和getResourceAsStream方法访问Web应用程序内的静态资源文件。   (2)在Servlet上下文属性中保存Web应用程序信息。上下文对象可以用来存储Java对象,通过字符串值的key来识别对象,这些属性对整个Web应用程序都是全局的,Servlet可以通过getAttribute、getAttributeNames、removeAttribute和setAttribute方法进行操作。   (3)获取应用初始化参数信息。可以调用ServletContext的getInitParameterNames方法返回一个初始化参数的枚举对象(java.util.Enumeration),或直接指定一个参数名来得到特定的参数值,如ServletContext.getInitParameter(String name)。      注意:这里的初始化参数指的是整个Web应用的初始化参数,而不是针对哪个具体Web组件的。例如,开发人员可以将Web应用的运行模式设置为应用的初始化参数。在初始化参数修改后,必须重新启动Web应用才会生效。      (4)提供日志支持。可以简单地通过调用ServletContext.log(String msg)或ServletContext.log(String msg,Throwable throwable)方法向底层的Servlet日志记录写入日志信息;ServletContext.log(String msg,Throwable throwable)方法还可写入异常信息和throwable的跟踪栈。   ServletContext对此Web 应用的任何客户端请求在任何时间都有效。要访问ServletContext对象,只要调用getServletContext()就可以了。   下面通过程序演示两个Servlet组件间如何通过上下文进行协同工作。其中AdminTemperatureServlet用来更新当前温度信息,ShowTemperatureServlet仅用来显示当前温度信息,两个Servlet间通过上下文中的属性来传递信息,实现温度信息实时更新与发布。   首先创建更新温度信息的AdminTemperatureServlet。它包含一个名为Temperature初始化参数,其值为8。代码如程序3-27所示。   程序3-27:AdminTemperatureServlet.java    package com.servlet; … @WebServlet(name="AdminTemperatureServlet",urlPatterns={ "/adminTemperature" } , initParams ={ @WebInitParam(name = "Temperature", value ="8")}) public class AdminTemperatureServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=gb2312"); PrintWriter out = response.getWriter(); String Temperature=(String)getServletContext().getAttribute ("Temperature"); if(Temperature==null){ //获取初始化参数 Temperature=(String)getInitParameter("Temperature"); //放入应用上下文 getServletContext().setAttribute("Temperature", Temperature); } out.println("气温更新 " + ""); out.println(""); out.println("
"); out.println("

当前气温

"); out.print("
"); out.println("当前气温(摄氏度)"); out.println(""); out.println(""); out.println("
"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=gb2312"); PrintWriter out = response.getWriter(); String Temperature=request.getParameter("temperature"); //将更新后的气温信息放入上下文 getServletContext().setAttribute("Temperature",Temperature); out.println("气温更新 " + ""); out.println(""); out.println("当前气温:"+Temperature+"摄氏度"); out.println(""); } … }      下面生成显示气温信息的ShowTemperatureServlet,代码如程序3-28所示。   程序3-28:ShowTemperatureServlet.java    package com.servlet; … @WebServlet(name=" ShowTemperatureServlet ", urlPatterns={"/ showTemperature "}) public class ShowTemperatureServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=gb2312"); PrintWriter out = response.getWriter(); String Temperature=(String)getServletContext().getAttribute ("Temperature"); if(Temperature==null){Temperature=new String("0"); } String oldTemperature=(String)request.getSession().getAttribute ("OldTemperature"); out.println("气温信息显示 " + ""); out.println(""); out.println("当前最新气温:"+Temperature+"摄氏度"); if(oldTemperature!=null){ out.println("
"); out.println("更新当前气温:"+oldTemperature+"摄氏度"); } out.println(""); //更新会话中的气温信息 request.getSession().setAttribute("OldTemperature",Temperature); } … }      程序说明:AdminTemperatureServlet的doGet方法首先调用getServletContext方法获取应用的上下文对象ServletContext,然后调用ServletContext对象的getAttribute方法获取存储在上下文中的Temperature属性信息进行显示。如果是第一次调用,ServletContext中尚不存在Temperature属性对象,则调用getInitParameter方法获取存储在初始化参数中的气温信息并调用ServletContext的setAttribute方法将属性添加到上下文对象的Temperature属性对象中。ShowTemperatureServlet采用同样的方法获取上下文中的属性信息进行显示。   重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/ adminTemperature,将得到如图3-32所示的结果,此时显示的为从Servlet初始化参数中获取的气温信息。将文本框中的气温数据更改为8并单击“更新”按钮提交。打开一个新的浏览器对话框,在地址栏中输入http://localhost:8080/Chapter3/showTemperature,将得到如图3-33所示的结果,此时显示的为从上下文对象获取的气温信息,可以看到气温信息已经更新了。重新进入如图3-32所示的页面来不断更新气温信息,然后重新刷新显示如图3-33所示的页面,可以看到,通过上下文,两个Servlet组件之间的信息交换变得很方便。 图3-32 气温更新页面 图3-33 气温显示页面 3.9 Servlet间协作   当Web容器接收到客户端的请求后,它负责创建HttpRequest对象和HttpResponse对象,然后将这两个对象以参数的形式传递给与请求URL地址相关联的Servlet的service方法进行处理。但对于复杂的处理过程,仅仅通过一个Servlet来实现对于请求的处理往往比较困难,这时经常需要几个Servlet间共同协作完成对于请求的处理,也就是说,在一个Servlet处理过程中或处理完毕后,将客户端的请求传递到另外一个Servlet来处理,这种像接力赛似的过程称为请求指派。为实现请求指派,Servlet规范定义了一个接口:javax.servlet.requestdispatcher。   Requestdispatcher封装了到同一Web应用内的另外一个资源的引用。可以通过调用Requestdispatcher的forword方法将请求传递到其他资源,或者调用Requestdispatcher的include方法将其他资源对此请求的响应包含进来。   下面通过一个简单的登录系统来演示如何利用Requestdispatcher对象来实现Servlet间的协作。系统后台功能主要由三个Servlet来实现。Main为主控Servlet,用来实现登录验证功能,并根据验证结果将请求转发到LoginSuccess或LoginFail,LoginSuccess处理登录成功条件下的请求处理,LoginFail处理登录失败条件下的请求处理。   首先生成登录信息提交页面。代码如程序3-29所示。   程序3-29:dl.html    登录
欢迎登录系统
     程序说明:页面模拟一个系统登录页面,用户名和密码信息通过表单提交到后台的Servlet处理。   下面生成主控Servlet Main。代码如程序3-30所示。   程序3-30:Main.java    package example.servlet; … @WebServlet(name=" Main ", urlPatterns={"/ Main "}) public class Main extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userID=request.getParameter("userID"); if(userID==null)userID=""; String password=request.getParameter("password"); if(password==null)password=""; if((userID.equals("guest")&&password.equals("guest"))){ RequestDispatcher dispatcher = request.getRequestDispatcher("LoginSuccess"); dispatcher.forward(request, response); } else{ RequestDispatcher dispatcher = request.getRequestDispatcher("LoginFail"); dispatcher.forward(request, response); } } … }      程序说明:首先调用Request对象的getParameter方法来获取登录页面提交的信息,然后根据提交的信息进行登录验证(为表述简单,这里只是用它来与固定值guest进行对比)。通过调用HttpServletRequest对象的getRequestDispatcher方法来得到其他Web组件对应的RequestDispatcher对象,其中getRequestDispatcher方法的参数为被请求指派资源在部署描述文件中的URL地址。最后调用RequestDispatcher对象的forward方法,将请求导向其他Servlet组件。   下面生成登录成功条件下的指派资源 Servlet LoginSuccess和登录失败条件下的指派资源 Servlet LoginFail,代码如程序3-31和程序3-32所示。   程序3-31:LoginSuccess.java    package example.servlet; … @WebServlet(name=" LoginSuccess ", urlPatterns={"/ LoginSuccess "}) public class LoginSuccess extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); String name=request.getParameter("userID"); out.println(""); out.println(""); out.println("登录成功"); out.println(""); out.println(""); out.println("

欢迎!"+name+"您已成功登录系统...

"); out.println(""); out.println(""); out.close(); } … }      程序说明:作为登录验证成功的响应,显示一条欢迎信息。由于RequestDispatcher对象forward方法将前端的请求对象request传递到本Servlet,因此,依然可以调用request对象的getParameter("userID")方法来获取用户的登录ID。用户请求对象的生命周期直到服务器端向客户端返回响应时才宣告结束。   程序3-32:LoginFail.java    package example.servlet; … @WebServlet(name=" LoginFail ", urlPatterns={"/ LoginFail "}) public class LoginFail extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); RequestDispatcher dispatcher = request.getRequestDispatcher ("login.html"); dispatcher.include(request, response); } … }      程序说明:作为登录验证失败的响应,调用RequestDispatcher对象的include方法,将登录页面作为响应的一部分输出到客户端显示。   保存程序并重新发布Web应用,打开IE浏览器,在地址栏中输入http:/localhost:8080/ Chapter3/dl.html,得到如图3-34所示的运行结果页面。在“用户名”文本框输入guest,在“密码”文本框输入guest,单击“提交”按钮,则得到如图3-35所示的页面,可以看到请求被指派给了Servlet LoginSuccess。单击浏览器上的“后退”按钮,分别在“用户名”文本框和“密码”文本框输入其他数据并再次提交,则得到如图3-36所示的运行结果页面,可以看到请求被指派给了Servlet LoginFail。仔细观察图3-35和图3-36中浏览器的地址栏显示,可以看到地址栏中显示的是同一个地址,这是因为请求指派是在服务器端进行的,因此在客户端的浏览器上觉察不到。   在3.5节中了解到可以通过HttpServletResponse的sendRedirect实现请求重定向,那么它与调用RequestDispatcher的forward方法有什么区别呢?   首先,从操作的本质上,RequestDispatcher的forward是容器中控制权的转向,在客户端浏览器地址栏中不会显示出转向后的地址;而HttpServletResponse的sendRedirect是完全的跳转,浏览器将会得到跳转的地址,并重新发送请求连接。这样,从浏览器的地址栏中可以看到跳转后的链接地址。 其次,从性能上,前者仍旧是在同一次请求处理过程中,后者是结束第一次请求,由浏览器发起一次新的请求,因此,RequestDispatcher的forward更加高效。在条件许可时,开发人员尽量使用RequestDispatcher的forward方法。 图3-34 登录页面 图3-35 登录成功页面 图3-36 登录失败页面   RequestDispatcher的forward也有局限,它只能转到Web应用内部的资源,而在有些情况下,比如,需要跳转到其他服务器上的某个资源时,则必须使用HttpServletResponse的sendRedirect方法。   Servlet中有两种方式获得转发对象(RequestDispatcher):一种是通过HttpServletRequest的getRequestDispatcher方法获得,一种是通过ServletContext的getRequestDispatcher方法获得。   重定向的方法只有一种:HttpServletResponse的sendRedirect方法。   这三个方法的参数都是一个URL形式的字符串,但在使用相对路径或绝对路径上有所区别。   (1)HttpServletResponse.sendRedirect(String)。   参数可以指定为相对路径、绝对路径或其他Web应用。假设以http://localhost/myApp/ cool/from.do作为起点。   若采用相对路径,代码为response.sendRedirect(" foo/to.do "),则容器相对于原来请求URL的目录加上sendRedirect参数来生成完整的URL:http://localhost/myApp/cool/foo/to.do。   若采用绝对路径,代码为response.sendRedirect(" / foo/to.do "),容器相对于Web服务器本身加sendRedirect参数生成完整的URL:http://localhost/foo/to.do。   若参数为其他Web应用资源,如response.sendRedirect("http://www.javaeye.com"),则容器直接定向到该URL。   (2)HttpServletRequest.getRequestDispatcher(String)。   参数可以指定为相对路径或绝对路径。相对路径情况下生成的完整URL与重定向方法相同。绝对路径与重定向不同,容器将相对于Web应用的根目录加参数生成完整的URL,即:request.getRequestDispatcher("/foo/to.do")生成的URL是http://localhost/myApp/foo/to.do。   (3)ServletContext.getRequestDispatcher(String)。   参数只能指定为绝对路径,生成的完整URL与HttpServletRequest.getRequestDispatcher (String)相同。 3.10 Filter   Filter (过滤器)是Servlet 2.3 规范以后增加的新特性。Filter拦截请求和响应,以便查看、提取或以某种方式操作正在客户端和服务器之间交换的数据。Filter可以改变一个请求(Request)或者是修改响应(Response)。Filter与Servlet的关联由Web应用的配置描述文件或注解来明确。用户发送请求给Servlet时,在Servlet处理请求之前,与此Servlet关联的Filter首先执行,然后才是Servlet的执行,Servlet执行完毕又会回到Filter。如果一个Servlet有多个Filter,则根据配置的先后次序依次执行。   Filter主要用在以下几个方面:   (1)访问特定资源(Web页、JSP页、Servlet)时的身份验证。   (2)访问资源的记录跟踪。   (3)访问资源的转换。   一个Filter 必须实现javax.Servlet.Filter 接口,即实现下面的三个方法:   (1)doFilter(ServletRequest, ServletResponse, FilterChain)。用来实现过滤行为的方法。引入的 FilterChain对象提供了后续Filter所要调用的信息。   (2)init(FilterConfig)。由容器所调用的Filter初始化方法。容器确保在第一次调用 doFilter方法前调用此方法,一般用来获取在web.xml文件中指定的初始化参数。   (3)destroy()。容器在破坏Filter实例前,doFilter()中的所有活动都被该实例终止后,调用该方法。   下面演示如何利用Filter来记录Web组件对请求的响应时间。首先生成一个Filter。在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→“文件/文件夹”命令,弹出“新建文件”对话框,如图3-37所示。   在“类别”列表中选中Web,在“文件类型”列表中选中“过滤器”,单击“下一步”按钮,得到如图3-38所示的“New过滤器”对话框。   如图3-38所示,在“类名”文本框中输入Filter的名称TimeTrackFilter,在“包”文本框中输入包的名称com.servlet,单击“下一步”按钮,得到如图3-39所示的对话框。 图3-37 创建Filter 图3-38 Filter的名称和位置   在这里要配置过滤器的部署信息,即将过滤器与它要过滤的Web组件或URL模式关联起来。 图3-39 配置Filter部署   在“过滤器名称”文本框中可以输入过滤器的逻辑名称,这里采用默认选项。单击“编辑”按钮打开“过滤器映射”对话框来设置过滤器映射信息,如图3-40所示。      说明:过滤器有两种映射模式。一种是对URL模式的映射,这也是默认的映射模式。在URL模式中可以使用通配符号,如“/*”。另外一种模式是对Servlet的映射,这时过滤器关联的是Servlet的逻辑名称。 图3-40 “过滤器映射”对话框   选中单选按钮URL,并在其右侧的文本框中输入“/Main”。单击“确定”按钮完成过滤器映射配置。此时Filter关联的URL对应的组件为3.9节创建的Servlet Main。最后单击图3-39中的“完成”按钮,Filter创建完毕。完整代码如程序3-33所示。   程序3-33:TimeTrackFilter.java    … @WebFilter(filterName = "TimeTrackFilter", urlPatterns = {"/Main"}) public class TimeTrackFilter implements Filter { private FilterConfig filterConfig = null; public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; } public void destroy() { this.filterConfig = null; } public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException { Date startTime, endTime; double totalTime; StringWriter sw = new StringWriter(); System.out.println("我在Filter中"); startTime = new Date(); chain.doFilter(request, response); endTime = new Date(); totalTime = endTime.getTime() - startTime.getTime(); totalTime = totalTime ; System.out.println("我在Filter中"); PrintWriter writer = new PrintWriter(sw); writer.println("==============="); writer.println("耗时: " + totalTime + " 毫秒" ); writer.println("==============="); filterConfig.getServletContext(). log(sw.getBuffer().toString()); } }      程序说明:跟Servlet一样,在新版本的Java EE规范中,提供了注解WebFilter来部署Filter组件,其中属性filterName 为Filter的逻辑名称,属性 urlPatterns为Filter的URL模式。   程序包含了所有Filter必须实现的3个接口方法:init、destroy和doFilter。当容器第一次加载该过滤器时,init 方法将被调用。该类在这个方法中包含了一个指向 FilterConfig 对象的引用。对请求和响应的过滤功能主要由doFilter实现。Web容器在垃圾收集之前调用 destroy方法,以便能够执行任何必需的清理代码。   TimeTrackFilter主要实现对过滤的Web组件处理耗时的跟踪,在调用FilterChain对象的doFilter(request, response)方法之前创建一个Date对象startTime, FilterChain对象在doFilter(request, response)方法执行完毕后,控制权仍旧回到当前的Filter,此时,再创建一个Date对象endTime来获取当前时刻,二者相减,就得到被过滤Web组件的执行时间。   为了使执行效果更明显,可以修改Servlet Main,使Servlet的线程暂时中止2秒。修改后的代码如程序3-34所示。   程序3-34:Main.java    package com.servlet; … public class Main extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ System.out.println("我在Servlet Main中"); try{ Thread.sleep(2000); }catch (InterruptedException ie){ System.out.println(ie.toString()); } String userID=request.getParameter("userID"); … }      程序说明:斜体部分为新增的代码,即调用Thread对象的sleep方法使线程暂停2秒。   重新发布Web应用,打开IE浏览器,在地址栏中输入http://localhost:8080/Chapter3/ dl.html,得到如图3-34所示的运行结果页面。在“用户名”文本框输入guest,在“密码”文本框输入guest,单击“提交”按钮,此时浏览器向URL模式“/Main”发出请求,由于URL模式“/Main”被关联到Filter TimeTrackFilter,则首先执行TimeTrackFilter的doFilter方法,然后执行Servlet Main,最后又回到Filter的doFilter方法。因此在Netbeans底部的“输出”窗口的GlassFish Server 3.1.1中可以看到如图3-41的输出信息。 图3-41 Filter运行过程中的输出信息   从上面的演示过程可以看出,开发人员不应当把Filter看作是请求到达Servlet之前的一道防火墙,而应当把它看作是包裹在Servlet组件外面的一层防护网。在请求到达Servlet前后都会经过Filter的处理。   Filter不仅可以对URL模式进行过滤,还可以对Servlet组件的逻辑名称进行过滤。下面为TimeTrackFilter添加对Servlet组件 PDFServlet的过滤。打开web.xml,在编辑器中选中“过滤器”视图,得到如图3-42所示的运行界面。 图3-42 修改Filter配置信息   单击“过滤器映射”下的“添加”按钮,弹出“编辑过滤器映射”对话框,如图3-43所示。 图3-43 修改Filter配置信息   选中“Servlet名称”单选按钮,在其右边的下拉列表中选中要过滤的Servlet的名称PDFServlet,单击“确定”按钮,完成TimeTrackFilter对Servlet组件PDFServlet的过滤 设置。   重新发布Web应用,打开IE浏览器,在地址栏中输入http://localhost:8080/Chapter3/ pdfshow,在Netbeans底部的“输出”窗口的GlassFish Server 3.1.1视图中可以看到执行Servlet组件 PDFServlet的耗时信息。   在Servlet 2.4以上的Web容器中,过滤器可以根据请求分发器(request dispatcher)所使用的方法有条件地对Web请求进行过滤。在图3-43中,在“分发程序类型”组中可以看到四个检查框,分别代表以下分发类型: * REQUEST——只有当request直接来自客户,过滤器才生效。 * FORWARD——只有当request被一个请求分发器使用forward方法转到另一个Web组件时,过滤器才生效。 * INCLUDE——只有当request被一个请求分发器使用include方法转到一个Web构件时,过滤器才生效。 * ERROR——只有当request被一个请求分发器使用“错误信息页”机制方法转到一个Web组件时,过滤器才生效。   以上四个条件可以组合使用。   在图3-39中,选中Filter的映射项PDFServlet,单击“编辑”按钮来修改映射信息。在弹出的“编辑 过滤器映射”对话框中,选中FORWARD复选框,如图3-44所示。单击“确定”按钮,完成对过滤器映射信息的修改。 图3-44 修改Filter映射信息   重新发布Web应用,打开IE浏览器,在地址栏中输入http://localhost:8080/Chapter3/ pdfshow,在Netbeans底部的“输出”窗口的GlassFish Server 3.1.1视图中将看不到Filter输出的执行Servlet组件 PDFServlet的耗时信息。因为按照修改后的Filter映射设置,只有调用请求分发器的forward方法对Servlet组件 PDFServlet发送请求才被过滤器过滤。 3.11 Listener   Listener(监听器)是Servlet 2.4 规范以后增加的新特性。Listener用来主动监听Web容器事件。所谓Web容器事件,是指Web应用上下文创建销毁、会话对象创建销毁以及会话属性信息的增删改等。通过事件监听,Listener对象可以在事件发生前、发生后进行一些必要的处理。Listener实现了Web应用的事件驱动,使得Web应用不仅可以被动地处理客户端发出的请求,而且可以主动对Web容器的变化进行响应,大大提高了Web应用的能力。   为了实现事件监听功能,Listener必须实现Listener接口,同时,代表Web容器事件的Event类作为参数传递到Listener接口,Listener可以通过它来对Web容器事件进行必要的处理。   目前Servlet规范共有7个Listener接口和5个Event类,Event类与Listener之间的关系如表3-3所示。 表3-3 Servlet规范中支持的Listener接口和Event类 Listener接口 Event类 ServletContextListener      ServletContextEvent ServletContextAttributeListener      ServletContextAttributeEvent HttpSessionListener      HttpSessionEvent HttpSessionActivationListener       HttpSessionAttributeListener      HttpSessionBindingEvent HttpSessionBindingListener       javax.servlet.AsyncListener      AsyncEvent      注:新的Java EE规范不再支持ServletRequestListener和ServletRequestAttributeListener接口。      1.ServletContextListener和ServletContextEvent   ServletContextEvent用来代表Web 应用上下文事件。ServletContextListener用于监听Web 应用上下文事件。当Web应用启动时 ServletContext 被创建或当Web应用关闭时 ServletContext 将要被销毁,Web容器都将发送ServletContextEvent事件到实现了ServletContextListener接口的对象实例。   实现ServletContextListener接口的实例必须实现以下接口方法: * void contextInitialized(ServletContextEvent sce)——通知Listener对象,Web应用已经被加载及初始化。 * void contextDestroyed(ServletContextEvent sce)——通知Listener对象,Web应用已经被销毁。   ServletContextEvent中最常用的方法为: * ServletContext getServletContext()——取得ServletContext对象。   Listener通常利用此方法来获取Servlet上下文信息,然后进行相应的处理。   2.ServletContextAttributeListener和ServletContextAttributeEvent   ServletContextAttributeEvent代表Web上下文属性事件,它包括增加属性、删除属性、修改属性等。ServletContextAttributeListener用于监听上述Web上下文属性事件。   ServletContextAttributeListener接口主要有以下方法: * void attributeAdded(ServletContextAttributeEvent scab)——当有对象加入Application的范围,通知Listener对象。 * void attributeRemoved(ServletContextAttributeEvent scab)——若有对象从Application的范围移除,通知Listener对象。 * void attributeReplaced(ServletContextAttributeEvent scab)——若在Application的范围中,有对象取代另一个对象时,通知Listener对象。   ServletContextAttributeEvent中常用的方法如下: * java.lang.String getName()——返回属性的名称。 * java.lang.Object getValue()——返回属性的值。   3.HttpSessionBindingListener和HttpSessionBindingEvent   HttpSessionBindingEvent代表会话绑定事件。当对象加入Session范围(即调用HttpSession对象的setAttribute方法的时候)或从Session范围中移除(即调用HttpSession对象的removeAttribute方法的时候或Session Time out的时候)时,都将触发该事件,此时,Web容器将发送消息给实现了HttpSessionBindingListener接口的对象。   实现了HttpSessionBindingListener接口的对象必须实现以下两个方法: * void valueBound(HttpSessionBindingEvent event)——通知Listener,有新的对象加入Session。 * void valueUnbound(HttpSessionBindingEvent event)——通知Listener,有对象从Session中删除。   4.HttpSessionAttributeListener和HttpSessionBindingEvent   HttpSessionAttributeListener也是用来监听HttpSessionBindingEvent事件,但是实现HttpSessionAttributeListener接口的对象必须实现以下不同的接口方法: * attributeAdded(HttpSessionBindingEvent se)——当在Session增加一个属性时,Web容器调用此方法。 * attributeRemoved(HttpSessionBindingEvent se)——当在Session删除一个属性时,Web容器调用此方法。 * attributeReplaced(HttpSessionBindingEvent se)——当在Session属性被重新设置时,Web容器调用此方法。      说明:HttpSessionAttributeListener和HttpSessionBindingListener的区别是HttpSession- AttributeListener是从会话的角度去观察,而HttpSessionBindingListener是从对象绑定的角度来观察。当会话超时或无效时,对象会从会话解除绑定。此时HttpSessionBindingListener会得到通知,而HttpSessionAttributeListener则不会。      5.HttpSessionListener和HttpSessionEvent   HttpSessionEvent代表HttpSession对象的生命周期事件包括HttpSession对象的创建、销毁等。HttpSessionListener用来对HttpSession对象的生命周期事件进行监听。   实现HttpSessionListener接口的对象必须实现以下接口方法: * sessionCreated(HttpSessionEvent se)——当创建一个Session时,Web容器调用此方法。 * sessionDestroyed(HttpSessionEvent se)——当销毁一个Session时,Web容器调用此方法。   6.HttpSessionActivationListener接口   主要用于服务器集群的情况下,同一个Session转移至不同的JVM的情形。此时,实现了HttpSessionActivationListener接口的Listener将被触发。   7.AsyncListener   对Servlet异步处理事件提供监听,实现AsyncListener接口的对象必须实现以下接口方法: * onStartAsync(AsyncEvent event)——异步线程开始时,Web容器调用此方法。 * onError(AsyncEvent event)——异步线程出错时,Web容器调用此方法。 * onTimeout(AsyncEvent event)——异步线程执行超时,Web容器调用此方法。 * onComplete(AsyncEvent event)——异步线程执行完毕时,Web容器调用此方法。   要注册一个AsyncListener,只需将准备好的AsyncListener对象作为参数传递给 AsyncContext对象的addListener()方法即可,如下列代码片段所示:    AsyncContext ctx = req.startAsync(); ctx.addListener(new AsyncListener() { public void onComplete(AsyncEvent asyncEvent) throws IOException { // 做一些清理工作或者其他 } ... });      利用上述七类Listener接口,Web应用实现了对Web容器的会话以及应用上下文层面上的事件的监听处理。   除HttpSessionBindingListener接口和AsyncListener接口,其他所有关于Listener的配置信息都存储在Web应用的部署描述文件Web.xml中,Web容器通过此文件中的信息来决定当某个特定事件发生时,将自动创建对应的Listener对象的实例并调用相应的接口方法进行处理。   下面通过创建一个网站计数器来演示如何应用Listener来开发Web应用。网站计数器要求满足以下功能:   (1)统计应用自部署以来的所有用户访问次数。   (2)对于用户在一次会话中的访问只记录一次,以保证数据的真实性。   (3)统计在线用户数量信息。   由于网站计数器要记录应用自部署以来的所有用户访问次数,因此,必须将用户访问信息实现持久化。由于用户访问信息比较简单,因此,可以将信息持久化存储到外部的资源文件中,而不是数据库。   利用ServletContextListener控制历史计数信息的读取和写入。当Web应用上下文创建时,将历史计数信息从外部资源文件读取到内存。当Web应用上下文关闭时,则将历史计数信息持久化存储到外部资源文件中。   利用HttpSessionListener监听在线用户数量变化。每创建一个新的会话,则代表产生一次新的用户访问。每一个会话销毁事件,则代表一位用户离线。这种方式也避免了用户重复刷新导致的重复计数。   首先在Web应用的文件夹“Web页”下建立一个名为Count.txt的空文件,用来存储历史访问数据。   下面在Web应用Chapter3中创建一个辅助工具类CounterFile来实现对资源文件的操作。代码如程序3-35所示。   程序3-35:CounterFile.java    package com.example; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; //用来操作记录访问次数的文件 public class CounterFile { private BufferedReader file; //BufferedReader对象,用于读取文件数据 public CounterFile() { } //ReadFile方法用来读取文件filePath中的数据,并返回这个数据 public String ReadFile(String filePath) throws FileNotFoundException { String currentRecord = null;//保存文本的变量 //创建新的BufferedReader对象 file = new BufferedReader(new FileReader(filePath)); String returnStr =null; try { //读取一行数据并保存到currentRecord变量中 currentRecord = file.readLine(); } catch (IOException e) {//错误处理 System.out.println("读取数据错误."); } if (currentRecord == null) //如果文件为空 returnStr = "没有任何记录"; else {//文件不为空 returnStr =currentRecord; } //返回读取文件的数据 return returnStr; } //ReadFile方法用来将数据counter+1后写入到文本文件filePath中 //以实现计数增长的功能 public synchronized void WriteFile(String filePath,String counter) throws FileNotFoundException { int Writestr = 0; Writestr=Integer.parseInt(counter); try { //创建PrintWriter对象,用于写入数据到文件中 PrintWriter pw = new PrintWriter(new FileOutputStream(filePath)); //用文本格式打印整数Writestr pw.println(Writestr); //清除PrintWriter对象 pw.close(); } catch(IOException e) { //错误处理 System.out.println("写入文件错误"+e.getMessage()); } } }      程序说明:程序用来实现对资源文件Count.txt的读写操作。其中方法ReadFile(String filePath)负责将信息从资源文件读取到内存,方法WriteFile(String filePath,String counter)负责将计数信息写回到资源文件。为了避免写文件时发生线程安全问题,这里将方法WriteFile用修饰符synchronized加以保护。   下面创建一个ServletContextListener来实现对Web应用创建、销毁事件的监听,以便在Web应用创建或销毁时从资源文件载入或回写历史访问数据。在“项目”视图中选中Web应用Chapter3,右击,在弹出的快捷菜单中选择“新建”→“Web应用程序监听程序”命令,弹出“New Web应用程序监听程序”对话框,如图3-45所示。 图3-45 “New Web应用程序监听程序”对话框   在“类名”文本框中输入辅助工具类名CountListener,在“包”文本框中输入Servlet类所在的包名com.servlet,在“要实现的接口”列表中选中“上下文监听程序”,单击“完成”按钮,NetBeans自动生成类CountListener的框架源文件。代码如程序3-36所示。   程序3-36:CounterListener.java    package com.example; import javax.servlet.ServletContextListener; import javax.servlet.ServletContextEvent; public class CounterListener implements ServletContextListener { String path=""; public void contextInitialized(ServletContextEvent evt) { CounterFile f=new CounterFile(); String name=evt.getServletContext().getInitParameter("CounterPath"); path=evt.getServletContext().getRealPath(name); try{ String temp=f.ReadFile(path); System.out.println(temp); //将计数器的值放入应用上下文 evt.getServletContext().setAttribute("Counter",temp); }catch(Exception e){ System.out.println(e.toString()); } } public void contextDestroyed(ServletContextEvent evt) { try{ String current= (String)evt.getServletContext().getAttribute ("Counter"); CounterFile f=new CounterFile(); f.WriteFile(path,current); }catch(Exception e){ System.out.println(e.toString()); } } }      程序说明:程序调用辅助工具类CounterFile来操作资源文件。在contextInitialized (ServletContextEvent evt)方法中将历史计数信息从外部读入到内存,在contextDestroyed(ServletContextEvent evt)方法中将历史计数信息持久化储存到外部文件。这样,在程序运行期间,不管应用的访问次数多么频繁,所用计数操作只是操作内存中的变量,应用程序的性能将不会因为频繁的IO操作而下降。   计数器文件的路径信息是从Web应用的上下文中读取的,因此在运行程序之前,要在Web应用上下文中添加一个名为CounterPath的上下文参数。打开Web.xml,切换到“常规”视图,单击“上下文参数”条目下的“添加”按钮,弹出“添加 上下文参数”对话框,如图3-46所示。   在“参数名称”文本框中输入CounterPath,在“参数值”中输入count.txt,单击“确定”按钮,Web上下文参数添加完毕。查看web.xml的源代码,可以看到添加的上下文参数和监听器CounterListener配置信息如程序3-37中斜体部分所示。 图3-46 “添加 上下文参数”对话框   程序3-37:Web.xml(部分)    … CounterPath count.txt ServletContextListener com.servlet.CounterListener …      为了监听在线用户数目,还要创建一个实现HttpSessionListener接口的Listener。在“项目”视图中选中Web应用Chapter 3,右击,在弹出的快捷菜单中选择“新建”→“Web应用程序监听程序”,弹出“New Web应用程序监听程序”对话框,如图3-47所示。 图3-47 “New Web应用程序监听程序”对话框   在“类名”文本框中输入类名SessionListener,在“包”文本框中输入Servlet类所在的包名com.servlet,在“要实现的接口”列表中选中“HTTP会话监听程序”,单击“完成”按钮, Netbeans 自动生成类SessionListener的框架源文件。完整源代码如程序3-38所示。   程序3-38:SessionListener.java    package com.example; import javax.servlet.http.HttpSessionListener; import javax.servlet.http.HttpSessionEvent; @WebListener( ) public class SessionListener implements HttpSessionListener { public void sessionCreated(HttpSessionEvent evt) { // 修改在线人数 String current= (String)evt.getSession().getServletContext(). getAttribute("online"); if(current==null)current="0"; int c=Integer.parseInt(current); c++; current=String.valueOf(c); evt.getSession().getServletContext().setAttribute("online", current); //修改历史人数 String his= (String)evt.getSession().getServletContext(). getAttribute("Counter"); if(his==null)his="0"; int total=Integer.parseInt(his)+1; his=String.valueOf(total); evt.getSession().getServletContext().setAttribute("Counter",his); } public void sessionDestroyed(HttpSessionEvent evt) { // TODO 在此处添加您的代码: // 修改在线人数 String current= (String)evt.getSession().getServletContext().getAttribute ("online"); if(current==null)current="0"; int c=Integer.parseInt(current); c--; current=String.valueOf(c); evt.getSession().getServletContext().setAttribute("online",current); } }      程序说明:程序通过监听会话事件来维护在线人数和历史访问次数信息。在sessionCreated(HttpSessionEvent evt)方法中,从Web应用上下文中获取历史计数信息和在线人数信息,并分别增加1。在sessionDestroyed(HttpSessionEvent evt)方法中,从Web应用上下文中获取在线人数信息,并减1。   最后创建一个Servlet来显示在线用户数量以及历史用户数量。代码如程序3-39所示。   程序3-39:Counter.java    package com.servlet; … public class Counter extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); String dumb=(String)request.getSession().getAttribute("dumb"); //触发session事件 String history =(String)getServletContext().getAttribute("Counter"); if( history==null) history="0"; String temp =(String)getServletContext().getAttribute("online"); if(temp==null)temp="0"; out.println(""); out.println(""); out.println("计数器"); out.println(""); out.println(""); out.println("

当前访问人数: " + temp + "

"); out.println("

历史访问人数: " + history + "

"); out.println(""); out.println(""); out.close(); } … }      程序说明:从Web应用上下文属性中获取历史访问次数和在线人数信息,然后通过Servlet输出到页面。   程序发布成功后,在浏览器地址栏输入http://localhost:8080/Chapter3/Counter,将得到如图3-48所示的运行页面。 图3-48 网站计数器显示信息 小 结   Servlet作为最核心的Java EE Web组件,在Java EE编程开发中具有重要的地位,而且是后面学习JSP编程的基础。业界流行的一些框架如Struts、Spring等都是在Servlet组件的基础上进行封装扩展后实现的。在理解Servlet的基本概念和工作原理的基础上,必须熟练运用Servlet编程的基本技能包括请求处理、响应生成和参数配置等,并掌握会话管理、Servlet间协同、Servlet上下文等高级编程技巧,为后续内容的学习打下基础。 习 题 3   1.什么是Servlet?与Applet有何异同?   2.详细论述Servlet的工作流程。   3.论述客户端请求、会话与Servlet上下文之间的关联。   4.上机实现本章中的所有例程。   5.利用Servlet的会话跟踪机制实现购物车应用。   6.利用Servlet实现一个简单的聊天室,并利用Listener动态显示在线用户列表。   7.编写一个登录验证的Filter实现禁止不通过登录就访问系统。