书名:Servlet、JSP和Spring MVC初学指南
ISBN:978-7-115-42974-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [加] Budi Kurniawan [美] Paul Deck
译 林仪明 俞黎敏
责任编辑 陈冀康
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Simplified Chinese translation copyright © 2016 by Posts and Telecommunications Press
ALL RIGHTS RESERVED
Servlet,JSP and Spring MVC A Tutorial,by Budi Kurniawan and Paul Deck
Copyright © 2015 by Brainy Software Inc.
本书中文简体版由Brainy Software授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
Servlet和JSP是开发Java Web应用程序的两种基本技术。Spring MVC是Spring框架中用于Web应用快速开发的一个模块,是当今最流行的Web开发框架之一。
本书是Servlet、JSP和Spring MVC的学习指南。全书内容分为两个部分,第一部分主要介绍Servlet和JSP基础知识和技术,包括第1章至第15章;第2部分主要介绍Spring MVC,包括第16章至第24章。最后,附录部分给出了Tomcat安装和配置指导,还介绍了Servlet and JSP注解以及SSL证书。
本书内容充实、讲解清晰,非常适合Web开发者尤其是基于Java的Web应用开发者阅读。
Java Servlet技术简称Servlet技术,是Java开发Web应用的底层技术。由Sun公司于1996年发布,用来代替CGI——当时生成Web动态内容的主流技术。CGI技术的主要问题是每个Web请求都需要新启动一个进程来处理。创建进程会消耗不少CPU周期,导致难以编写可扩展的CGI程序。而Servlet有着比CGI程序更好的性能,因为Servlet在创建后(处理第一个请求时)就一直保持在内存中。此后,SUN公司发布了JavaServer Pages(JSP)技术,以进一步简化servlet程序开发。
自从Servlet和JSP技术诞生后,涌现出大量的基于Java的Web框架来帮助开发人员快速编写Web应用。这些框架构建于Servlet和JSP之上,帮助开发人员更加关注业务逻辑,无须编写重复性(技术)代码。目前,Spring MVC是最为流行的可扩展Java Web应用开发框架。
Spring MVC又叫Spring Web MVC,是Spring框架的一个模块,用于快速开发Web应用。MVC代表Model-View-Controller,是一个广泛应用于GUI开发的设计模式。该模式不局限于Web开发,也广泛应用在桌面开发技术上,如Java Swing和JavaFX。
下面将简要介绍HTTP、基于Servlet和JSP的Web编程,以及本书的章节内容编排。
注意
本书中所有示例代码基于Servlet 3.0、JSP 2.3以及Spring MVC 4。本书假定读者已有Java以及面向对象编程基础。对于Java新手,我们建议阅读由Budi Kurniawan编写的《Java : A Beginner’s Tutorial (Fourth Edition)》(ISBN 9780992133047)一书。
Servlet是一个Java程序,一个Servlet应用有一个或多个Servlet程序。JSP页面会被转换和编译成Servlet程序。
Servlet应用无法独立运行,必须运行在Servlet容器中。Servlet容器将用户的请求传递给Servlet应用,并将结果返回给用户。由于大部分Servlet应用都包含多个JSP页面,因此更准确地说是“Servlet/JSP应用”。
Web用户通过Web浏览器例如IE、Mozilla Firefox或者谷歌Chrome来访问Servlet应用。通常,Web浏览器又叫Web客户端。
图I.1展示了Servlet/JSP应用的架构。
图I.1 Servlet/JSP应用架构
Web服务器和Web客户端间通过HTTP协议通信,因此Web服务器也叫HTTP服务器。下面会详细讨论HTTP协议。
Servlet/JSP容器是一个可以同时处理Servlet和静态内容的Web容器。过去,由于通常认为HTTP服务器比Servlet/JSP容器更加可靠,因此人们习惯将Servlet/JSP容器作为HTTP服务器如Apache HTTP服务器的一个模块。这种模式下,HTTP服务器用来处理静态资源,而Servlet/JSP容器则负责生成动态内容。如今,Servlet/JSP容器更加成熟可靠,并被广泛地独立部署。Apache Tomcat和Jetty是当前最流行的Servlet/JSP容器,并且它们是免费而且开源的。你可以访问http://tomcat.apache.org 以及http://www.eclipse.org/jetty 下载。
Servlet和JSP只是Java企业版中众多技术中的两个,其他Java EE技术还有Java消息服务,企业Java对象、JavaServer Faces以及Java持久化等,完整的Java EE技术列表可以访问如下地址:
http://www.oracle.com/technetwork/java/javaee/tech/index.html
要运行Java EE应用,需要一个Java EE容器,例如GlassFish、JBoss、Oracle Weblogic或者IBM WebSphere。诚然,我们可以将一个Servlet/JSP应用部署到一个Java EE容器上,但一个Servlet/JSP容器就已经满足需要了,并且更加轻量。当然,Tomcat和Jetty不是Java EE容器,因此无法运行EJB或JMS技术。
HTTP协议使得Web服务器与浏览器之间可以通过互联网或内网进行数据交互。万维网联盟(W3C),作为一个制定标准的国际社区,负责和维护HTTP协议。HTTP第一版是0.9,之后是HTTP 1.0,当前最新版本是HTTP 1.1。HTTP 1.1版本的RFC编号是2616,下载地址为http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf。按计划,HTTP的下一个版本是HTTP/2。
Web服务器7×24小时不间断运行,并等待HTTP客户端(通常是Web浏览器)来连接并请求资源。通常,由客户端发起一个连接,服务端不会主动连接客户端。
注意:
2011年,标准化组织IETF发布了WebSocket协议,即RFC 6455规范。该协议允许一个HTTP连接升级为WebSocket连接,支持双向通信,这就使得服务端可以通过WebSocket协议主动发起同客户端的会话通信。
互联网用户需要通过点击或者输入一个URL链接或地址来访问一个资源,如下为两个示例:
http://google.com/index.html
http://facebook.com/index.html
URL的第一个部分是http,代表所采用的协议。除HTTP协议外,URL还可以采用其他类型的协议,如下为两个示例:
mailto:joe@example.com
ftp://marketing@ftp.example.org
通常,HTTP的URL格式如下:
protocol://[host.]domain[:port][/context][/resource][?query string]
或者
protocol://IP address[:port][/context][/resource][?query string]
中括号中的内容是可选的,因此一个最简的URL是http://yahoo.ca 或者http://192.168.1.9 。
需要说明的是,除了输入http://google.com,你还可以用http://209.85.143.99来访问谷歌。可以用ping命令来获取域名所对应的IP地址:
ping google.com
由于IP地址不容易记忆,实践中更倾向于使用域名。一台计算机可以托管不止一个域名,因此不同的域名可能指向同一个IP。另外,example.com或者example.org无法被注册,因为它们被保留作为各类文档手册举例使用。
URL中的Host部分用来表示在互联网或内网中一个唯一的地址,例如:http://yahoo.com(没有host)所访问的地址完全不同于http://mail.yahoo.com (有host)。多年以来,作为最受欢迎的主机名,www是默认的主机名,通常,http://www.domainName 会被映射到http://domainName 。
HTTP的默认端口是80端口。因此,对于采用80端口的Web服务器,可以无须输入端口号。但有时候,Web服务器并未运行在80端口上,此时必须输入相应的端口号。例如:Tomcat服务器的默认端口号是8080,为了能正确访问,必须提供输入端口号:
http://localhost:8080
localhost作为一个保留关键字,用于指向本机。
URL中的context部分用来代表应用名称,该部分也是可选的。一台Web服务器可以运行多个上下文(应用),其中一个可以配置为默认上下文,对于访问默认上下文中的资源,可以跳过context部分。
最后,一个context可以有一个或多个默认资源(通常为index.html,index.htm或者default.htm)。一个没有带资源名称的URL通常指向默认资源。当存在多个默认资源时,其中最高优先级的资源将被返回给客户端。
在资源名之后可以有一个或多个查询语句或者路径参数。查询语句是一个Key/Value组,多个查询语句间用“&”符号分隔。路径参数类似于查询语句,但只有value部分,多个value部分用“/”符号分隔。
一个HTTP请求包含三部分内容:
如下为一个具体示例:
POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language:en-gb
Connection:Keep-Alive
Host:localhost
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36
➥ (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36
Content-Length:30
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
lastName=Blanks&firstName=Mike
请求的第一行即是:方法-URI-协议/版本
POST /examples/default.jsp HTTP/1.1
请求方法为POST,URI为/examples/default.jsp,而协议/版本为HTTP/1.1。
HTTP 1.1规范定义了7种类型的方法,包括GET、POST、HEAD、OPTIONS、PUT、DELETE以及TRACE,其中GET和POST广泛应用于互联网应用。
URI定义了一个互联网资源,通常解析为服务器根目录的相对路径。因此,通常用/符号打头。另外URL是URI的一个具体类型。(详见http://www.ietf.org/rfc/rfc2396.txt。)
HTTP请求所包含的请求头信息包含关于客户端环境以及实体内容等非常有用的信息。例如,浏览器所设置的语言实体内容长度等。每个header用回车/换行(即CRLF)分隔。
HTTP请求头信息和请求正文用一行空行分隔,HTTP服务器据此判断请求正文的起始位置。因此在一些关于互联网的书籍中,CRLF作为HTTP请求的第四种组件。
在此前所举的例子中,请求正文如下行:
lastName=Blanks&firstName=Mike
在正常的HTTP请求中,请求正文的内容不止如此。
同HTTP请求一样,HTTP响应包含三部分:
如下是一个HTTP响应实例:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Date: Thu, 8 Jan 2015 13:13:33 GMT
Content-Type: text/html
Last-Modified: Wed, 7 Jan 2015 13:13:12 GMT
Content-Length: 112
<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>
类似于HTTP请求报文,HTTP响应报文第一行说明了HTTP协议的版本是1.1,并且请求结果是成功的(状态代码200为响应成功)。
同HTTP请求报文头信息一样,HTTP响应报文头信息也包含了大量有用的信息。HTTP响应报文的响应正文是HTML文档。HTTP响应报文的头信息和响应正文也是用CRLF分隔的。
状态代码200表示Web服务器能正确响应所请求的资源。若一个请求的资源不能被找到或者理解,则Web服务器将返回不同的状态代码。例如:访问未授权的资源将返回401,而使用被禁用的请求方法将返回405。完整的HTTP响应状态代码列表详见如下网址:
http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
第1章:“Servlets”,介绍Servlet API,本章重点关注两个java包:javax.servlet 和javax.servlet.http packages。
第2章:“会话管理”,讨论了会话管理——在Web应用开发中非常重要的主题(因为HTTP是无状态的),本章比较了4种不同的状态保持技术:URL重写、隐藏域、Cookies和HTTPSession对象。
第3章:“JavaServer Pages(JSP)”,JSP是Servlet技术的补充完善,是Servlet技术的重要组成部分,本章包括了JSP语法、指令、脚本元素和动作。
第4章:“表达式语言”,本章介绍了JSP 2.0中最重要的特性“表达式语言”。该特性的目标是帮助开发人员编写无脚本的JSP页面,让JSP页面更加简洁而且有效。本章将帮助你学会通过EL来访问Java Bean和上下文对象。
第5章:“JSTL”,本章介绍了JSP技术中最重要的类库:标准标签库——一组帮助处理常见问题的标签。具体内容包括访问Map或集合对象、条件判断、XML处理,以及数据库访问和数据处理。
第6章:“自定义标签”,大多数时候,JSTL用于访问上下文对象并处理各种任务,但对于特定的任务,我们需要编写自定义标签,本章将介绍如何编写标签。
第7章:“标签文件”,本章介绍在JSP 2.0中引入的新特性——标签文件,标签文件可以简化自定义标签的编写。
第8章:“监听器”,本章介绍了Servlet中的事件驱动编程,展示了Servlet API中的事件类以及监控器接口,以及如何应用。
第9章:“Filters”,本章介绍了Filter API,包括Filter、FilterConfig和FilterChain接口,并展示了如何编写一个Filter实现。
第10章:“修饰Requests和Responses”,本章介绍如何用修饰器模式来包装Servlet请求和响应对象,并改变Servlet请求和响应的行为。
第11章:“异步处理”,本章主要讨论Servlet 3.0引入的新特性——异步处理。该特性非常适合于当Servlet应用负载较高且有一个或多个耗时操作。该特性允许由一个新线程来运行耗时操作,使得当前的Web请求处理线程可以处理新的Web请求。
第12章:“安全”,介绍了如何通过声明式以及编程式来保护Java Web应用,本章覆盖四个主题:认证、授权、加密和数据完整性。
第13章:“部署”,介绍了Servlet/JSP应用的部署流程,以及部署描述符。
第14章:“动态加载以及Servlet容器加载器”介绍了Servlet 3.0中的两个新特性,动态注册支持在无须重启Web应用的情况下注册新的Web对象,以及框架开发人员最关心的容器初始化。
第15章:“Spring框架”,介绍了最流行的开源框架。
第16章:“模型2和MVC模式”,讨论了Spring MVC所实现的设计模式。
第17章:“Spring MVC介绍”,Spring MVC概述。本章编写了第一个Spring MVC应用。
第18章:“基于注解的控制器”,讨论了MVC模式中最重要的一个对象—控制器。本章,我们将学会如何编写基于注解的控制器,这是Spring MVC 2.5版本引入的方法。
第19章:“数据绑定和表单标签库”,讨论Spring MVC最强大的一个特性,并利用它来展示表单数据。
第20章:“转换器和格式化”,讨论了数据绑定的辅助对象类型。
第21章:“验证器”,本章将展示如何通过验证器来验证用户输入数据。
第22章:“国际化”,本章将展示如何用Spring MVC来构建多语言网站。
第23章:“上传文件”,介绍两种不同的方式来处理文件上传。
第24章:“下载文件”,介绍如何用编程方式向客户端传输一个资源。
附录A:“Tomcat”,介绍如何安装和配置Tomcat。
附录B:“Web Annotations”,列出所有可用配置Web对象,如Servlet、Listener或Filter的注解。这些来自Servlet 3.0规范的注解可以帮助减少部署描述配置。
附录C:“SSL证书”,介绍了如何用KeyTool工具生成公钥/私钥对,并生成数字证书。
本书所有的示例应用压缩包可以通过如下地址下载:
http://books.brainysoftware.com/download
Servlet API是开发Servlet的主要技术。掌握Servlet API是成为一名强大的Java web开发者的基本条件,你必须熟悉Servlet API中定义的核心接口和类。
本章介绍了Servlet API,并教你如何编写第一个Servlet。
Servlet API有以下4个Java包:
本章主要关注javax.servlet和javax.servlet.http的成员。
图1.1中展示了javax.servlet中的主要类型。
图1.1 javax.servlet中的主要类型
Servlet技术的核心是Servlet,它是所有Servlet类必须直接或间接实现的一个接口。在编写实现Servlet的Servlet类时,直接实现它。在扩展实现这个接口的类时,间接实现它。
Servlet接口定义了Servlet与Servlet容器之间的契约。这个契约归结起来就是,Servlet容器将Servlet类载入内存,并在Servlet实例上调用具体的方法。在一个应用程序中,每种Servlet类型只能有一个实例。
用户请求致使Servlet容器调用Servlet的Service方法,并传入一个ServletRequest实例和一个ServletResponse实例。ServletRequest中封装了当前的HTTP请求,因此,Servlet开发人员不必解析和操作原始的HTTP数据。ServletResponse表示当前用户的HTTP响应,使得将响应发回给用户变得十分容易。
对于每一个应用程序,Servlet容器还会创建一个ServletContext实例。这个对象中封装了上下文(应用程序)的环境详情。每个上下文只有一个ServletContext。每个Servlet实例也都有一个封装Servlet配置的ServletConfig。
下面来看Servlet接口。上面提到的其他接口,将在本章的其他小节中讲解。
Servlet接口中定义了以下5个方法:
void init(ServletConfig config) throws ServletException
void service(ServletRequest request, ServletResponse response)
throws ServletException, java.io.IOException
void destroy()
java.lang.String getServletInfo()
ServletConfig getServletConfig()
注意,编写Java方法签名的惯例是,对于与包含该方法的类型不处于同一个包中的类型,要使用全类名。正因为如此,在Service方法javax.servlet.ServletException的签名中(与Servlet位于同一个包中)是没有包信息的,而java.io.Exception则是编写完整的名称。
init、service和destroy是生命周期方法。Servlet容器根据以下规则调用这3个方法:
Servlet中的另外两个方法是非生命周期方法,即getServletInfo和getServletConfig:
注意线程安全性。Servlet实例会被一个应用程序中的所有用户共享,因此不建议使用类级变量,除非它们是只读的,或者是java.util.concurrent.atomic包的成员。
1.3节将介绍如何编写Servlet实现。
其实,编写Servlet应用程序出奇简单。只需要创建一个目录结构,并把Servlet类放在某个目录下。本节将教你如何编写一个名为app01a的Servlet应用程序。最初,它会包含一个Servlet,即MyServlet,其效果是向用户发出一条问候。
要运行Servlets,还需要一个Servlet容器。Tomcat是一个开源的Servlet容器,它是免费的,并且可以在任何能跑Java的平台上运行。如果你到现在都还没有安装Tomcat,就应该去看看附录A,并安装一个。
确定你的机器上有了Servlet容器后,下一步就要编写和编译一个Servlet类。本例中的Servlet类是MyServlet,如清单1.1所示。按照惯例,Servlet类的名称要以Servlet作为后缀。
清单1.1 MyServlet类
package app01a;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
@WebServlet(name = "MyServlet", urlPatterns = { "/my" })
public class MyServlet implements Servlet {
private transient ServletConfig servletConfig;
@Override
public void init(ServletConfig servletConfig)
throws ServletException {
this.servletConfig = servletConfig;
}
@Override
public ServletConfig getServletConfig() {
return servletConfig;
}
@Override
public String getServletInfo() {
return "My Servlet";
}
@Override
public void service(ServletRequest request,
ServletResponse response) throws ServletException,
IOException {
String servletName = servletConfig.getServletName();
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head></head>"
+ "<body>Hello from " + servletName
+ "</body></html>");
}
@Override
public void destroy() {
}
}
看到清单1.1中的代码时,可能首先注意到的是下面这个标注:
@WebServlet(name = "MyServlet", urlPatterns = { "/my" })
WebServlet标注类型用来声明一个Servlet。命名Servlet时,还可以暗示容器,是哪个URL调用这个Servlet。name属性是可选的,如有,通常用Servlet类的名称。重要的是urlPatterns属性,它也是可选的,但是一般都是有的。在MyServlet中,urlPatterns告诉容器,/my样式表示应该调用Servlet。
注意,URL样式必须用一个正斜杠开头。
Servlet的init方法只被调用一次,并将private transient变量ServletConfig设为传给该方法的ServletConfig对象:
private transient ServletConfig servletConfig;
@Override
public void init(ServletConfig servletConfig)
throws ServletException {
this.servletConfig = servletConfig;
}
如果想通过Servlet内部使用ServletConfig,只需要将被传入的ServletConfig赋给一个类变量。
Service方法发送字符串“Hello from MyServlet”给浏览器。对于每一个针对Servlet进来的HTTP请求,都会调用Service方法。
为了编译Servlet,必须将Servlet API中的所有类型都放在你的类路径下。Tomcat中带有servlet-api.jar文件,其中包含了javax.servlet的成员,以及javax.servlet.http包。这个压缩文件放在Tomcat安装目录下的lib目录中。
Servlet应用程序必须在某一个目录结构下部署。图1.2展示了app01a的应用程序目录。
图1.2 应用程序目录
这个目录结构最上面的 app01a 目录就是应用程序目录。在应用程序目录下,是WEB-INF目录。它有两个子目录:
Servlet/JSP应用程序一般都有JSP页面、HTML文件、图片文件以及其他资料。这些应该放在应用程序目录下,并且经常放在子目录下。例如,所有的图片文件可以放在一个image目录下,所有的JSP页面可以放在jsp目录下,等等。
放在应用程序目录下的任何资源,用户只要输入资源URL,都可以直接访问到。如果想让某一个资源可以被Servlet访问,但不可以被用户访问,那么就要把它放在WEB-INF目录下。
现在,准备将应用程序部署到Tomcat。使用Tomcat时,一种部署方法是将应用程序目录复制到Tomcat安装目录下的webapps目录中。也可以通过在Tomcat的conf目录中编辑server.xml文件实现部署,或者单独部署一个XML文件,这样就不需要编辑server.xml了。其他的Servlet容器可能会有不同的部署规则。关于如何将Servlet/JSP应用程序部署到Tomcat的详细信息,请查阅附录A。
部署Servlet/JSP应用程序时,建议将它部署成一个WAR文件。WAR文件其实就是以.war作为扩展名的JAR文件。利用带有JDK或者类似WinZip工具的JAR软件,都可以创建WAR文件。然后,将WAR文件复制到Tomcat的webapps目录下。当开始启动Tomcat时,Tomcat就会自动解压这个war文件。部署成WAR文件在所有Servlet容器中都适用。我们将在第13章讨论更多关于部署的细节。
要测试这个Servlet,需要启动或者重启Tomcat,并在浏览器中打开下面的URL(假设Tomcat配置为监听端口8080,这是它的默认端口):
http://localhost:8080/app01a/my
其输出结果应该类似于图1.3。
图1.3 MyServlet的响应结果
恭喜,你已经成功编写了第一个Servlet应用程序!
对于每一个HTTP请求,Servlet容器都会创建一个ServletRequest实例,并将它传给Servlet的Service方法。ServletRequest封装了关于这个请求的信息。
ServletRequest接口中有一些方法。
public int getContentLength()
返回请求主体的字节数。如果不知道字节长度,这个方法就会返回−1。
public java.lang.String getContentType()
返回请求主体的MIME类型,如果不知道类型,则返回null。
public java.lang.String getParameter(java.lang.String name)
返回指定请求参数的值。
public java.lang.String getProtocol()
返回这个HTTP请求的协议名称和版本。
getParameter是在ServletRequest中最常用的方法。该方法通常用于返回HTML表单域的值。在本章后续的“处理表单”小节中,将会学到如何获取表单值。
getParameter也可以用于获取查询字符串的值。例如,利用下面的URI调用Servlet:
http://domain/context/servletName?id=123
用下面这个语句,可以通过Servlet内部获取id值:
String id = request.getParameter("id");
注意,如果该参数不存在,getParameter将返回null。
除了getParameter外,还可以使用getParameterNames、getParameterMap和getParameterValues获取表单域名、值以及查询字符串。这些方法的使用范例请参阅“Http Servlets”小节。
javax.servlet.ServletResponse接口表示一个Servlet响应。在调用Servlet的Service方法前,Servlet容器首先创建一个ServletResponse,并将它作为第二个参数传给Service方法。ServletResponse隐藏了向浏览器发送响应的复杂过程。
在ServletResponse中定义的方法之一是getWriter方法,它返回了一个可以向客户端发送文本的java.io.PrintWriter。默认情况下,PrintWriter对象使用ISO-8859-1编码。
在向客户端发送响应时,大多数时候是将它作为HTML发送。因此,你必须非常熟悉HTML。
注意:
还有一个方法可以用来向浏览器发送输出,它就是getOutputStream。但这个方法是用于发送二进制数据的,因此,大多数情况使用的是getWriter,而不是getOutputStream。
在发送任何HTML标签前,应该先调用setContentType方法,设置响应的内容类型,并将“text/html”作为一个参数传入。这是在告诉浏览器,内容类型为HTML。在没有内容类型的情况下,大多数浏览器会默认将响应渲染成HTML。但是,如果没有设置响应内容类型,有些浏览器就会将HTML标签显示为普通文本。
在清单1.1的MyServlet中已经用过ServletResponse。在本章以及后续章节中,还会看到在其他应用程序中也使用它。
当Servlet容器初始化Servlet时,Servlet容器会给Servlet的init方法传入一个ServletConfig。ServletConfig封装可以通过@WebServlet或者部署描述符传给Servlet的配置信息。这样传入的每一条信息就叫一个初始参数。一个初始参数有key和value两个元件。
为了从Servlet内部获取到初始参数的值,要在Servlet容器传给Servlet的init方法的ServletConfig中调用getInitParameter方法。getInitParameter的方法签名如下:
java.lang.String getInitParameter(java.lang.String name)
此外,getInitParameterNames方法则是返回所有初始参数名称的一个Enumeration:
java.util.Enumeration<java.lang.String> getInitParameterNames()
例如,为了获取contactName参数值,要使用下面的方法签名:
String contactName = servletConfig.getInitParameter("contactName");
除getInitParameter和getInitParameterNames外,ServletConfig还提供了另一个很有用的方法:getServletContext。利用这个方法可以从Servlet内部获取ServletContext。关于这个对象的深入探讨,请查阅本章1.7节。
下面举一个ServletConfig的范例,在app01a中添加一个名为ServletConfigDemoServlet的Servlet。这个新的Servlet如清单1.7所示。
清单1.2 ServletConfigDemoServlet类
package app01a;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
@WebServlet(name = "ServletConfigDemoServlet",
urlPatterns = { "/servletConfigDemo" },
initParams = {
@WebInitParam(name="admin", value="Harry Taciak"),
@WebInitParam(name="email", value="admin@example.com")
}
)
public class ServletConfigDemoServlet implements Servlet {
private transient ServletConfig servletConfig;
@Override
public ServletConfig getServletConfig() {
return servletConfig;
}
@Override
public void init(ServletConfig servletConfig)
throws ServletException {
this.servletConfig = servletConfig;
}
@Override
public void service(ServletRequest request,
ServletResponse response)
throws ServletException, IOException {
ServletConfig servletConfig = getServletConfig();
String admin = servletConfig.getInitParameter("admin");
String email = servletConfig.getInitParameter("email");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head></head><body>" +
"Admin:" + admin +
"<br/>Email:" + email +
"</body></html>");
}
@Override
public String getServletInfo() {
return "ServletConfig demo";
}
@Override
public void destroy() {
}
}
如清单1.2所示,在@WebServlet的initParams属性中,给Servlet传入了两个初始参数(admin和email):
@WebServlet(name = "ServletConfigDemoServlet",
urlPatterns = { "/servletConfigDemo" },
initParams = {
@WebInitParam(name="admin", value="Harry Taciak"),
@WebInitParam(name="email", value="admin@example.com")
}
)
利用下面这个URL,可以调用ServletConfigDemoServlet:
http://localhost:8080/app01a/servletConfigDemo
其结果类似于图1.4。
图1.4 ServletConfigDemoServlet效果展示
另一种方法是,在部署描述符中传入初始参数。在这里使用部署描述符,比使用@WebServlet更容易,因为部署描述符是一个文本文件,不需要重新编译Servlet类,就可以对它进行编辑。
部署描述符将在1.11节以及第13章中详细讲解。
ServletContext表示Servlet应用程序。每个Web应用程序只有一个上下文。在将一个应用程序同时部署到多个容器的分布式环境中,每台Java虚拟机上的Web应用都会有一个ServletContext对象。
通过在ServletConfig中调用getServletContext方法,可以获得ServletContext。
有了ServletContext,就可以共享从应用程序中的所有资料处访问到的信息,并且可以动态注册Web对象。前者将对象保存在ServletContext中的一个内部Map中。保存在ServletContext中的对象被称作属性。
ServletContext中的下列方法负责处理属性:
java.lang.Object getAttribute(java.lang.String name)
java.util.Enumeration<java.lang.String> getAttributeNames()
void setAttribute(java.lang.String name, java.lang.Object object)
void removeAttribute(java.lang.String name)
前面的例子中展示了如何通过实现Servlet接口来编写Servlet。但你注意到没有?它们必须给Servlet中的所有方法都提供实现,即便其中有一些根本就没有包含任何代码。此外,还需要将ServletConfig对象保存到类级变量中。
值得庆幸的是GenericServlet抽象类的出现。本着尽可能使代码简单的原则, GenericServlet实现了Servlet和ServletConfig接口,并完成以下任务:
GenericServlet通过将ServletConfig赋给init方法中的类级变量servletConfig,来保存ServletConfig。下面就是GenericServlet中的init实现:
public void init(ServletConfig servletConfig)
throws ServletException {
this.servletConfig = servletConfig;
this.init();
}
但是,如果在类中覆盖了这个方法,就会调用Servlet中的init方法,并且还必须调用super.init(servletConfig)来保存ServletConfig。为了避免上述麻烦,GenericServlet提供了第二个init方法,它不带参数。这个方法是在ServletConfig被赋给servletConfig后,由第一个init方法调用:
public void init
throws ServletException {
this.servletConfig = servletConfig;
this.init();
}
这意味着,可以通过覆盖没有参数的init方法来编写初始化代码,ServletConfig则仍然由GenericServlet实例保存。
清单1.3中的GenericServletDemoServlet类是对清单1.2中ServletConfigDemoServlet类的改写。注意,这个新的Servlet扩展了GenericServlet,而不是实现Servlet。
清单1.3 GenericServletDemoServlet类
package app01a;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
@WebServlet(name = "GenericServletDemoServlet",
urlPatterns = { "/generic" },
initParams = {
@WebInitParam(name="admin", value="Harry Taciak"),
@WebInitParam(name="email", value="admin@example.com")
}
)
public class GenericServletDemoServlet extends GenericServlet {
private static final long serialVersionUID = 62500890L;
@Override
public void service(ServletRequest request,
ServletResponse response)
throws ServletException, IOException {
ServletConfig servletConfig = getServletConfig();
String admin = servletConfig.getInitParameter("admin");
String email = servletConfig.getInitParameter("email");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head></head><body>" +
"Admin:" + admin +
"<br/>Email:" + email +
"</body></html>");
}
}
可见,通过扩展GenericServlet,就不需要覆盖没有计划改变的方法。因此,代码变得更加整洁。在清单 1.3 中,唯一被覆盖的方法是 Service 方法。而且,不必亲自保存ServletConfig。
利用下面这个URL调用Servlet,其结果应该与ServletConfigDemoServlet相似:
http://localhost:8080/app01a/generic
即使GenericServlet是对Servlet一个很好的加强,但它也不常用,因为它毕竟不像HttpServlet那么高级。HttpServlet才是主角,在现实的应用程序中被广泛使用。关于它的详情,请查阅1.9节。
不说全部,至少大多数应用程序都要与HTTP结合起来使用。这意味着可以利用HTTP提供的特性。javax.servlet.http包是Servlet API中的第二个包,其中包含了用于编写Servlet应用程序的类和接口。javax.servlet.http中的许多类型都覆盖了javax.servlet中的类型。
图1.5展示了javax.servlet.http中的主要类型。
图1.5 javax.servlet.http中的主要类型
HttpServlet类覆盖了javax.servlet.GenericServlet类。使用HttpServlet时,还要借助分别代表Servlet请求和Servlet响应的HttpServletRequest和HttpServletResponse对象。HttpServletRequest接口扩展javax.servlet.ServletRequest,HttpServletResponse扩展javax.servlet.ServletResponse。
HttpServlet覆盖GenericServlet中的Service方法,并通过下列签名再添加一个Service方法:
protected void service(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, java.io.IOException
新Service方法和javax.servlet.Servlet中Service方法之间的区别在于,前者接受HttpServletRequest和HttpServletResponse,而不是ServletRequest和ServletResponse。
像往常一样,Servlet容器调用javax.servlet.Servlet中原始的Service方法。HttpServlet中的编写方法如下:
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
原始的Service方法将Servlet容器的request和response对象分别转换成HttpServletRequest和HttpServletResponse,并调用新的Service方法。这种转换总是会成功的,因为在调用Servlet的Service方法时,Servlet容器总会传入一个HttpServletRequest和一个HttpServletResponse,预备使用HTTP。即便正在实现javax.servlet.Servlet,或者扩展javax.servlet.GenericServlet,也可以将传给Service方法的servlet request和servlet response分别转换成HttpServletRequest和HttpServletResponse。
然后,HttpServlet中的Service方法会检验用来发送请求的HTTP方法(通过调用request.getMethod),并调用以下方法之一:doGet、doPost、doHead、doPut、doTrace、doOptions和doDelete。这7种方法中,每一种方法都表示一个HTTP方法。doGet和doPost是最常用的。因此,不再需要覆盖Service方法了,只要覆盖doGet或者doPost,或者覆盖doGet和doPost即可。
总之,HttpServlet有两个特性是GenericServlet所不具备的:
HttpServletRequest表示HTTP环境中的Servlet请求。它扩展javax.servlet.ServletRequest接口,并添加了几个方法。新增的部分方法如下:
java.lang.String getContextPath()
返回表示请求上下文的请求URI部分。
Cookie[] getCookies()
返回一个Cookie对象数组。
java.lang.String getHeader(java.lang.String name)
返回指定HTTP标题的值。
java.lang.String getMethod()
返回生成这个请求的HTTP方法名称。
java.lang.String getQueryString()
返回请求URL中的查询字符串。
HttpSession getSession()
返回与这个请求相关的会话对象。如果没有,将创建一个新的会话对象。
HttpSession getSession(boolean create)
返回与这个请求相关的会话对象。如果没有,并且create参数为True,将创建一个新的会话对象。
HttpServletResponse表示HTTP环境中的Servlet响应。下面是它里面定义的部分方法:
void addCookie(Cookie cookie)
给这个响应对象添加一个cookie。
void addHeader(java.lang.String name, java.lang.String value)
给这个响应对象添加一个header。
void sendRedirect(java.lang.String location)
发送一条响应码,将浏览器跳转到指定的位置。
下面的章节将进一步学习这些方法。
一个Web应用程序中几乎总会包含一个或者多个HTML表单,供用户输入值。你可以轻松地将一个HTML表单从一个Servlet发送到浏览器。当用户提交表单时,在表单元素中输入的值就会被当作请求参数发送到服务器。
HTML输入域(文本域、隐藏域或者密码域)或者文本区的值,会被当作字符串发送到服务器。空的输入域或者文本区会发送空的字符串。因此,有输入域名称的,ServletRequest.getParameter绝对不会返回null。
HTML的select元素也向header发送了一个字符串。如果select元素中没有任何选项被选中,那么就会发出所显示的这个选项值。
包含多个值的select元素(允许选择多个选项并且用<select multiple>表示的select元素)发出一个字符串数组,并且必须通过SelectRequest.getParameterValues进行处理。
复选框比较奇特。核查过的复选框会发送字符串“on”到服务器。未经核查的复选框则不向服务器发送任何内容,ServletRequest.getParameter(fieldName)返回null。
单选框将被选中按钮的值发送到服务器。如果没有选择任何按钮,将没有任何内容被发送到服务器,并且ServletRequest.getParameter(fieldName)返回null。
如果一个表单中包含多个输入同名的元素,那么所有值都会被提交,并且必须利用ServletRequest.getParameterValues来获取它们。ServletRequest.getParameter将只返回最后一个值。
清单1.4中的FormServlet类示范了如何处理HTML表单。它的doGet方法将一个Order表单发送到浏览器。它的doPost方法获取到所输入的值,并将它们输出。这个Servlet就是app01b应用程序的一部分。
清单1.4 FormServlet类
package app01b;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "FormServlet", urlPatterns = { "/form" })
public class FormServlet extends HttpServlet {
private static final long serialVersionUID = 54L;
private static final String TITLE = "Order Form";
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<head>");
writer.println("<title>" + TITLE + "</title></head>");
writer.println("<body><h1>" + TITLE + "</h1>");
writer.println("<form method='post'>");
writer.println("<table>");
writer.println("<tr>");
writer.println("<td>Name:</td>");
writer.println("<td><input name='name'/></td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Address:</td>");
writer.println("<td><textarea name='address' "
+ "cols='40' rows='5'></textarea></td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Country:</td>");
writer.println("<td><select name='country'>");
writer.println("<option>United States</option>");
writer.println("<option>Canada</option>");
writer.println("</select></td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Delivery Method:</td>");
writer.println("<td><input type='radio' " +
"name='deliveryMethod'"
+ " value='First Class'/>First Class");
writer.println("<input type='radio' " +
"name='deliveryMethod' "
+ "value='Second Class'/>Second Class</td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Shipping Instructions:</td>");
writer.println("<td><textarea name='instruction' "
+ "cols='40' rows='5'></textarea></td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td> </td>");
writer.println("<td><textarea name='instruction' "
+ "cols='40' rows='5'></textarea></td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Please send me the latest " +
"product catalog:</td>");
writer.println("<td><input type='checkbox' " +
"name='catalogRequest'/></td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td> </td>");
writer.println("<td><input type='reset'/>" +
"<input type='submit'/></td>");
writer.println("</tr>");
writer.println("</table>");
writer.println("</form>");
writer.println("</body>");
writer.println("</html>");
}
@Override
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<head>");
writer.println("<title>" + TITLE + "</title></head>");
writer.println("</head>");
writer.println("<body><h1>" + TITLE + "</h1>");
writer.println("<table>");
writer.println("<tr>");
writer.println("<td>Name:</td>");
writer.println("<td>" + request.getParameter("name")
+ "</td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Address:</td>");
writer.println("<td>" + request.getParameter("address")
+ "</td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Country:</td>");
writer.println("<td>" + request.getParameter("country")
+ "</td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Shipping Instructions:</td>");
writer.println("<td>");
String[] instructions = request
.getParameterValues("instruction");
if (instructions != null) {
for (String instruction : instructions) {
writer.println(instruction + "<br/>");
}
}
writer.println("</td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Delivery Method:</td>");
writer.println("<td>"
+ request.getParameter("deliveryMethod")
+ "</td>");
writer.println("</tr>");
writer.println("<tr>");
writer.println("<td>Catalog Request:</td>");
writer.println("<td>");
if (request.getParameter("catalogRequest") == null) {
writer.println("No");
} else {
writer.println("Yes");
}
writer.println("</td>");
writer.println("</tr>");
writer.println("</table>");
writer.println("<div style='border:1px solid #ddd;" +
"margin-top:40px;font-size:90%'>");
writer.println("Debug Info<br/>");
Enumeration<String> parameterNames = request
.getParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
writer.println(paramName + ": ");
String[] paramValues = request
.getParameterValues(paramName);
for (String paramValue : paramValues) {
writer.println(paramValue + "<br/>");
}
}
writer.println("</div>");
writer.println("</body>");
writer.println("</html>");
}
}
利用下面的URL,可以调用FormServlet:
http://localhost:8080/app01b/form
被调用的doGet方法会被这个HTML表单发送给浏览器:
<form method='post'>
<input name='name'/>
<textarea name='address' cols='40' rows='5'></textarea>
<select name='country'>");
<option>United States</option>
<option>Canada</option>
</select>
<input type='radio' name='deliveryMethod' value='First Class'/>
<input type='radio' name='deliveryMethod' value='Second Class'/>
<textarea name='instruction' cols='40' rows='5'></textarea>
<textarea name='instruction' cols='40' rows='5'></textarea>
<input type='checkbox' name='catalogRequest'/>
<input type='reset'/>
<input type='submit'/>
</form>
表单的方法设为post,确保当用户提交表单时,使用HTTP POST方法。它的action属性默认,表示该表单会被提交给请求它时用的相同的URL。
图1.6展示了一个空的Order表单。
图1.6 一个空的Order表单
现在,填写表单,并单击Submit按钮。在表单中输入的值,将利用HTTP POST方法被发送给服务器,这样就会调用Servlet的doPost方法。因此,将会看到图1.7所示的那些值。
图1.7 在Order表单中输入的值
如在前面的例子中所见,编写和部署Servlet都是很容易的事情。部署的一个方面是用一个路径配置Servlet的映射。在这些范例中,是利用WebServlet标注类型,用一个路径映射了一个Servlet。
利用部署描述符是配置Servlet应用程序的另一种方法,部署描述符的详情将在第13章“部署描述符”中探讨。部署描述符总是命名为web.xml,并且放在WEB-INF目录下。本章介绍了如何创建一个名为app01c的Servlet应用程序,并为它编写了一个web.xml。
app01c有SimpleServlet和WelcomeServlet两个Servlet,还有一个要映射Servlets的部署描述符。清单1.5和清单1.6分别展示了SimpleServlet和WelcomeServlet。注意,Servlet类没有用@WebServlet标注。部署描述符如清单1.7所示。
清单1.5 未标注的SimpleServlet类
package app01c;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SimpleServlet extends HttpServlet {
private static final long serialVersionUID = 8946L;
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head></head>" +
"<body>Simple Servlet</body></html");
}
}
清单1.6 未标注的WelcomeServlet类
package app01c;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class WelcomeServlet extends HttpServlet {
private static final long serialVersionUID = 27126L;
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head></head>"
+ "<body>Welcome</body></html>");
}
}
清单1.7 部署描述符
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
➥ http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<servlet>
<servlet-name>SimpleServlet</servlet-name>
<servlet-class>app01c.SimpleServlet</servlet-class>
<load-on-startup>10</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SimpleServlet</servlet-name>
<url-pattern>/simple</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>WelcomeServlet</servlet-name>
<servlet-class>app01c.WelcomeServlet</servlet-class>
<load-on-startup>20</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>WelcomeServlet</servlet-name>
<url-pattern>/welcome</url-pattern>
</servlet-mapping>
</web-app>
使用部署描述符有诸多好处。其一,可以将在@WebServlet中没有对等元素的元素,如load-on-startup元素。这个元素使得Servlet在应用程序启动时加载,而不是在第一次调用时加载。如果Servlet的init方法需要花一些时间才能完成的话,使用load-on-startup意味着第一次调用Servlet所花的时间并不比后续的调用长,这项功能就特别有用。
使用部署描述符的另一个好处是,如果需要修改配置值,如Servlet路径,则不需要重新编译Servlet类。
此外,可以将初始参数传给一个Servlet,并且不需要重新编译Servlet类,就可以对它们进行编辑。
部署描述符还允许覆盖在Servlet标注中定义的值。Servlet上的WebServlet标注如果同时也在部署描述符中进行声明,那么它将不起作用。然而,在有部署描述符的应用程序中,却不在部署描述符中标注Servlet时,则仍然有效。这意味着,可以标注Servlet,并在同一个应用程序的部署描述符中声明这些Servlet。
图1.8展示了有部署描述符的目录结构。这个目录结构与app01a的目录结构没有太大区别。唯一的区别在于,app01c在WEB-INF目录中有一个web.xml文件(部署描述符)。
图1.8 有部署描述符的b3的目录结构
现在,在部署描述符中声明SimpleServlet和WelcomeServlet,可以利用这些URL来访问它们:
http://localhost:8080/app01c/simple
http://localhost:8080/app01c/welcome
关于部署以及部署描述符的更多信息,请参考第13章。
Servlet技术是Java EE技术的一部分。所有Servlet都运行在Servlet容器中,容器和Servlet间的接口为javax.servlet.Servlet。javax.servlet包还提供了一个名为GenericServlet的Servlet实现类,该类是一个辅助类,以便可以方便的创建一个servlet。不过,大部分servlet都运行在HTTP环境中,因此派生一个javax.servlet.http.HttpServlet的子类更为有用,注意HttpServlet也是GenericServlet的子类。
由于HTTP的无状态性,使得会话管理或会话跟踪成为Web应用开发一个无可避免的主题。默认下,一个Web服务器无法区分一个HTTP请求是否为第一次访问。
例如,一个Web邮件应用要求用户登录后才能查看邮件,因此,当用户输入了相应的用户名和密码后,应用不应该再次提示需要用户登录,应用必须记住哪些用户已经登录。换句话说,应用必须能管理用户的会话。
本章将阐述4种不同的状态保持技术:URL重写、隐藏域、cookies和HTTPSession对象。本章的示例代码为app02a。
URL重写是一种会话跟踪技术,它将一个或多个token添加到URL的查询字符串中,每个token通常为key=value形式,如下:
url?key-1=value-1&key-2=value-2 ... &key-n=value-n
注意,URL和tokens间用问号(?)分割,token间用与号(&)。
URL重写适合于tokens无须在太多URL间传递的情况下,然而它有如下限制:
因为存在如上限制,URL重写仅适合于信息仅在少量页面间传递,且信息本身不敏感。
清单2.1中的Top10Servlet类会显示最受游客青睐的10个伦敦和巴黎的景点。信息分成两页展示,第一页展示指定城市的5个景点,第二页展示另外5个。该Servlet使用URL重写来记录所选择的城市和页数。该类扩展自HttpServlet,并通过/top10访问。
清单2.1 Top10Servlet类
package app02a.urlrewriting;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "Top10Servlet", urlPatterns = { "/top10" })
public class Top10Servlet extends HttpServlet {
private static final long serialVersionUID = 987654321L;
private List<String> londonAttractions;
private List<String> parisAttractions;
@Override
public void init() throws ServletException {
londonAttractions = new ArrayList<String>(10);
londonAttractions.add("Buckingham Palace");
londonAttractions.add("London Eye");
londonAttractions.add("British Museum");
londonAttractions.add("National Gallery");
londonAttractions.add("Big Ben");
londonAttractions.add("Tower of London");
londonAttractions.add("Natural History Museum");
londonAttractions.add("Canary Wharf");
londonAttractions.add("2012 Olympic Park");
londonAttractions.add("St Paul's Cathedral");
parisAttractions = new ArrayList<String>(10);
parisAttractions.add("Eiffel Tower");
parisAttractions.add("Notre Dame");
parisAttractions.add("The Louvre");
parisAttractions.add("Champs Elysees");
parisAttractions.add("Arc de Triomphe");
parisAttractions.add("Sainte Chapelle Church");
parisAttractions.add("Les Invalides");
parisAttractions.add("Musee d'Orsay");
parisAttractions.add("Montmarte");
parisAttractions.add("Sacre Couer Basilica");
}
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
String city = request.getParameter("city");
if (city != null &&
(city.equals("london") || city.equals("paris"))) {
// show attractions
showAttractions(request, response, city);
} else {
// show main page
showMainPage(request, response);
}
}
private void showMainPage(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head>" +
"<title>Top 10 Tourist Attractions</title>" +
"</head><body>" +
"Please select a city:" +
"<br/><a href='?city=london'>London</a>" +
"<br/><a href='?city=paris'>Paris</a>" +
"</body></html>");
}
private void showAttractions(HttpServletRequest request,
HttpServletResponse response, String city)
throws ServletException, IOException {
int page = 1;
String pageParameter = request.getParameter("page");
if (pageParameter != null) {
try {
page = Integer.parseInt(pageParameter);
} catch (NumberFormatException e) {
// do nothing and retain default value for page
}
if (page > 2) {
page = 1;
}
}
List<String> attractions = null;
if (city.equals("london")) {
attractions = londonAttractions;
} else if (city.equals("paris")) {
attractions = parisAttractions;
}
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><head>" +
"<title>Top 10 Tourist Attractions</title>" +
"</head><body>");
writer.println("<a href='top10'>Select City</a> ");
writer.println("<hr/>Page " + page + "<hr/>");
int start = page * 5 - 5;
for (int i = start; i < start + 5; i++) {
writer.println(attractions.get(i) + "<br/>");
}
writer.print("<hr style='color:blue'/>" +
"<a href='?city=" + city +
"&page=1'>Page 1</a>");
writer.println(" <a href='?city=" + city +
"&page=2'>Page 2</a>");
writer.println("</body></html>");
}
}
init方法,仅当该servlet第一次被用户访问时调用,构造两个类级别的列表,londonAttractions和parisAttractions,每个列表有10个景点。
doGet方法,该方法每次请求时被调用,检查URL中是否包括请求参数city,并且其值是否为“london”或“paris”,方法据此决定是调用showAttractions方法还是showMainPage方法:
String city = request.getParameter("city");
if (city != null &&
(city.equals("london") || city.equals("paris"))) {
// show attractions
showAttractions(request, response, city);
} else {
// show main page
showMainPage(request, response);
}
用户一开始访问该servlet时不带任何请求参数,此时调用showMainPage,该方法发送两个链接到浏览器,每个链接都包含token:city=cityName。用户所见如图2.1所示,现在用户可以选择一个城市。
图2.1 Top10Servlet的初始页面
如果你查看网页源代码,你会看见如下HTML:
Please select a city:<br/>
<a href='?city=london'>London</a><br/>
<a href='?city=paris'>Paris</a>
请注意a元素中的href属性,该属性值包括一个问号加token city=london或city=paris. 注意,此处为相对URL,即URL中没有协议部分,相对于当前页面。因此,若你点击了任一链接,则会提交
http://localhost:8080/app02a/top10?city=london
或
http://localhost:8080/app02a/top10?city=paris
到服务器上。
根据用户所点击的链接,doGet方法识别请求参数的city值并传递给showAttractions方法,该方法会检查URL中是否包含page参数,如果没有该参数或该参数值无法转换为数字,则该方法设定page参数值为1,并将头5个景点发给客户端。图2.2为选择伦敦时的界面。
showAttractions方法还发送了3个链接到客户端:Select City、Page 1和Page 2。Select City 是无参数访问servlet,Page 1和Page 2链接包括两个tokens,即city和page:
http://localhost:8080/app02a/top10?city=cityName&page=pageNumber
若选择了伦敦,并点击了Page 2,则将以下URL发送给服务端:
http://localhost:8080/app02a/top10?city=london&page=2
图2.2 伦敦前十景点,第一页
此时系统会展示伦敦的另外5个景点,如图2.3所示。
图2.3 伦敦前十景点,第二页
本例展示了如何用URL重写技术来传递参数——city到服务端以便服务端能正确展示。
使用隐藏域来保持状态类似于URL重写技术,但不是将值附加到URL上,而是放到HTML表单的隐藏域中。当表单提交时,隐藏域的值也同时提交到服务器端。隐藏域技术仅当网页有表单时有效。该技术相对于URL重写的优势在于:没有字符数限制,同时无须额外的编码。但该技术同URL重写一样,不适合跨越多个界面。
清单2.3展示了如何通过隐藏域来更新客户信息。清单2.2的Customer类为客户对象模型。
清单2.2 Customer类
package app02a.hiddenfields;
public class Customer {
private int id;
private String name;
private String city;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
清单2.3 CustomerServlet类
package app02a.hiddenfields;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
* Not thread-safe. For illustration purpose only
*/
@WebServlet(name = "CustomerServlet", urlPatterns = {
"/customer", "/editCustomer", "/updateCustomer"})
public class CustomerServlet extends HttpServlet {
private static final long serialVersionUID = -20L;
private List<Customer> customers = new ArrayList<Customer>();
@Override
public void init() throws ServletException {
Customer customer1 = new Customer();
customer1.setId(1);
customer1.setName("Donald D.");
customer1.setCity("Miami");
customers.add(customer1);
Customer customer2 = new Customer();
customer2.setId(2);
customer2.setName("Mickey M.");
customer2.setCity("Orlando");
customers.add(customer2);
}
private void sendCustomerList(HttpServletResponse response)
throws IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><head><title>Customers</title></head>"
+ "<body><h2>Customers </h2>");
writer.println("<ul>");
for (Customer customer : customers) {
writer.println("<li>" + customer.getName()
+ "(" + customer.getCity() + ") ("
+ "<a href='editCustomer?id=" + customer.getId()
+ "'>edit</a>)");
}
writer.println("</ul>");
writer.println("</body></html>");
}
private Customer getCustomer(int customerId) {
for (Customer customer : customers) {
if (customer.getId() == customerId) {
return customer;
}
}
return null;
}
private void sendEditCustomerForm(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
int customerId = 0;
try {
customerId =
Integer.parseInt(request.getParameter("id"));
} catch (NumberFormatException e) {
}
Customer customer = getCustomer(customerId);
if (customer != null) {
writer.println("<html><head>"
+ "<title>Edit Customer</title></head>"
+ "<body><h2>Edit Customer</h2>"
+ "<form method='post' "
+ "action='updateCustomer'>");
writer.println("<input type='hidden' name='id' value='"
+ customerId + "'/>");
writer.println("<table>");
writer.println("<tr><td>Name:</td><td>"
+ "<input name='name' value='" +
customer.getName().replaceAll("'", "'")
+ "'/></td></tr>");
writer.println("<tr><td>City:</td><td>"
+ "<input name='city' value='" +
customer.getCity().replaceAll("'", "'")
+ "'/></td></tr>");
writer.println("<tr>"
+ "<td colspan='2' style='text-align:right'>"
+ "<input type='submit' value='Update'/></td>"
+ "</tr>");
writer.println("<tr><td colspan='2'>"
+ "<a href='customer'>Customer List</a>"
+ "</td></tr>");
writer.println("</table>");
writer.println("</form></body>");
} else {
writer.println("No customer found");
}
}
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String uri = request.getRequestURI();
if (uri.endsWith("/customer")) {
sendCustomerList(response);
} else if (uri.endsWith("/editCustomer")) {
sendEditCustomerForm(request, response);
}
}
@Override
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// update customer
int customerId = 0;
try {
customerId =
Integer.parseInt(request.getParameter("id"));
} catch (NumberFormatException e) {
}
Customer customer = getCustomer(customerId);
if (customer != null) {
customer.setName(request.getParameter("name"));
customer.setCity(request.getParameter("city"));
}
sendCustomerList(response);
}
}
CustomerServlet类继承自HttpServlet,其URL映射分别为/customer、/editCustomer和 /updateCustomer。前两个URL会调用Servlet的doGet方法,而/updateCustomer 会调用doPost方法。
/customer是本例的入口URL。该URL会列举出在init 方法中所初始化的类级别的列表对象customers(在真实应用中,通常是从数据库中获取用户信息),如图2.4所示。
图2.4 客户列表
如图2.4所示,每个客户信息后都有一个edit链接,每个edit链接的href属性为 /editCustomer? id=customerId。当通过/editCustomer访问servlet时,servlet会返回一个编辑表单,如图2.5所示。
图2.5 客户编辑表单
如果你点击的是第一个客户,servlet返回表单中的隐藏域如下:
<form method='post' action='updateCustomer'>
<input type='hidden' name='id' value='1'/>
<table>
<tr><td>Name:</td>
<td><input name='name' value='Donald DC.'/></td>
</tr>
<tr>
<td>City:</td><td><input name='city' value='Miami'/></td>
</tr>
<tr>
<td colspan='2' style='text-align:right'>
<input type='submit' value='Update'/>
</td>
</tr>
<tr>
<td colspan='2'><a href='customer'>Customer List</a></td>
</tr>
</table>
</form>
该隐藏域为所编辑的客户id,因此当表单提交时,服务端就知道应更新哪个客户信息。
需要强调的是,表单是通过post方式提交的,因此调用的是servlet的doPost方法。
URL重写和隐藏域仅适合保存无须跨越太多页面的信息。如果需要在多个页面间传递信息,则以上两种技术实现成本高昂,因为你不得不在每个页面都进行相应处理。幸运的是,Cookies技术可以帮助我们。
Cookies是一个很少的信息片段,可自动地在浏览器和Web服务器间交互,因此cookies可存储在多个页面间传递的信息。Cookie作为HTTP header的一部分,其传输由HTTP协议控制。此外,你可以控制cookies的有效时间。浏览器通常支持每个网站高达20个cookies。
Cookies的问题在于用户可以通过改变其浏览器设置来拒绝接受cookies。
要使用cookies,需要熟悉javax.servlet.http.Cookie类以及HttpServletRequest和HttpServlet Response两个接口。
可以通过传递name和value两个参数给Cookie 类的构造函数来创建一个cookies:
Cookie cookie = new Cookie(name, value);
如下是一个创建语言选择的cookie示例:
Cookie languageSelectionCookie = new Cookie("language", "Italian");
创建完一个Cookie对象后,你可以设置domain、path和maxAge属性。其中,maxAge 属性决定cookie何时过期。
要将cookie发送到浏览器,需要调用HttpServletResponse的add方法:
httpServletResponse.addCookie(cookie);
浏览器在访问同一Web服务器时,会将之前收到的cookie一并发送。
此外,Cookies也可以通过客户端的javascript脚本创建和删除,不过这些不在本书范围内。
服务端若要读取浏览器提交的cookie,可以通过HttpServletRequest接口的getCookies方法,该方法返回一个Cookie数组,若没有cookies则返回null。你需要遍历整个数组来查询某个特定名称的cookie。如下为查询名为maxRecords的cookie的示例:
Cookie[] cookies = request.getCookies();
Cookie maxRecordsCookie = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("maxRecords")) {
maxRecordsCookie = cookie;
break;
}
}
}
目前,还没有类似于getCookieByName这样的方法来帮助简化工作。此外,也没有一个直接的方法来删除一个cookie,你只能创建一个同名的cookie,并将maxAge属性设置为0,并添加到HttpServletResponse接口中。如下为删除一个名为userName的cookie代码:
Cookie cookie = new Cookie("userName", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
清单2.4的PreferenceServlet 类展示了如何通过cookies来进行会话管理,该Servlet允许用户通过修改四个cookie值来设定显示配置。
清单2.4 PreferenceServlet类
package app02a.cookie;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "PreferenceServlet", urlPatterns = { "/preference" })
public class PreferenceServlet extends HttpServlet {
private static final long serialVersionUID = 888L;
public static final String MENU =
"<div style='background:#e8e8e8;"
+ "padding:15px'>"
+ "<a href='cookieClass'>Cookie Class</a> "
+ "<a href='cookieInfo'>Cookie Info</a> "
+ "<a href='preference'>Preference</a>" + "</div>";
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head>" + "<title>Preference</title>"
+ "<style>table {" + "font-size:small;"
+ "background:NavajoWhite }</style>"
+ "</head><body>"
+ MENU
+ "Please select the values below:"
+ "<form method='post'>"
+ "<table>"
+ "<tr><td>Title Font Size: </td>"
+ "<td><select name='titleFontSize'>"
+ "<option>large</option>"
+ "<option>x-large</option>"
+ "<option>xx-large</option>"
+ "</select></td>"
+ "</tr>"
+ "<tr><td>Title Style & Weight: </td>"
+"<td><select name='titleStyleAndWeight' multiple>"
+ "<option>italic</option>"
+ "<option>bold</option>"
+ "</select></td>"
+ "</tr>"
+ "<tr><td>Max. Records in Table: </td>"
+ "<td><select name='maxRecords'>"
+ "<option>5</option>"
+ "<option>10</option>"
+ "</select></td>"
+ "</tr>"
+ "<tr><td rowspan='2'>"
+ "<input type='submit' value='Set'/></td>"
+ "</tr>"
+ "</table>" + "</form>" + "</body></html>");
}
@Override
public void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
String maxRecords = request.getParameter("maxRecords");
String[] titleStyleAndWeight = request
.getParameterValues("titleStyleAndWeight");
String titleFontSize =
request.getParameter("titleFontSize");
response.addCookie(new Cookie("maxRecords", maxRecords));
response.addCookie(new Cookie("titleFontSize",
titleFontSize));
// delete titleFontWeight and titleFontStyle cookies first
// Delete cookie by adding a cookie with the maxAge = 0;
Cookie cookie = new Cookie("titleFontWeight", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
cookie = new Cookie("titleFontStyle", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
if (titleStyleAndWeight != null) {
for (String style : titleStyleAndWeight) {
if (style.equals("bold")) {
response.addCookie(new
Cookie("titleFontWeight", "bold"));
} else if (style.equals("italic")) {
response.addCookie(new Cookie("titleFontStyle",
"italic"));
}
}
}
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><head>" + "<title>Preference</title>"
+ "</head><body>" + MENU
+ "Your preference has been set."
+ "<br/><br/>Max. Records in Table: " + maxRecords
+ "<br/>Title Font Size: " + titleFontSize
+ "<br/>Title Font Style & Weight: ");
// titleStyleAndWeight will be null if none of the options
// was selected
if (titleStyleAndWeight != null) {
writer.println("<ul>");
for (String style : titleStyleAndWeight) {
writer.print("<li>" + style + "</li>");
}
writer.println("</ul>");
}
writer.println("</body></html>");
}
}
PreferenceServlet的doGet方法展示一个包含多个输入项的表单,如图2.6所示。
图2.6 通过cookies来管理用户偏好
表单上部有3个链接:Cookie Class、Cookie Info和Preference。它们可以导航到本应用的其他Servlet上。关于Cookie Class和Cookie Info,我们稍后介绍。
当用户提交表单时,Web服务器会调用PreferenceServlet的doPost方法,该方法创建4个cookies,即maxRecords、titleFontSize、titleFontStyle和titleFontWeight,并覆盖该cookie之前的值,然后将用户输入的值返回给浏览器。
可以通过如下URL访问PreferenceServlet:
http://localhost:8080/app02a/preference
CookieClassServlet(见清单2.5)和CookieInfoServlet(见清单2.6)各自应用这些cookie来格式化其内容。CookieClassServlet将Cookie的属性展示为一个HTML列表。
Cookie中的max Records值决定显示多少个列表项,可通过Preference Servlet调整该值。
清单2.5 CookieClassServlet类
package app02a.cookie;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "CookieClassServlet",
urlPatterns = { "/cookieClass" })
public class CookieClassServlet extends HttpServlet {
private static final long serialVersionUID = 837369L;
private String[] methods = {
"clone", "getComment", "getDomain",
"getMaxAge", "getName", "getPath",
"getSecure", "getValue", "getVersion",
"isHttpOnly", "setComment", "setDomain",
"setHttpOnly", "setMaxAge", "setPath",
"setSecure", "setValue", "setVersion"
};
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
Cookie[] cookies = request.getCookies();
Cookie maxRecordsCookie = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("maxRecords")) {
maxRecordsCookie = cookie;
break;
}
}
}
int maxRecords = 5; // default
if (maxRecordsCookie != null) {
try {
maxRecords = Integer.parseInt(
maxRecordsCookie.getValue());
} catch (NumberFormatException e) {
// do nothing, use maxRecords default value
}
}
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head>" + "<title>Cookie Class</title>"
+ "</head><body>"
+ PreferenceServlet.MENU
+ "<div>Here are some of the methods in " +
"javax.servlet.http.Cookie");
writer.print("<ul>");
for (int i = 0; i < maxRecords; i++) {
writer.print("<li>" + methods[i] + "</li>");
}
writer.print("</ul>");
writer.print("</div></body></html>");
}
}
CookieInfoServlet 类读取titleFontSize、titleFontWeight和titleFontStyle 三个cookie值,并写入到如下发给浏览器的CSS中,其中x、y和z分别为如上所提的cookie。
.title {
font-size: x;
font-weight: y;
font-style: z;
}
该style应用在一个div元素中,并格式化文字“Session Management with Cookies:”。
清单2.6 CookieInfoServlet类
package app02a.cookie;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "CookieInfoServlet", urlPatterns = { "/cookieInfo" })
public class CookieInfoServlet extends HttpServlet {
private static final long serialVersionUID = 3829L;
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
Cookie[] cookies = request.getCookies();
StringBuilder styles = new StringBuilder();
styles.append(".title {");
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
String value = cookie.getValue();
if (name.equals("titleFontSize")) {
styles.append("font-size:" + value + ";");
} else if (name.equals("titleFontWeight")) {
styles.append("font-weight:" + value + ";");
} else if (name.equals("titleFontStyle")) {
styles.append("font-style:" + value + ";");
}
}
}
styles.append("}");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<html><head>" + "<title>Cookie Info</title>"
+ "<style>" + styles.toString() + "</style>"
+ "</head><body>" + PreferenceServlet.MENU
+ "<div class='title'>"
+ "Session Management with Cookies:</div>");
writer.print("<div>");
// cookies will be null if there's no cookie
if (cookies == null) {
writer.print("No cookie in this HTTP response.");
} else {
writer.println("<br/>Cookies in this HTTP response:");
for (Cookie cookie : cookies) {
writer.println("<br/>" + cookie.getName() + ":"
+ cookie.getValue());
}
}
writer.print("</div>");
writer.print("</body></html>");
}
}
可以通过如下URL来访问CookieClassServlet:
http://localhost:8080/app02a/cookieClass
可以通过如下URL来访问CookieInfoServlet:
http://localhost:8080/app02a/cookieInfo
图2.7和图2.8分别展示了CookieClassServlet和CookieInfoServlet的显示界面。
图2.7 CookieClassServlet输出
图2.8 CookieInfoServlet输出
在所有的会话跟踪技术中,HttpSession 对象是最强大和最通用的。一个用户可以有且最多有一个HttpSession,并且不会被其他用户访问到。
HttpSession对象在用户第一次访问网站的时候自动被创建,你可以通过调用HttpServletRequest的getSession方法获取该对象。getSession有两个重载方法:
HttpSession getSession()
HttpSession getSession(boolean create)
没有参数的getSession方法会返回当前的HttpSession,若当前没有,则创建一个返回。getSession(false)返回当前HttpSession,如当前会话不存在,则返回null。getSession(true)返回当前HttpSession,若当前没有,则创建一个getSession(true)同getSession()一致。
可以通过HttpSession的setAttribute方法将值放入HttpSession,该方法签字如下:
void setAttribute(java.lang.String name, java.lang.Object value)
请注意,不同于URL重新、隐藏域或cookie,放入到HttpSession 的值,是存储在内存中的,因此,不要往HttpSession放入太多对象或大对象。尽管现代的Servlet容器在内存不够用的时候会将保存在HttpSessions的对象转储到二级存储上,但这样有性能问题,因此小心存储。
此外,放到HttpSession的值不限于String类型,可以是任意实现java.io.Serializable的java对象,因为Servlet容器认为必要时会将这些对象放入文件或数据库中,尤其在内存不够用的时候,当然你也可以将不支持序列化的对象放入HttpSession,只是这样,当Servlet容器视图序列化的时候会失败并报错。
调用setAttribute方法时,若传入的name参数此前已经使用过,则会用新值覆盖旧值。
通过调用HttpSession的getAttribute方法可以取回之前放入的对象,该方法的签名如下:
java.lang.Object getAttribute(java.lang.String name)
HttpSession 还有一个非常有用的方法,名为getAttributeNames,该方法会返回一个Enumeration 对象来迭代访问保存在HttpSession中的所有值:
java.util.Enumeration<java.lang.String> getAttributeNames()
注意,所有保存在HttpSession的数据不会被发送到客户端,不同于其他会话管理技术,Servlet容器为每个HttpSession 生成唯一的标识,并将该标识发送给浏览器,或创建一个名为JSESSIONID的cookie,或者在URL后附加一个名为jsessionid 的参数。在后续的请求中,浏览器会将标识提交给服务端,这样服务器就可以识别该请求是由哪个用户发起的。Servlet容器会自动选择一种方式传递会话标识,无须开发人员介入。
可以通过调用 HttpSession的getId方法来读取该标识:
java.lang.String getId()
此外,HttpSession.还定义了一个名为invalidate 的方法。该方法强制会话过期,并清空其保存的对象。默认情况下,HttpSession 会在用户不活动一段时间后自动过期,该时间可以通过部署描述符的 session-timeout 元素配置,若设置为30,则会话对象会在用户最后一次访问30分钟后过期,如果部署描述符没有配置,则该值取决于Servlet容器的设定。
大部分情况下,你应该主动销毁无用的HttpSession,以便释放相应的内存。
可以通过调用HttpSession 的getMaxInactiveInterval 方法来查看会话多久会过期。该方法返回一个数字类型,单位为秒。调用setMaxInactiveInterval 方法来单独对某个HttpSession 设定其超时时间:
void setMaxInactiveInterval(int seconds)
若设置为0,则该HttpSession 永不过期。通常这不是一个好的设计,因此该 HttpSession 所占用的堆内存将永不释放,直到应用重加载或Servlet容器关闭。
清单2.9 ShoppingCartServlet 为一个小的有4个商品的在线商城,用户可以将商品添加到购物车中,并可以查看购物车内容,所用到的Product类可见清单2.7,ShoppingItem 类可见清单2.8,Product类定义了4个属性(id、name、description和price),ShoppingItem 有两个属性,即quantity和Product。
清单2.7 Product类
package app02a.httpsession;
public class Product {
private int id;
private String name;
private String description;
private float price;
public Product(int id, String name, String description, float price)
{
this.id = id;
this.name = name;
this.description = description;
this.price = price;
}
// get and set methods not shown to save space
}
清单2.8 ShoppingItem类
package app02a.httpsession;
public class ShoppingItem {
private Product product;
private int quantity;
public ShoppingItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
// get and set methods not shown to save space
}
清单2.9 ShoppingCartServlet类
package app02a.httpsession;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet(name = "ShoppingCartServlet", urlPatterns = {
"/products", "/viewProductDetails",
"/addToCart", "/viewCart" })
public class ShoppingCartServlet extends HttpServlet {
private static final long serialVersionUID = -20L;
private static final String CART_ATTRIBUTE = "cart";
private List<Product> products = new ArrayList<Product>();
private NumberFormat currencyFormat = NumberFormat
.getCurrencyInstance(Locale.US);
@Override
public void init() throws ServletException {
products.add(new Product(1, "Bravo 32' HDTV",
"Low-cost HDTV from renowned TV manufacturer",
159.95F));
products.add(new Product(2, "Bravo BluRay Player",
"High quality stylish BluRay player", 99.95F));
products.add(new Product(3, "Bravo Stereo System",
"5 speaker hifi system with iPod player",
129.95F));
products.add(new Product(4, "Bravo iPod player",
"An iPod plug-in that can play multiple formats",
39.95F));
}
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
String uri = request.getRequestURI();
if (uri.endsWith("/products")) {
sendProductList(response);
} else if (uri.endsWith("/viewProductDetails")) {
sendProductDetails(request, response);
} else if (uri.endsWith("viewCart")) {
showCart(request, response);
}
}
@Override
public void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException {
// add to cart
int productId = 0;
int quantity = 0;
try {
productId = Integer.parseInt(
request.getParameter("id"));
quantity = Integer.parseInt(request
.getParameter("quantity"));
} catch (NumberFormatException e) {
}
Product product = getProduct(productId);
if (product != null && quantity >= 0) {
ShoppingItem shoppingItem = new ShoppingItem(product,
quantity);
HttpSession session = request.getSession();
List<ShoppingItem> cart = (List<ShoppingItem>) session
.getAttribute(CART_ATTRIBUTE);
if (cart == null) {
cart = new ArrayList<ShoppingItem>();
session.setAttribute(CART_ATTRIBUTE, cart);
}
cart.add(shoppingItem);
}
sendProductList(response);
}
private void sendProductList(HttpServletResponse response)
throws IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><head><title>Products</title>" +
"</head><body><h2>Products</h2>");
writer.println("<ul>");
for (Product product : products) {
writer.println("<li>" + product.getName() + "("
+ currencyFormat.format(product.getPrice())
+ ") (" + "<a href='viewProductDetails?id="
+ product.getId() + "'>Details</a>)");
}
writer.println("</ul>");
writer.println("<a href='viewCart'>View Cart</a>");
writer.println("</body></html>");
}
private Product getProduct(int productId) {
for (Product product : products) {
if (product.getId() == productId) {
return product;
}
}
return null;
}
private void sendProductDetails(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
int productId = 0;
try {
productId = Integer.parseInt(
request.getParameter("id"));
} catch (NumberFormatException e) {
}
Product product = getProduct(productId);
if (product != null) {
writer.println("<html><head>"
+ "<title>Product Details</title></head>"
+ "<body><h2>Product Details</h2>"
+ "<form method='post' action='addToCart'>");
writer.println("<input type='hidden' name='id' "
+ "value='" + productId + "'/>");
writer.println("<table>");
writer.println("<tr><td>Name:</td><td>"
+ product.getName() + "</td></tr>");
writer.println("<tr><td>Description:</td><td>"
+ product.getDescription() + "</td></tr>");
writer.println("<tr>" + "<tr>"
+ "<td><input name='quantity'/></td>"
+ "<td><input type='submit' value='Buy'/>"
+ "</td>"
+ "</tr>");
writer.println("<tr><td colspan='2'>"
+ "<a href='products'>Product List</a>"
+ "</td></tr>");
writer.println("</table>");
writer.println("</form></body>");
} else {
writer.println("No product found");
}
}
private void showCart(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><head><title>Shopping Cart</title>"
+ "</head>");
writer.println("<body><a href='products'>" +
"Product List</a>");
HttpSession session = request.getSession();
List<ShoppingItem> cart = (List<ShoppingItem>) session
.getAttribute(CART_ATTRIBUTE);
if (cart != null) {
writer.println("<table>");
writer.println("<tr><td style='width:150px'>Quantity"
+ "</td>"
+ "<td style='width:150px'>Product</td>"
+ "<td style='width:150px'>Price</td>"
+ "<td>Amount</td></tr>");
double total = 0.0;
for (ShoppingItem shoppingItem : cart) {
Product product = shoppingItem.getProduct();
int quantity = shoppingItem.getQuantity();
if (quantity != 0) {
float price = product.getPrice();
writer.println("<tr>");
writer.println("<td>" + quantity + "</td>");
writer.println("<td>" + product.getName()
+ "</td>");
writer.println("<td>"
+ currencyFormat.format(price)
+ "</td>");
double subtotal = price * quantity;
writer.println("<td>"
+ currencyFormat.format(subtotal)
+ "</td>");
total += subtotal;
writer.println("</tr>");
}
}
writer.println("<tr><td colspan='4' "
+ "style='text-align:right'>"
+ "Total:"
+ currencyFormat.format(total)
+ "</td></tr>");
writer.println("</table>");
}
writer.println("</table></body></html>");
}
}
ShoppingCartServlet 映射有如下URL:
除/addToCart外,其他URL都会调用doGet方法。doGet 首先根据所请求的URL来生成相应内容:
String uri = request.getRequestURI();
if (uri.endsWith("/products")) {
sendProductList(response);
} else if (uri.endsWith("/viewProductDetails")) {
sendProductDetails(request, response);
} else if (uri.endsWith("viewCart")) {
showCart(request, response);
}
如下URL访问应用的主界面:
http://localhost:8080/app02a/products
该URL会展示商品列表,如图2.9所示。
图2.9 产品列表
单击Details(详细)链接,Servlet会显示所选产品的详细信息,如图2.10所示。请注意页面上的输入框和Buy按钮,输入一个数字并单击Buy按钮,就可以添加该产品到购物车中。
图2.10 产品详细页
提交购物表单,Web容器会调用ShoppingCartServlet的doPost方法,该方法将一个商品添加到该用户的HttpSession。
doPost方法首先构造一个ShoppingItem实例,传入用户所编辑的商品和数量:
ShoppingItem shoppingItem = new ShoppingItem(product,
quantity);
然后获取当前用户的HttpSession,并检查是否已经有一个名为“cart”的List对象:
HttpSession session = request.getSession();
List<ShoppingItem> cart = (List<ShoppingItem>) session
.getAttribute(CART_ATTRIBUTE);
若不存在,则创建一个并添加到HttpSession中:
if (cart == null) {
cart = new ArrayList<ShoppingItem>();
session.setAttribute(CART_ATTRIBUTE, cart);
}
最后,将所创建的ShoppingItem添加到该list中:
cart.add(shoppingItem);
当用户单击View Cart(查看购物车)链接时,Web容器调用showCart方法,获取当前用户的HttpSession并调用其getAttribute方法来获取购物商品列表:
HttpSession session = request.getSession();
List<ShoppingItem> cart = (List<ShoppingItem>) session
.getAttribute(CART_ATTRIBUTE);
然后迭代访问List对象,并将购物项发送给浏览器:
if (cart != null) {
for (ShoppingItem shoppingItem : cart) {
Product product = shoppingItem.getProduct();
int quantity = shoppingItem.getQuantity();
…
本章中,你已经学习了会话管理的概念以及4种会话管理技术,URL重写和隐藏域是轻量级的会话跟踪技术,适用于那些仅跨少量页面的数据。而cookies和HttpSession对象,更加灵活但也有限制,尤其是在应用HttpSession时会消耗服务器内存。