4. 会话跟踪
4.1. 问题
一个 Web 应用程序可能包含服务器与客户端之间的多次表单交互。该过程的工作原理如下:
- 步骤 1
- 客户端 C1 与服务器建立连接并发出初始请求。
- 服务器向客户端 C1 发送表单 F1,并关闭在步骤 1 中建立的连接。
- 步骤 2
- 客户端 C1 填写表单并将其发回服务器。为此,浏览器会与服务器建立新的连接。
- 服务器处理表单 1 中的数据,据此计算出信息 I1,将表单 F2 发送给客户端 C1,并关闭在步骤 3 中建立的连接。
- 步骤 3
- 步骤 3 和 4 的循环在步骤 5 和 6 中重复。在步骤 6 结束时,服务器将收到两个表单 F1 和 F2,并据此计算出信息 I1 和 I2。
当前的问题是:服务器如何跟踪与客户端 C1 相关的信息 I1 和 I2?这个问题被称为跟踪客户端 C1 的会话。为了理解其根本原因,让我们来分析一个同时为多个客户端提供服务的 TCP-IP 服务器应用程序的示意图:
![]() |
在典型的 TCP-IP 客户端-服务器应用程序中:
- 客户端与服务器建立连接
- 通过此连接与服务器交换数据
- 连接由双方中的一方关闭
该机制的两个关键点是:
- 为每个客户端建立一个单独的连接
- 该连接将贯穿服务器与其客户端对话的整个过程
服务器之所以能在任何时刻识别正在交互的客户端,正是因为连接——或者说“通道”——将服务器与客户端紧密相连。由于该通道专用于特定客户端,因此通过该通道接收的所有数据均来自该客户端,而通过该通道发送的所有数据也都能送达该客户端。
HTTP 客户端-服务器机制与上述模型非常相似,唯一的区别在于客户端与服务器的交互仅限于一次往返通信:
- 客户端向服务器建立连接并发出请求
- 服务器发送响应并关闭连接
如果在时间 T1,客户端 C 向服务器发出请求,它将获得连接 C1,该连接将用于单次请求-响应交互。如果在时间 T2,同一客户端向服务器发出第二次请求,它将获得连接 C2,该连接与连接 C1 不同。 对于服务器而言,用户 C 的第二次请求与其初始请求并无区别:在两种情况下,服务器都将该客户端视为新客户端。若要建立客户端 C 与服务器之间不同连接的关联,客户端 C 必须被服务器“识别”为“常客”,且服务器必须检索其关于该常客的信息。
让我们设想一个按以下方式运行的系统:
- 存在一个单一队列
- 设有多个服务窗口。这意味着可以同时为多名顾客提供服务。当某个窗口有空位时,顾客会离开队伍前往该窗口接受服务
- 如果这是客户首次到访,柜员会给他们一张印有编号的号牌。客户只能提出一个问题。一旦得到答复,他们必须离开柜台窗口并回到队尾。柜员会将客户的信息记录在标有该编号的文件中。
- 当轮到该客户再次办理业务时,可能由与上次不同的柜员接待。柜员会索要其代币,并调出与该代币号对应的档案。客户再次提出请求,获得答复,相关信息会被添加到其档案中。
- 如此循环往复……随着时间推移,客户将获得所有请求的答复。通过代币及其关联的档案,实现了不同请求之间的关联追踪。
客户端-服务器Web应用程序中的会话跟踪机制工作原理与此类似:
- 客户端发出首次请求时,Web 服务器会向其发放一个令牌
- 客户端将在后续每次请求中提交该令牌以进行身份验证
令牌可以采用多种形式:
- 表单中的隐藏字段
- 客户端发起首次请求(服务器能识别该请求,因为客户端此时没有令牌)
- 服务器发送响应(一个表单),并将令牌放置在表单内的隐藏字段中。此时,连接被关闭(客户端带着令牌退出会话)。服务器可能已将相关信息与该令牌关联起来。
- 客户端通过重新提交表单发起第二次请求。服务器从表单中提取令牌。随后,服务器可通过该令牌访问首次请求期间计算出的信息,从而处理客户端的第二次请求。 新信息被添加到与该令牌关联的文件中,第二份响应被发送给客户端,连接第二次关闭。令牌已被放回响应表单中,以便用户在下次请求时能够提供它。
- 以此类推……
此技术的主要缺点在于令牌必须放置在表单中。如果服务器的响应不是表单,则无法再使用隐藏字段方法。
- Cookie 方法
- 客户端发出首次请求(服务器通过客户端未持有令牌来识别此请求)
- 服务器通过在 HTTP 头部添加一个 Cookie 进行响应。这通过 HTTP Set-Cookie 命令实现:
Set-Cookie: param1=value1;param2=value2;....
其中 param1、param2 等为参数名称及其对应值。这些参数中将包含令牌。通常情况下,Cookie 中仅包含令牌,服务器会将其他信息存储在与该令牌关联的文件夹中。接收该 Cookie 的浏览器会将其存储在磁盘上的文件中。服务器响应后,连接即被关闭(客户端携带着其令牌退出会话)。
- (待续)
- 客户端向服务器发出第二次请求。每次向服务器发送请求时,浏览器都会检查其拥有的所有 Cookie,以确认是否包含来自该服务器的 Cookie。如果存在,则将其发送给服务器,形式始终为 HTTP 命令——即 Cookie 命令,其语法与服务器使用的 Set-Cookie 命令类似:
Cookie: param1=value1;param2=value2;....
在浏览器发送的参数中,服务器将找到一个令牌,该令牌使其能够识别客户端并检索与其关联的信息。
这是最常用的令牌形式。它有一个缺点:用户可以配置浏览器拒绝Cookie。这样,这些用户就无法访问使用Cookie的Web应用程序。
- URL 重写
- 客户端发出首次请求(服务器能识别出这一点,因为客户端没有令牌)
- 服务器发送响应。该响应包含用户必须使用的链接,以便继续使用应用程序。在每个链接的 URL 中,服务器都会以 URL;token=value 的形式添加令牌。
- 当用户点击其中一个链接以继续使用应用程序时,浏览器会向 Web 服务器发送请求,并在 HTTP 头中包含所请求的 URL(URL;token=value)。随后,服务器便能检索到该令牌。
4.2. 用于会话跟踪的 Java API
下面我们将介绍几种用于会话跟踪的主要方法:
获取当前请求所属的 Session 对象。如果该请求尚未属于任何会话,则会创建一个会话。 | |
当前会话的标识符 | |
当前会话的创建时间(自 1970 年 1 月 1 日 00:00 起经过的毫秒数)。 | |
客户端上次访问该会话的日期 | |
会话最长闲置时间(以秒为单位)。超过此时间后,会话将失效。 | |
设置会话的最大闲置时长(以秒为单位)。超过此时间后,会话将失效。 | |
如果会话刚刚创建,则返回 true | |
将一个值与指定会话中的参数关联。此机制允许存储在整个会话期间始终可用的信息。 | |
从会话数据中移除该参数。 | |
返回会话中与该参数关联的值。若参数不存在,则返回 null。 | |
将当前会话的所有属性作为枚举列表返回 | |
关闭当前会话。与之关联的所有信息将被销毁。 |
4.3. 示例 1
我们提供一个摘自Wrox出版、Eyrolles发行的优秀著作《J2EE编程》的示例。该书是Java Web解决方案开发人员获取高级知识的宝库。书中作为单个Java Servlet介绍的应用程序,在此已被改编为一个主Servlet,该Servlet通过调用JSP页面向客户端显示各种可能的响应。
该应用程序名为“sessions”,在 <tomcat>\conf\server.xml 文件中的配置如下:
在上述 docBase 文件夹中,您将找到以下元素:

error.jsp、invalid.jsp 和 valid.jsp 这三个文件均与 sessions 应用程序相关。在上述 WEB-INF 文件夹中,我们发现:

上图所示为 sessions 应用程序的 web.xml 配置文件。在 classes 文件夹中,您将找到 Servlet 类文件:

该应用程序的 web.xml 文件如下:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>cycledevie</servlet-name>
<servlet-class>cycledevie</servlet-class>
<init-param>
<param-name>urlSessionValide</param-name>
<param-value>/valide.jsp</param-value>
</init-param>
<init-param>
<param-name>urlSessionInvalide</param-name>
<param-value>/invalide.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreur</param-name>
<param-value>/erreur.jsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>cycledevie</servlet-name>
<url-pattern>/cycledevie</url-pattern>
</servlet-mapping>
</web-app>
主 Servlet 名为 cycledevie(servlet-name),并与 cycledevie.class 文件(servlet-class)相关联。它有一个别名 /cycledevie(servlet-mapping),允许通过 URL http://localhost:8080/sessions/cycledevie 调用它。它有三个初始化参数:
显示当前会话属性的页面 URL | |
当前会话失效后显示的页面 URL | |
主 Servlet 初始化错误时显示的页面 URLcycledevie |
会话应用程序的组件如下:
主Servlet - 分析客户端请求:
| |
| |
当用户使当前会话失效时显示。随后会提示创建一个新的会话。 | |
当主 Servlet 在初始化过程中遇到错误时显示。 |
主Servlet的生命周期如下:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class cycledevie extends HttpServlet{
// instance variables
String msgErreur=null;
String urlSessionInvalide=null;
String urlSessionValide=null;
String urlErreur=null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// was the initialization successful?
if(msgErreur!=null){
// we hand over to the error page
getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
}
// retrieve the current session
HttpSession session=request.getSession();
// analyze the action to be taken
String action=request.getParameter("action");
// invalidate current session
if(action!=null && action.equals("invalider")){
// the current session is invalidated
session.invalidate();
// we hand over to the urlSessionInvalide url
getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
}
// other cases
// we hand over to the urlSessionInvalide url
getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request,response);
}
//-------- INIT
public void init(){
// retrieve initialization parameters
ServletConfig config=getServletConfig();
urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
urlSessionValide=config.getInitParameter("urlSessionValide");
urlErreur=config.getInitParameter("urlErreur");
// parameters ok?
if(urlSessionValide==null || urlSessionInvalide==null){
msgErreur="Configuration incorrecte";
}
}
}
请注意以下几点:
- 在初始化方法中,Servlet 会获取其三个参数
- 在处理(doGet)请求时,Servlet:
- 首先检查初始化过程中是否发生错误。如果发生任何错误,则重定向至 error.jsp 页面。
- 检查 action 参数的值。如果该参数的值为“invalid”,Servlet 将重定向至 invalid.jsp 页面;否则,将重定向至 valid.jsp 页面。
JSP 页面 **valide.jsp** 显示当前会话的特征:
<%@ page import="java.util.*" %>
<%
// jspService
// ici on est dans le cas où on doit décrire la session en cours
String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- top of page HTML -->
<html>
<meta http-equiv="pragma" content="no-cache">
<head>
<title>Cycle de vie d'une session</title>
</head>
<body>
<h3>Cycle de vie d'une session</h3>
<hr>
<br>Etat session : <%= etat %>
<br>ID session : <%= session.getId() %>
<br>Heure de création : <%= new Date(session.getCreationTime()) %>
<br>Heure du dernier accès : <%= new Date(session.getLastAccessedTime()) %>
<br>Intervalle maximum d'inactivité : <%= session.getMaxInactiveInterval() %>
<br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
<br><a href="/sessions/cycledevie">Recharger la page</a>
<body>
</html>
请注意在该行中
中,我们使用了一个不知从何而来的会话对象。实际上,该对象是 JSP 页面可用的隐式对象之一,就像我们之前遇到的 request、response、out、config(ServletConfig)和 context(ServletContext)对象一样。页面上的两个链接指向了前面介绍的生命周期 Servlet:
<br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
<br><a href="/sessions/cycledevie">Recharger la page</a>
用于注销会话的链接包含 action=invalidate 参数,这使生命周期 Servlet 能够识别用户希望注销当前会话。另一个链接用于刷新页面。为防止浏览器从缓存中获取页面,需使用以下 HTML 指令:
。它指示浏览器不要对接收到的页面使用缓存。
invalidate.jsp 页面内容如下:
<!-- top of page HTML -->
<html>
<head>
<title>Cycle de vie d'une session</title>
</head>
<body>
<h3>Cycle de vie d'une session</h3>
<hr>
Votre session a été invalidée
<a href="/sessions/cycledevie">Créer une nouvelle session</a>
</body>
</html>
它提供了一个不带 action 参数的 cycledevie servlet 链接。此链接将导致 cycledevie servlet 创建一个新会话。
error.jsp 页面如下所示:
<%
// jspService
// ici on est dans le cas où on doit décrire la session en cours
String msgErreur= request.getAttribute("msgErreur");
if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- top of page HTML -->
<html>
<head>
<title>Cycle de vie d'une session</title>
</head>
<body>
<h3>Cycle de vie d'une session</h3>
<hr>
Application indisponible(<%= msgErreur %>)
</body>
</html>
其作用是显示生命周期 Servlet 发送给它的错误消息。现在让我们来看一些执行示例。首次请求该 Servlet:

上图表明我们处于一个新的会话中。我们点击“重新加载页面”链接:

上一个结果表明,我们仍处于与上一页相同的会话中(ID相同)。请注意,该会话的最后访问时间已发生变化。现在,让我们点击“失效会话”链接:

请注意此新页面中带有 action=invalidate 参数的 URL。现在,让我们使用“创建新会话”链接来创建一个新会话:

我们可以看到一个新的会话已经开始。在之前的示例中,会话依赖于 Cookie 机制。现在让我们在浏览器中禁用 Cookie 并重复测试。以下示例是在 Netscape Communicator 上进行的。出于某些无法解释的原因,使用 IE6 进行的测试产生了意想不到的结果,仿佛 IE6 即使在禁用 Cookie 后仍继续使用它们。首次请求 cycledevie Servlet:

现在我们点击“重新加载页面”链接:

我们可以观察到两点:
- 会话 ID 已发生变化
- Servlet 将该会话识别为新会话
Tomcat 服务器为用户在浏览器中禁用 Cookie 的问题提供了解决方案。它使用两种机制来实现本段开头提到的令牌:Cookie 和 URL 重写。如果会话 Cookie 不可用,它将尝试从客户端请求的 URL 中检索令牌。 要使此机制生效,URL 必须包含该令牌。一般而言,HTML 文档中所有指向 Web 应用程序的链接都必须包含该应用程序的令牌。这可以通过调用 encodeURL 方法来实现:
该方法会将当前会话令牌添加到作为参数传递的 URL 中,格式为 URL;jsessionid=xxxx |
我们将应用程序修改如下:
- 在 cycledevie.java Servlet 中,URL 经过了编码:
// we hand over to the error page
getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
// we hand over to the urlSessionInvalide url
getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
// we hand over to the urlSessionInvalide url
getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
- 在 valide.jsp 页面中,URL 经过编码:
<%
// jspService
// ici on est dans le cas où on doit décrire la session en cours
String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
// encodage URL cycledevie
String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>
............
<br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
<br><a href="<%= URLcycledevie %>">Recharger la page</a>
- 在 invalid.jsp 页面中,URL 经过了编码:
<%
// jspservice - on invalide la session en cours
session.invalidate();
// encodage URL cycledevie
String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>
..........
<a href="<%= URLcycledevie %>">Créer une nouvelle session</a>
现在我们可以进行测试了。我们使用的是 Netscape 4.5,并且已禁用 Cookie。我们首次请求 cycledevie Servlet:

然后通过“重新加载页面”链接刷新页面:

我们可以看到:
- 会话状态未发生变化(ID相同)
- 如上方的“地址”字段所示,cycledevie servlet 的 URL 确实包含该令牌
- 因此,Tomcat 服务器会从请求的 URL 中检索会话令牌(前提是开发人员已将其进行编码)。
4.4. 示例 2
下面我们通过一个示例展示如何在客户端会话中存储信息。在此示例中,唯一的信息是一个计数器,该计数器会在用户每次调用 Servlet 的 URL 时递增。首次调用时,将显示以下页面:

如果您点击上方的“重新加载页面”链接,将看到以下新页面:

该应用程序包含三个组件:
- 一个处理客户端请求的 Servlet
- 一个显示计数器值的 JSP 页面
- 一个用于显示错误信息的 JSP 页面
这三个组件已安装在正在使用的“sessions”Web应用程序中。其 web.xml 文件已进行修改以配置新的 Servlet:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
...
<servlet>
<servlet-name>compteur</servlet-name>
<servlet-class>compteur</servlet-class>
<init-param>
<param-name>urlAffichageCompteur</param-name>
<param-value>/compteur.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreur</param-name>
<param-value>/erreurcompteur.jsp</param-value>
</init-param>
</servlet>
...
<servlet-mapping>
<servlet-name>compteur</servlet-name>
<url-pattern>/compteur</url-pattern>
</servlet-mapping>
</web-app>
- 该 Servlet 名为 counter(servlet-name),并关联到类文件 counter.class(servlet-class)
- 它有两个初始化参数:
- displayCounterURL:显示计数器的 JSP 页面的 URL
- urlErreur:显示错误信息的 JSP 页面的 URL
- 并有一个别名 /compteur,这意味着它将通过 URL http://localhost:8080/sessions/compteur 被调用
compteur.java Servlet 代码如下:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class compteur extends HttpServlet{
// instance variables
String msgErreur=null;
String urlAffichageCompteur=null;
String urlErreur=null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// was the initialization successful?
if(msgErreur!=null){
// we hand over to the error page
getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
}
// retrieve the current session
HttpSession session=request.getSession();
// and the
String compteur=(String)session.getAttribute("compteur");
if(compteur==null) compteur="0";
// counter incrementation
try{
compteur=""+(Integer.parseInt(compteur)+1);
}catch(Exception ex){}
// save counter in session
session.setAttribute("compteur",compteur);
// and in the query
request.setAttribute("compteur",compteur);
// hand over to counter display url
getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request,response);
}
//-------- INIT
public void init(){
// retrieve initialization parameters
ServletConfig config=getServletConfig();
urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
urlErreur=config.getInitParameter("urlErreur");
// parameters ok?
if(urlAffichageCompteur==null){
msgErreur="Configuration incorrecte";
}
}
}
该 Servlet 的结构与我们之前遇到的 Servlet 相同。请注意计数器的处理方式:
- 通过 request.getSession() 获取会话
- 通过 session.getAttribute("counter") 从该会话中获取计数器
- 如果获取到的值为 null,则表示会话刚刚开始。此时将计数器设置为 0。
- 计数器被递增,存储回会话中(session.setAttribute("counter", counter)),并放入将传递给显示 Servlet 的请求中(request.setAttribute("counter", counter)。
显示页面 compteur.jsp 如下所示:
<%
// jspService
// on récupère le compteur
String compteur= (String) request.getAttribute("compteur");
if(compteur==null) compteur="inconnu";
%>
<!-- top of page HTML -->
<html>
<head>
<title>Comptage au fil d'une session</title>
</head>
<body>
<h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
<hr>
compteur = (<%= compteur %>)
<br><a href="/sessions/compteur">Recharger la page</a>
</body>
</html>
上方的页面仅需获取主Servlet传递给它的计数器属性(request.getAttribute("counter")并将其显示出来。
错误页面 *erreurcompteur.jsp* 如下所示:
<%
// jspService
// une erreur s'est produite
String msgErreur= request.getAttribute("msgErreur");
if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of page HTML -->
<html>
<head>
<title>Comptage au fil d'une session</title>
</head>
<body>
<h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
<hr>
Application indisponible(<%= msgErreur %>)
</body>
</html>
4.5. 示例 3
我们建议编写一个 Java 应用程序,使其作为前一个计数器应用程序的客户端。该程序将连续调用该应用程序 N 次,其中 N 作为参数传递。我们的目标是演示一个编程实现的 Web 客户端以及如何管理 Cookie。我们将以同一位作者在 Java 讲义中介绍的通用 Web 客户端作为起点。其调用方式如下:
webclient URL GET/HEAD
- URL:请求的 URL
- GET/HEAD:GET 用于请求页面的 HTML 代码,HEAD 用于将响应限制为仅包含 HTTP 头部
以下是一个使用 URL http://localhost:8080/sessions/compteur 的示例:
E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/compteur GET
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 14:21:18 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=B8A9076E552945009215C34A97A0EC5D;Path=/sessions
<!-- top of page HTML -->
<html>
<head>
<title>Comptage au fil d'une session</title>
</head>
<body>
<h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
<hr>
compteur = (1)
<br><a href="/sessions/compteur">Recharger la page</a>
</body>
</html>
clientweb 程序会显示从服务器接收到的所有内容。在上文中,我们可以看到 HTTP Set-cookie 命令,服务器使用该命令向客户端发送一个 Cookie。此处的 Cookie 包含两部分信息:
- JSESSIONID,即会话令牌
- Path 字段定义了 Cookie 所属的 URL。Path=/sessions 表示每次浏览器请求以 /sessions 开头的 URL 时,都必须将该 Cookie 发回给服务器。 在 sessions 应用程序中,我们使用了多种 Servlet,包括 /sessions/lifecycle 和 /sessions/counter Servlet。如果调用 /sessions/cycledevie Servlet,浏览器将收到一个 J 令牌。如果随后使用同一浏览器调用 /sessions/compteur Servlet,浏览器会将 J 令牌发回给服务器,因为该令牌适用于所有以 /sessions 开头的 URL。 在本例中,cycledevie 和 compteur Servlet 无需共享同一会话令牌。因此,它们本不应被放置在同一个 Web 应用程序中。这一点需要牢记:同一应用程序内的所有 Servlet 共享同一会话令牌。
- Cookie 还可以设置过期时间。此处缺少该信息,因此当浏览器关闭时,该 Cookie 将被删除。例如,Cookie 的过期时间可以设为 N 天。只要它在有效期内,每次访问其所属域名(Path)下的任一 URL 时,浏览器都会将其发回。 以一家在线CD商店为例。它可以通过追踪客户在其目录中的浏览路径,逐步确定客户的偏好——例如古典音乐。这些偏好可以存储在一个有效期为3个月的Cookie中。如果同一位客户在一个月后重返该网站,浏览器会将该Cookie发回给服务器应用程序。基于Cookie中包含的信息,服务器应用程序随后可以根据客户的偏好定制生成的页面。
以下是 Web 客户端代码。它将作为另一个客户端的起点。
// imported packages
import java.io.*;
import java.net.*;
public class clientweb{
// requests a URL
// displays its contents on the screen
public static void main(String[] args){
// syntax
final String syntaxe="pg URI GET/HEAD";
// number of arguments
if(args.length != 2)
erreur(syntaxe,1);
// note the URI required
String URLString=args[0];
String commande=args[1].toUpperCase();
// URI validity check
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrect
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
// order verification
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// incorrect order
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// extract useful information from URL
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// we can work
Socket client=null; // the customer
BufferedReader IN=null; // the customer's reading flow
PrintWriter OUT=null; // the customer's writing flow
String réponse=null; // server response
try{
// connect to the server
client=new Socket(host,port);
// create customer input/output flows TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// request URL - send HTTP headers
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// we read the answer
while((réponse=IN.readLine())!=null){
// the answer is processed
System.out.println(réponse);
}//while
// it's over
client.close();
} catch(Exception e){
// we handle the exception
erreur(e.getMessage(),4);
}//catch
}//hand
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
}//class
现在我们编写 clientCounter 程序,其调用方式如下:
clientCounter URL N [JSESSIONID]
- URL:计数器 Servlet 的 URL
- N:调用此 Servlet 的次数
- JSESSIONID:可选参数——会话令牌
该程序的目的是通过管理会话 Cookie 向计数器 Servlet 发起 N 次调用,并每次显示服务器返回的计数器值。在完成 N 次调用后,计数器值必须为 N。以下是一个初始的执行示例:
E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/counter 3
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A;Path=/sessions
cookie trouvÚ : 92DB3808CE8FCB47D47D997C8B52294A
compteur : 1
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
compteur : 2
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
compteur : 3
程序显示:
- 以以下形式显示其发送给服务器的 HTTP 头部 -->
- 接收到的 HTTP 头部
- 每次调用后的计数器值
我们可以看到在第一次调用期间:
- 客户端未发送 Cookie
- 服务器发送了一个 Cookie
对于后续请求:
- 客户端会系统性地回传在首次请求中从服务器接收到的 Cookie。正是这一点使得服务器能够识别该客户端并递增其计数器。
- 服务器不再发送 Cookie
我们将上述令牌作为第三个参数传入,重新运行之前的程序:
E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/compteur 3 92DB3808CE8FCB47D47D997C8B52294A
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
compteur : 4
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
compteur : 5
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
compteur : 6
这里我们可以看到,一旦客户端发出第一个请求,服务器就会收到一个有效的会话 Cookie。需要注意的是,对于 Tomcat 而言,会话的默认最大闲置时间为 20 分钟(实际上这是可配置的)。如果程序的第二个请求能够及时发送在第一个请求中收到的 Cookie,服务器就会将其视为同一会话。这凸显了一个潜在的安全漏洞。 如果我能在网络上截获一个会话令牌,我就能冒充发起该会话的用户。在我们的示例中,第一次调用代表发起会话的用户(可能使用具有获取令牌权限的用户名和密码),而第二次调用代表从第一次调用中“窃取”了会话令牌的用户。如果当前操作是一笔银行交易,这可能会引发严重问题……
客户端代码如下:
// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientCompteur{
// requests a URL
// displays its contents on the screen
public static void main(String[] args){
// syntax
final String syntaxe="pg URL-COMPTEUR N [JSESSIONID]";
// number of arguments
if(args.length !=2 && args.length != 3)
erreur(syntaxe,1);
// note the URL required
String URLString=args[0];
// URL validity check
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrect
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
// check number of calls N
int N=0;
try{
N=Integer.parseInt(args[1]);
if(N<=0) throw new Exception();
}catch(Exception ex){
// incorrect N argument
erreur("Le nombre d'appels N doit être un entier >0",3);
}
// has the JSESSIONID token been passed as a parameter?
String JSESSIONID="";
if (args.length==3) JSESSIONID=args[2];
// extract useful information from URL
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// we can work
Socket client=null; // the customer
BufferedReader IN=null; // the customer's reading flow
PrintWriter OUT=null; // the customer's writing flow
String réponse=null; // server response
// the model searched for in HTTP headers
Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
// the model searched for in the HTML code
Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
// the result of the model comparison
Matcher résultat=null;
// a Boolean giving the result of the counter search
boolean compteurTrouvé;
try{
// we make N calls to the server
for(int i=0;i<N;i++){
// connect to the server
client=new Socket(host,port);
// create customer input/output flows TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// request URL - send HTTP headers
envoie(OUT,"GET " + path + query + " HTTP/1.1");
envoie(OUT,"Host: " + host + ":" + port);
if(! JSESSIONID.equals("")){
envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
}
envoie(OUT,"Connection: close");
envoie(OUT,"");
// we read the response through to the end of the headers, looking for any cookies
while((réponse=IN.readLine())!=null){
// follow-up response
System.out.println(réponse);
// empty line?
if(réponse.equals("")) break;
// line HTTP not empty
// if you don't have the session token, look for it
if (JSESSIONID.equals("")){
// compare the HTTP line with the cookie template
résultat=modèleCookie.matcher(réponse);
if(résultat.find()){
// we found the cookie
JSESSIONID=résultat.group(1);
}
}
}//while
// that's it for HTTP headers - move on to HTML code
compteurTrouvé=false;
while((réponse=IN.readLine())!=null){
// does the current line contain the counter?
if (! compteurTrouvé){
résultat=modèleCompteur.matcher(réponse);
if(résultat.find()){
// counter found - displayed
System.out.println("compteur : " + résultat.group(1));
compteurTrouvé=true;
}
}
}//while
// it's over
client.close();
}//for
} catch(Exception e){
// we handle the exception
erreur(e.getMessage(),4);
}//catch
}//hand
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
// monitoring client-server exchanges
public static void envoie(PrintWriter OUT,String msg){
// sends message to server
OUT.println(msg);
// screen tracking
System.out.println("--> "+msg);
}//error
}//class
让我们来分析一下这个程序的关键点:
- 我们需要进行 N 次客户端-服务器交互。这就是为什么它们被放在循环中的原因
- 每次交互时,客户端都会与服务器建立一个 TCP/IP 连接。连接建立后,它会将请求的 HTTP 头部发送给服务器:
// on demande l'URL - envoi des entêtes HTTP
envoie(OUT,"GET " + path + query + " HTTP/1.1");
envoie(OUT,"Host: " + host + ":" + port);
if(! JSESSIONID.equals("")){
envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
}
envoie(OUT,"Connection: close");
envoie(OUT,"");
如果 JSESSIONID 令牌可用,则将其作为 Cookie 发送;否则,则不发送。
- 请求发送后,客户端将等待服务器的响应。它首先检查该响应的 HTTP 头部,以查找可能存在的 Cookie。为了找到它,它会将收到的行与 Cookie 的正则表达式进行比对:
// the model searched in HTTP headers
Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
// we read the response through to the end of the headers, looking for any cookies
while((réponse=IN.readLine())!=null){
// follow-up response
System.out.println(réponse);
// empty line?
if(réponse.equals("")) break;
// line HTTP not empty
// if you don't have the session token, look for it
if (JSESSIONID.equals("")){
// compare the HTTP line with the cookie template
résultat=modèleCookie.matcher(réponse);
if(résultat.find()){
// we found the cookie
JSESSIONID=résultat.group(1);
}
}
}//while
- 一旦首次找到该令牌,后续对服务器的调用将不再搜索该令牌。处理完响应的 HTTP 头部后,我们将转而分析该响应的 HTML 代码。在其中,我们寻找提供计数器值的行。此搜索同样使用正则表达式进行:
// the counter model searched for in the HTML code
Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
// that's it for HTTP headers - move on to HTML code
compteurTrouvé=false;
while((réponse=IN.readLine())!=null){
// does the current line contain the counter?
if (! compteurTrouvé){
résultat=modèleCompteur.matcher(réponse);
if(résultat.find()){
// counter found - displayed
System.out.println("compteur : " + résultat.group(1));
compteurTrouvé=true;
}
}
}//while
4.6. 示例 4
在上一个示例中,Web 客户端将令牌作为 Cookie 返回。我们看到它也可以将令牌包含在请求的 URL 本身中,形式为 URL;jsessionid=xxx。让我们验证一下这一点。将 clientCompteur.java 程序重命名为 clientCompteur2.java,并按以下方式进行修改:
....
// on demande l'URL - envoi des entêtes HTTP
if(JSESSIONID.equals(""))
envoie(OUT,"GET " + path + query + " HTTP/1.1");
else envoie(OUT,"GET " + path + query + ";jsessionid=" + JSESSIONID + " HTTP/1.1");
envoie(OUT,"Host: " + host + ":" + port);
envoie(OUT,"Connection: close");
envoie(OUT,"");
....
因此,客户端通过 GET URL;jsessionid=xx HTTP/1.1 请求计数器 URL,且不再发送 Cookie。这是唯一的更改。以下是首次调用的结果:
E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/counter 2
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=48A6DBA8357D808EC012AAF3A2AFDA63;Path=/sessions
cookie trouvÚ : 48A6DBA8357D808EC012AAF3A2AFDA63
compteur : 1
--> GET /sessions/compteur;jsessionid=48A6DBA8357D808EC012AAF3A2AFDA63 HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
compteur : 2
在第一次请求中,客户端未携带会话令牌请求该 URL。服务器响应时发送了令牌。随后,客户端重新请求同一 URL,并在 URL 后附加了收到的令牌。我们可以看到计数器已递增,这证明服务器正确识别出这是同一会话。
4.7. 示例 5
本例展示了一个由三个页面组成的应用程序,我们将它们分别命名为 page0、page1 和 page2。用户必须按以下顺序访问这些页面:
- page0 是一个用于收集信息的表单:姓名
- page1 是作为对 page0 表单提交的响应而收到的表单。它请求第二项信息:年龄
- page2 是一个 HTML 文档,用于显示从 page0 获取的姓名以及从 page1 获取的年龄。
此处包含三次客户端与服务端之间的交互:
- 在第一次交互中,客户端请求 page0 表单,服务器将其发送
- 在第二次交互中,客户端请求 page1 表单,服务器将其发送。客户端将姓名发送给服务器。
- 在第三次交互中,客户端请求 page3 文档,服务器将其发送。客户端将年龄发送给服务器。page3 文档必须显示姓名和年龄。姓名是在第二次交互中由服务器获取的,此后已被“遗忘”。使用会话来存储第二次交互中的姓名,以便在第三次交互中可用。
在第一次交互中获取的 page0 文档如下:

我们填写姓名字段:

我们点击“下一步”按钮,随后进入以下页面1:

我们填写年龄字段:

我们点击“下一步”按钮,随后会看到以下页面2:

当页面0提交至服务器时,如果“姓名”字段为空,服务器可能会返回错误代码:

当您将 page1 提交至服务器时,如果年龄字段无效,服务器可能会返回错误代码:

该应用程序由一个Servlet和四个JSP页面组成:
显示 page0 | |
显示 page1 | |
显示 page2 | |
显示错误页面 |
该 Web 应用程序名为 suitedepages,在 Tomcat 的 server.xml 文件中配置如下:
suitedepages 应用程序的 web.xml 配置文件如下:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>main</servlet-name>
<servlet-class>main</servlet-class>
<init-param>
<param-name>urlPage0</param-name>
<param-value>/page0.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPage1</param-name>
<param-value>/page1.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPage2</param-name>
<param-value>/page2.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreur</param-name>
<param-value>/erreur.jsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>main</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
</web-app>
主 Servlet 命名为 main,并通过其别名(servlet-mapping)可通过 URL http://localhost:8080/suitedepages/main* 访问。它有四个初始化参数,即用于不同视图的四个 JSP 页面的 URL。main* Servlet 的代码如下:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.util.regex.*;
public class main extends HttpServlet{
// instance variables
String msgErreur=null;
String urlPage0=null;
String urlPage1=null;
String urlPage2=null;
String urlErreur=null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// was the initialization successful?
if(msgErreur!=null){
// we hand over to the error page
getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
}
// we retrieve the step parameter
String étape=request.getParameter("etape");
// retrieve the current session
HttpSession session=request.getSession();
// the current step is processed
if(étape==null) étape0(request,response,session);
if(étape.equals("1")) étape1(request,response,session);
if(étape.equals("2")) étape2(request,response,session);
// other cases are invalid
étape0(request,response,session);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request,response);
}
//-------- INIT
public void init(){
// retrieve initialization parameters
ServletConfig config=getServletConfig();
urlPage0=config.getInitParameter("urlPage0");
urlPage1=config.getInitParameter("urlPage1");
urlPage2=config.getInitParameter("urlPage2");
urlErreur=config.getInitParameter("urlErreur");
// parameters ok?
if(urlPage0==null || urlPage1==null || urlPage2==null){
msgErreur="Configuration incorrecte";
}
}
//-------- step0
public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// we set a few attributes
request.setAttribute("nom","");
// we present page 0
request.getRequestDispatcher(urlPage0).forward(request,response);
}
//-------- step1
public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// retrieve the name from the query
String nom=request.getParameter("nom");
// name positioned?
if(nom==null) étape0(request,response,session);
// remove any spaces from the name
nom=nom.trim();
// we put it in a query attribute
request.setAttribute("nom",nom);
// empty name?
if(nom.equals("")){
// it's a mistake
ArrayList erreurs=new ArrayList();
erreurs.add("Nous n'avez pas indiqué de nom");
// put the errors in the query
request.setAttribute("erreurs",erreurs);
// back to page 0
étape0(request,response,session);
}
// valid name - stored in the current session
session.setAttribute("nom",nom);
// set the age attribute in the query
request.setAttribute("age","");
// we present page 1
request.getRequestDispatcher(urlPage1).forward(request,response);
}
//-------- step2
public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// retrieve the name from the session
String nom=(String)session.getAttribute("nom");
// name positioned?
if(nom==null) étape0(request,response,session);
// we put it in a query attribute
request.setAttribute("nom",nom);
// the age is retrieved from the query
String age=request.getParameter("age");
// age positioned?
if(age==null){
// back to page 1
request.setAttribute("age","");
request.getRequestDispatcher(urlPage1).forward(request,response);
}
// the age is stored in the query
age=age.trim();
request.setAttribute("age",age);
// valid age?
if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
// this is a mistake
ArrayList erreurs=new ArrayList();
erreurs.add("Age invalide");
// put the errors in the query
request.setAttribute("erreurs",erreurs);
// back to page 1
request.getRequestDispatcher(urlPage1).forward(request,response);
}
// age valid - page 2 is presented
request.getRequestDispatcher(urlPage2).forward(request,response);
}
}
- init 方法会获取四个初始化参数,如果其中任何一个缺失,则设置一条错误信息
- 我们已经看到,请求由三个交互步骤组成。为了跟踪这些交互的当前阶段,page0 和 page1 表单中包含一个名为 "etape" 的隐藏变量,其值为 1(page0)或 2(page1)。该数字可解释为待显示的下一页的编号。 在 doGet 方法中,该参数从请求中获取,并根据其值将处理委托给其他三个方法:
- étape0 处理初始请求并发送 page0
- étape1 处理 page0 上的表单,若发生错误则发送 page1 或再次发送 page0
- étape0 处理 page1 上的表单,并发送 page2;若发生错误,则再次发送 page1
- étape0
- 显示一个名称为空的 page0
- 步骤 1
- 从 page0 表单中检索“name”参数。
- 检查名称是否存在(不为空)。如果不存在,则再次显示 page0,如同首次调用一样。
- 检查名称是否为空。如果是,则再次显示 page0 并附带一条错误消息。
- 将名称存储在当前会话中,如果名称有效,则显示 page1。
- 步骤2
- 从当前会话中检索 name 参数。
- 检查名称是否存在(不为空)。如果不存在,则再次显示 page0,如同首次调用一样。
- 从 page1 发送的当前请求中检索 age 参数。
- 检查 age 是否有效。如果无效,则再次显示 page1 并显示一条错误消息。
- 将 name 和 age 作为请求属性存储,如果 name 和 age 有效,则显示 page2。
page0.jsp 页面内容如下:
<%@ page import="java.util.*" %>
<% // page0.jsp
// on récupère les attributs de la requête
String nom=(String)request.getAttribute("nom");
ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
// attributs valides ?
if(nom==null){
// retour à la servlet principale
request.getRequestDispatcher("/main").forward(request,response);
}
%>
<html>
<head>
<title>page 0</title>
</head>
<body>
<h3>Page 0/2</h3>
<form name="frmNom" method="POST" action="/suitedepages/main">
<input type="hidden" name="etape" value="1">
<table>
<tr>
<td>Votre nom</td>
<td><input type="text" name="nom" value="<%= nom %>"></td>
</tr>
</table>
<input type="submit" value="Suite">
</form>
<% // erreurs ?
if (erreurs!=null){
%>
<hr>
<font color="red">
Les erreurs suivantes se sont produites
<ul>
<% for(int i=0;i<erreurs.size();i++){ %>
<li><%= erreurs.get(i) %>
<% }//for %>
</ul>
<% }//if %>
</body>
</html>
- 在以下两种情况下,主Servlet可以调用page0.jsp页面:
- 在初始请求期间
- 在处理 page0 表单后发生错误时
- 主 Servlet 会提供 `nameToDisplay` 参数以及任何错误列表。因此,page0.jsp Servlet 首先会检索这两项信息。
- 表单通过隐藏字段“etape”提交至主 Servlet,该字段用于标识用户当前处于应用程序的哪个阶段。
page1.jsp 页面内容如下:
<%@ page import="java.util.*" %>
<% // page1.jsp
// on récupère les attributs de la requête
String nom=(String)request.getAttribute("nom");
String age=(String)request.getAttribute("age");
ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
// attributs valides ?
if(nom==null || age==null){
// retour à la servlet principale
request.getRequestDispatcher("/main").forward(request,response);
}
%>
<html>
<head>
<title>page 1</title>
</head>
<body>
<h3>Page 1/2</h3>
<form name="frmAge" method="POST" action="/suitedepages/main">
<input type="hidden" name="etape" value="2">
<table>
<tr>
<td>Nom</td>
<td><font color="green"><%= nom %></font></td>
</tr>
<tr>
<td>Votre âge</td>
<td><input type="text" name="age" size="3" value="<%= age %>"></td>
</tr>
</table>
<input type="submit" value="Suite">
</form>
<% // erreurs ?
if (erreurs!=null){
%>
<hr>
<font color="red">
Les erreurs suivantes se sont produites
<ul>
<% for(int i=0;i<erreurs.size();i++){ %>
<li><%= erreurs.get(i) %>
<% }//for %>
</ul>
<% }//if %>
</body>
</html>
page1.jsp 页面的结构与 page0.jsp 页面类似,不同之处在于它现在从主 Servlet 接收两个属性:name 和 age。最后,page2.jsp 页面如下所示:
<%
// page2.jsp
// on récupère les attributs de la requête
String nom=(String)request.getAttribute("nom");
String age=(String)request.getAttribute("age");
// attributs valides ?
if(nom==null || age==null){
// retour à la servlet principale
request.getRequestDispatcher("/main").forward(request,response);
}
%>
<html>
<head>
<title>page 2</title>
</head>
<body>
<h3>Page 2/2</h3>
<table>
<tr>
<td>Nom</td>
<td><font color="green"><%= nom %></font></td>
</tr>
<tr>
<td>Votre âge</td>
<td><font color="green"><%= age %></font></td>
</tr>
</table>
</body>
</html>
page2.jsp 页面也会从主 Servlet 接收 name 和 age 属性,并简单地显示它们。最后,error.jsp 页面负责在 Servlet 初始化错误时显示错误信息,其代码如下:
<%
// jspService
// une erreur s'est produite
String msgErreur= request.getAttribute("msgErreur");
if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of page HTML -->
<html>
<head>
<title>Suite de pages</title>
</head>
<body>
<h3>Suite de pages</h3>
<hr>
Application indisponible(<%= msgErreur %>)
</body>
</html>
它显示由主Servlet传递给它的msgError属性。
综上所述,我们可以看到,在应用程序的三个阶段中,浏览器总是首先查询主Servlet。然而,生成待显示响应的并非主Servlet,而是四个JSP页面中的一个。用户不会察觉这一点,因为浏览器会在其“地址”字段中继续显示最初请求的URL——即主Servlet的URL。
