
3.5 生成响应
Servlet的核心职责就是根据客户端的请求来生成动态响应。在ServletResponse接口中定义了一系列与生成响应结果相关的方法,如表3-2所示。
表3-2 ServletResponse接口的主要方法

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("<html><head><title>HTTP2 Test</title> <link rel=\"stylesheet\" href=\"my.css\"></head> <body>Hello</body></html>"); } }
程序说明:调用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运行结果