ITEEDU

第6章 会话管理

  在本章中,我们将要讨论的是如何使用Servlet API来管理某个用户在多个HTTP请求中都要使用的数据。请注意HTTP是一个无状态协议,它不像TCP协议那样在会话过程中保持一个到服务器的连接。说明维护会话信息的重要性的经典例子是Internet网上购物系统。这里,系统必须保存每一个用户购物车里商品的列表。为了实现这一功能,服务器必须能够区分不同的客户,而且还要有一种为每一个客户存储数据的方法。

6.1 会话跟踪

  Servlet API提供了一种简单而又高效的模型来跟踪会话信息。在Web服务器看来,一个会话是由在一次浏览过程中所发出的全部HTML请求组成的。换句话说,一次会话是从你打开浏览器开始到你关闭浏览器结束。会话跟踪的第一个障碍就是如何惟一标识每一个客户会话。这只能通过为每一个客户分配一个某种标识,并将这些标识保存在客户端上,以后客户端发给服务器的每一个HTML请求都提供这些标识来实现。那么为什么不能用客户机的IP地址作为标识呢?这是因为在一台客户机上可能同时发出多个不同的客户的请求,而且,如果多个不同客户的请求还可能是通过代理服务器发出的。在这些情况下,IP地址并能作为惟一标识。正如我们将要看到的,惟一的标识是通过使用一种称为URL rewriting的技术或者用永久cookies来保存的。
为了更好地说明使用servlet API是如何管理会话信息的,我们现在就直接看一个简单的例子。图6.1显示的是这个servlet的源程序,这个servlet保存了在当前浏览会话中用户访问站点次数。

 package javaservlets.session;

import javax.servlet.*;
import javax.servlet.http.*;

/**
* <p>This is a simple servlet that uses session tracking and
* Cookies to count the number of times a client session has
* accessed this servlet.
*/

public class Counter extends HttpServlet
{
// Define our counter key into the session
static final String COUNTER_KEY = "Counter.count";

/**
* <p>Performs the HTTP GET operation
*
* @param req The request from the client
* @param resp The response from the servlet
*/

public void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, java.io.IOException
{
// Get the session object for this client session.
// The parameter indicates to create a session
// object if one does not exist
HttpSession session = req.getSession(true);

// Set the content type of the response
resp.setContentType("text/html");

// Get the PrintWriter to write the response
java.io.PrintWriter out = resp.getWriter();

// Is there a count yet?
int count = 1;
Integer i = (Integer) session.getValue(COUNTER_KEY);

// If a previous count exists, set it
if (i != null) {
count = i.intValue() + 1;
}

// Put the count back into the session
session.putValue(COUNTER_KEY, new Integer(count));

// Print a standard header
out.println("<html>");
out.println("<head>");
out.println("<title>Session Counter</title>");
out.println("</head>");
out.println("<body>");
out.println("Your session ID is <b>" +
session.getId());
out.println("</b> and you have hit this page <b>" +
count +
"</b> time(s) during this browser session");

out.println("<form method=GET action=\"" +
req.getRequestURI() + "\">");
out.println("<input type=submit " +
"value=\"Hit page again\">");
out.println("</form>");

// Wrap up
out.println("</body>");
out.println("</html>");
out.flush();
} 
图6.1 Counter.java代码清单

  servlet中使用HttpservletRequest对象的getSession方法来取得当前的用户会话。GetSession的参数决定了如果会话尚不存在,getSession是否创建一个新会话。还有一个版本的getSession没有任何参数,它将缺省地创建一个新会话。当一个新用户第一次调用servlet引擎时,这将会强制产生一个新的会话。请注意,我是说servlet引擎而不是某一个servlet。所有的会话数据都是由servlet引擎来维护的,并且在servlet之间共享。这样你就可以使用一组servlet一起为一个客户会话服务了。另外,Servlet API规范上指出:“为了确保会话被正确维护,servlet的开发都必须在提交应答之前调用getSession方法。”这正是说,在向应答的输出流中写入之前,你一定要调用getSession方法。
一旦你获得了会话对象,它工作起来就像标准Java的哈希表或字典一样。使用一个惟一的键,你可以在会话对象中加入或者获取任何对象。由于会话数据是由servlet引擎维护存储的,你在为这些键赋值时一定要注意维护它的惟一性。我建议将servlet的名字甚至它的包名作为键的一部分,这样你就不会不小心修改其他servlet设置的键值了。
除了保存应用程序数据,会话对象还包含许多访问会话属性的方法,包括会话的标识(可以用getId方法取得)。图6.2显示了Counter Servlet的运行结果。
用户第一次打开Counter Servlet的时候,如果会话还不存在,就会创建一个新的会话(一定要注意,其他servlet可能已经建立了这个用户的会话对象)。通过一个惟一键从会话对象中取得一个整数,如果这个整数还不存在,就使用初始值1,否则每一次给这个整数加1。最后,新的值被写回会话对象。一个简单的HTML页被返回给浏览器显示,它显示了会话ID以及用户通过单击“Hit Page again”打开Counter这一页的次数。

6.1.1 管理会话数据

  管理会话数据的三个方面必须牢记在心,它们是:会话交换(swapping)、会话重定位(relocation)和会话持久性(persistence)。因为在规范中并没有明确规定,所以会话数据的管理方式决定于你所使用的servlet引擎的实现。不过无论特定的servlet引擎如何管理会话数据,你都可以确信,只有实现了java.io.Serializable接口的数据对象才能够被交换、重定位或是保持。序列化功能是在JDK1.1中引入的,它可以将对象的状态信息写入任意的输出流中(如文件)。在第10章中的applet到servlet的通信中,我们还会用到序列化功能。

  会话交换
所有的servlet引擎都只有有限的资源可以用来保存会话信息,比如服务器上的内存就只有那么多。为了尽量保持资源的消耗可以得到控制,大多数servlet引擎都为某一时刻可以驻留在内存中的会话数量定义了上限。由于在HttpSession对象中保存了最后访问的时间,最近最少使用(Least Recently Used,LRU)算法实现起来较容易。如果会话的个数超过了这个上限,最老的一个会话就会被序列化(也就是实现了java.io.Serializable接口)。如果会话不能被序列化,servlet引擎就不得不把它一直保存在内存中。一旦交换到磁盘上的会话被请求,它就会被读出并重新入在内存里。

  会话重定位
并没有servlet请求总是一定要用相同的Java虚拟机来响应的要求,甚至可以用多个服务器来响应servlet的请求。健壮的servlet引擎和应用程序服务器具有内置的负载平衡功能,这样,所有的请求就可以尽快地得到响应。为了让所有的servlet引擎和应用程序服务器都能正确处理会话中存储的数据,会话对象必须可以重定位。同样,这要求会话中存储的所有数据实现java.io.Serializable接口。如果会话对象和会话中存储的所有数据都可以序列化,把会话对象从一个虚拟机移动到另外一个虚拟机就简单得多了(正如我们将在第16章中看到的,这是RMI的基础模块之一)。请注意,下面的引擎必须处理如何同步会话数据的问题。

  会话持久性
Web服务器都声称提供每周7天,一天24小时不间断的服务。不过,任何主机都会有停机检修的时候,比如停下来清除内存泄漏,它们难道在开玩笑吗?又是序列化在起作用。一个servlet服务器可以在关机时简单地将所有的会话对象以及其中的数据保存到磁盘上,然后在启动时再重新加载。这样,刚把最后一件商品装入购物车的用户恐怕还要因为什么都没少而感谢你呢?

6.1.2 会话的生存期

  没有什么是永恒的,会话信息也不例外。所有的servlet引擎在会话保持一段时间的空闲之后,最终使它们无效。大多数引擎(Java Web Servlet,ServletExec和JRun)都允许你配置这个超时时间的长度。一个会话无效时都会发生什么呢?首先,服务器将会释放该会话绑定的所有值,然后将会话对象释放,这样虚拟机就可以收集这些空间以便以后分配,后面我们还会看到如何得到一个对象解除其在会话中的绑定时的消息。

6.1.3 浏览会话

  为了更好地说明会话的用法,我们先来开发一个显示当前servlet引擎上所有会话的信息的servlet。值得注意的是由于安全方面的考虑,HttpSessionContext对象——通过它可以得到当前会话ID的列表——在Sevlet API2.1版本以后已经被弃用了,所以使用2.1以及后续的Servlet API时,这个servlet将不能正确运行。通过图6.3所示的Killer Servlet,你不但可以列出所有的会话以及它们当前绑定的数据,而且还可以手工地使任何会话无效,这显然是一个灾难!

 package javaservlets.session;

import javax.servlet.*;
import javax.servlet.http.*;

/**
* <p>This servlet gathers all of the information about all of
* the sessions and returns a formatted table as part of a
* form. The user can then kill any of the sessions by
* clicking a checkbox. This servlet on functions
* prior to version 2.1 of the Servlet API
*/

public class Killer extends HttpServlet
{
/**
* <p>Performs the HTTP GET operation
*
* @param req The request from the client
* @param resp The response from the servlet
*/
public void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, java.io.IOException
{
// Requesting more informatin about a particular
// session?
String info = req.getParameter("info");
if (info != null) {
getInfo(info, req, resp);
return;
}

// Set the content type of the response
resp.setContentType("text/html");

// Force the browser not to cache
resp.setHeader("Expires", "Tues, 01 Jan 1980 00:00:00 GMT");

// Get the PrintWriter to write the response
java.io.PrintWriter out = resp.getWriter();

// Write the page header
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet Session Killer</title>");
out.println("</head>");
out.println("<body>");

out.println("This page lists all of the current session ");
out.println("information. Check the session and press ");
out.println("'Kill' to remove the session. WARNING - ");
out.println("Killing an active session my cause problems ");
out.println("for some clients.<br>");

// If the user presses 'kill' send the request to ourselves
out.println("<form method=GET action=\"" +
req.getRequestURI() + "\">");
out.println("<center><table border>");
out.println("<tr><th>Kill</th><th>Session ID</th>" +
"<th>Last Accessed</th></tr>");

// Get the HttpSessionContext object which holds
// all of the session data
HttpSession session = req.getSession(true);
HttpSessionContext context = session.getSessionContext();

// Get the session IDs to kill
String toKill[] = req.getParameterValues("id");
if (toKill != null) {

// Loop through and kill them
for (int i = 0; i < toKill.length; i++) {
HttpSession curSession = context.getSession(toKill[i]);

// Invalidate the session
if (curSession != null) {
getServletContext().log("Killing session " +
curSession.getId());
curSession.invalidate();
}
}
}

// Enumerate through the list of sessions
java.util.Enumeration enum = context.getIds();
while (enum.hasMoreElements()) {
String sessionID = (String) enum.nextElement();

// Format the table entry
out.println("<tr><td><input type=checkbox name=id " +
"value=\"" + sessionID + "\"></td>");
out.println("<td><a href=\"" + req.getRequestURI() +
"?info=" + sessionID + "\">" +
sessionID + "</td>");

// Get the last time accessed
String time = "";
HttpSession curSession = context.getSession(sessionID);
if (curSession != null) {
long last = curSession.getLastAccessedTime();
time = (new java.util.Date(last)).toString();
}
out.println("<td>" + time + "</td></tr>");
}
out.println("</table><br>");
out.println("<input type=submit " +
"value=\"Kill Marked Sessions\">");
out.println("</center></form>");

// Wrap up
out.println("</body>");
out.println("</html>");
out.flush();
}

/**
* <p>Displays a page with detailed session info
* @param id The session id
* @param req The request from the client
* @param resp The response from the servlet
*/
public void getInfo(String id, HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, java.io.IOException
{
// Set the content type of the response
resp.setContentType("text/html");

// Force the browser not to cache
resp.setHeader("Expires", "Tues, 01 Jan 1980 00:00:00 GMT");

// Get the PrintWriter to write the response
java.io.PrintWriter out = resp.getWriter();

// Write the page header
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet Session Information</title>");
out.println("</head>");
out.println("<body>");

// Get the HttpSessionContext object which holds
// all of the session data
HttpSession session = req.getSession(true);
HttpSessionContext context = session.getSessionContext();

// Attempt to find the session
HttpSession curSession = context.getSession(id);
if (curSession == null) {
out.println("Session " + id + " not found");
}
else {

out.println("<center><h1>Information for session " +
id + "</h1>");

// Display a table with all of the info
out.println("<table border>");

// Creation time
long creationTime = curSession.getCreationTime();
out.println("<tr><td>Creation Time<td><td>" +
(new java.util.Date(creationTime)) +
"</td></tr>");

// Last accessed time
long lastTime = curSession.getLastAccessedTime();
out.println("<tr><td>Last Access Time<td><td>" +
(new java.util.Date(lastTime)) +
"</td></tr>");

out.println("</table>");

// Get an array of value names
String names[] = curSession.getValueNames();

if ((names != null) && (names.length > 0)) {
out.println("<br><h1>Bound objects</h1>");

// Display a table with all of the bound values
out.println("<table border>");

for (int i = 0; i < names.length; i++) {
out.println("<tr><td>" + names[i] + "</td><td>" +
curSession.getValue(names[i]) +
"</td></tr>");
}
out.println("</table>");
}

out.println("</center>");
}



// Wrap up
out.println("</body>");
out.println("</html>");
out.flush();
}
图6.3 Killer.java代码清单

  Killer Servlet通过HttpSessionContext对象取得当前所有会话的ID列表,然后取得每一个会话的数据并把它格式化为HTML页面(如图6.4所示)。请注意响应的首部,它被设置成强制浏览器不要对当前页进行高速缓存。通过将失效时间设置为1980年,的确使用我测试过的所有浏览器都重新下载页面而不是从高速缓存中存取。
用户可以单击某个会话ID旁边的复选框,然后按下“Kill Marked Sessions”按钮来向Killer Servlet发出一个请求,这时所有被选中的会话都会被设为无效。这看上去是不是很危险?
如果用户单击某个会话ID,那么一个使Killer Servlet显示这个会话的详细信息的请求就会被提交。其结果如图6.5所示。不但这个会话的相关信息(诸如创建时间和上次访问时间)被显示出来,而且绑定在这个会话上的所有数据键以及它们的当前值都可以被显示出来。这也是说明为什么将可以取得所有会话信息的HttpSessionContext类删除的一个很好的例子。如果有人可以取得你的服务器上所有会话的信息,你会怎么想呢?

6.2 Cookies

  上面我们已经看到了如何用Servlet API跟踪会话,但是我们实际上还不知道怎样在客户机和服务器之间维护惟一的会话ID。一种方法是使用cookies,这最早是由Netscape引入的。cookie是一小块可以嵌入在HTTP请求和应答中的数据。典型情况下,Web服务器将cookie值嵌入到应答的首部,而浏览器则在其以后的请求中都将携带同样的cookie。cookie的信息中可以有一部分用来存储会话ID,这个ID被服务器用来将某些HTTP请求绑定在会话中。cookie由浏览器保存在客户端,通常保存在一个名为cookie.txt的文件。cookie还含有一些其他属性,诸如可选的注释、版本号以及最长生存期。图6.6显示了cookies servlet的代码,它显示了当前请求的首部所包含的所有cookie的一些信息。图6.7显示的是它的输出结果。

package javaservlets.session;

import javax.servlet.*;
import javax.servlet.http.*;

/**
* <p>This is a simple servlet that displays all of the
* Cookies present in the request
*/
public class Cookies extends HttpServlet
{

/**
* <p>Performs the HTTP GET operation
*
* @param req The request from the client
* @param resp The response from the servlet
*/
public void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, java.io.IOException
{

// Set the content type of the response
resp.setContentType("text/html");

// Get the PrintWriter to write the response
java.io.PrintWriter out = resp.getWriter();

// Get an array containing all of the cookies
Cookie cookies[] = req.getCookies();

// Write the page header
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet Cookie Information</title>");
out.println("</head>");
out.println("<body>");

if ((cookies == null) || (cookies.length == 0)) {
out.println("No cookies found");
}
else {

out.println("<center><h1>Cookies found in the request</h1>");

// Display a table with all of the info
out.println("<table border>");
out.println("<tr><th>Name</th><th>Value</th>" +
"<th>Comment</th><th>Max Age</th></tr>");

for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
out.println("<tr><td>" + c.getName() + "</td><td>" +
c.getValue() + "</td><td>" +
c.getComment() + "</td><td>" +
c.getMaxAge() + "</td></tr>");
}

out.println("</table></center>");
}

// Wrap up
out.println("</body>");
out.println("</html>");
out.flush();
}
} 
图6.6 Cookies.java代码清单

  值得注意的是HttpServletRequest对象有一个getCookies的方法,它可以返回当前请求中的cookie对象的一个数组。我个人认为getCookies方法返回一个数组而不是一个枚举和其他Java对象(如哈希表的方法)是很不协调的。也许这一点会在下一版本的规范中被改正。
关于是否应当使用cookie有很多的争论,因为一些人认为cookie可能会造成对隐私权的侵犯(当然事实上尚未发现)。有鉴于此,大部分浏览器允许用关闭cookie功能,然而这使我们跟踪会话变得更加困难。如果不能依赖cookie的支持又该怎办呢?你将不得不使用URL rewriting,这种方法很多久以来就被CGI所使用。

6.3 URL Rewriting

  那么我们如何面对那些必须关闭cookie支持以保证超出一般的安全的信息技术部门中的用户,以及那些固执地使用古老的没有cookie支持的浏览器上网冲浪的用户呢?我们不得不使用URL Rewriting技术。servlet创建的所有链接的重定向都必须将会话ID编码为URL的一部分。在服务器指定的URL编码的方法中,最可能的一种是给URL加入一些参数或者附加的路径信息。
为了说明URL Rewriting技术,我们对Counter Servlet略作修改。图6.8显示了CounterRewrite Servlet的源程序,它使用URL Rewriting技术来在HTTP请求之间维护会话信息。

  package javaservlets.session;

import javax.servlet.*;
import javax.servlet.http.*;

/**
* This is a simple servlet that uses session tracking 
* and URL rewriting to count the number of times a client session
* has accessed this servlet.
*/

public class CounterRewrite extends HttpServlet
{
// Define our counter key into the session
static final String COUNTER_KEY = "CounterRewrite.count";

/**
*Performs the HTTP GET operation
*
* @param req The request from the client
* @param resp The response from the servlet
*/

public void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, java.io.IOException
{
// Set the content type of the response
resp.setContentType("text/html");

// Get the PrintWriter to write the response
java.io.PrintWriter out = resp.getWriter();

// Get the session
HttpSession session = req.getSession(true);

// Is there a count yet?
int count = 1;
Integer i = (Integer) session.getValue(COUNTER_KEY);

// If a previous count exists, set it
if (i != null) {
count = i.intValue() + 1;
}

// Put the count back into the session
session.putValue(COUNTER_KEY, new Integer(count));

// Print a standard header
out.println("<html>");
out.println("<head>");
out.println("<title>Session Counter " +
"with URL rewriting</title>");
out.println("</head>");
out.println("<body>");
out.println("Your session ID is <b>" +
session.getId());
out.println("</b> and you have hit this page <b>" +
count +
"</b> time(s) during this browser session");

// Format the URL
String url = req.getRequestURI(); //+ ";" + SESSION_KEY +
//session.getId();

out.println("<form method=POST action=\"" +
resp.encodeUrl(url) + "\">");
out.println("<input type=submit " +
"value=\"Hit page again\">");
out.println("</form>");

// Wrap up
out.println("</body>");
out.println("</html>");
out.flush();
}

/**
* 

Performs the HTTP GET operation * * @param req The request from the client * @param resp The response from the servlet */ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, java.io.IOException { // Same as get request doGet(req, resp); } }

图6.8 CounterRewrite.java代码清单

  请注意,主要的修改在于我们在表单的ACTION语句中写URL的方式。encodeURL方法被用来修改URL,使URL包含会话ID;类似的,encodeRedirectURL被用来重定向页面。
想看看它做得怎么样吗?关闭你的液晶的cookie支持,然后打开以前的那个CounterServlet。如果你再次打开该页,你会发现生成了新的会话ID而访问计数总是1。这是因为由于客户没有保存cookie的信息,使得servlet引擎无法将同一客户的HTTP请求绑定一起。如果你调用CounterRewrite Servlet,你会发现会话ID被保持,而每次访问该页面都会使计数器加1。如图6.9所示。
如果你看一下在浏览器地址栏中的URL地址,你将会了解到encodeURL所加进去的附加信息(很明显,这里我使用的是JRun,而其他的servlet引擎也是用类似的方法实现的)。

6.4 不使用浏览器的会话跟踪

  如果你使用了浏览器,那么cookie和URL Rewriting技术都会做得很好,但是如果我们想要编写一个独立运行的Java应用程序,使它可以与管理会话信息的servlet直接通信又该怎么办呢?我们不得不手工从服务器的第一次应答中取得会话ID,然后在以后的每一次请求的首部都设置这个会话ID。我们创建了一个简单的应用程序来说明这一切是怎么实现的。这个应用程序从servlet中取得计数器的值。第10章深入探讨了applet和servlet的通信,所以在这里,我们主要关注的是在Java中如何读取和设置cookie。图6.10显示了CounterJava Servlet的源程序,这个servlet使用会话跟踪来保持一个计数器。主要的不同在于我们只要将这个整数计数器的值通过DataOutputStream返回就可以了,而不必将结果组织成HTML格式以便浏览器显示。

    package javaservlets.session;
     import javax.servlet.*;   
     import javax.servlet.http.*;   
     import java.io.*;
    /**  * <p>This servlet shows how to send a session count
      * to a client using data input/output streams.
      */
      public class CounterJava extends HttpServlet
      {
      // Define our counter key into the session
      static final String COUNTER_KEY = "CounterJava.count";
      
      /**
      * <p>Services the HTTP request
      *
      * @param req The request from the client
      * @param resp The response from the servlet
      */
      public void service(HttpServletRequest req,
      HttpServletResponse resp)
      throws ServletException, java.io.IOException
      {
      // Get the session
      HttpSession session = req.getSession(true);
        // Is there a count yet?
      int count = 1;
      Integer i = (Integer) session.getValue(COUNTER_KEY);
         // If a previous count exists, set it
      if (i != null) {
      count = i.intValue() + 1;
      }
         // Put the count back into the session
      session.putValue(COUNTER_KEY, new Integer(count));
      
      // Get the input stream for reading data from the client
      DataInputStream in =
      new DataInputStream(req.getInputStream());
         // We'll be sending binary data back to the client so
      // set the content type appropriately
      resp.setContentType("application/octet-stream");
         // Data will always be written to a byte array buffer so
      // that we can tell the client the length of the data
      ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
         // Create the output stream to be used to write the
      // data to our buffer
      DataOutputStream out = new DataOutputStream(byteOut);
         // If there is any data being sent from the client,
      // read it here
         // Write the data to our internal buffer.
      out.writeInt(count);
         // Flush the contents of the output stream to the
      // byte array
      out.flush();
         // Get the buffer that is holding our response
      byte[] buf = byteOut.toByteArray();
      
      // Notify the client how much data is being sent
      resp.setContentLength(buf.length);
         // Send the buffer to the client
      ServletOutputStream servletOut = resp.getOutputStream();
         // Wrap up
      servletOut.write(buf);
      servletOut.close();
      }
      }
图6.10 CounterJava.java代码清单

  这个servlet的会话管理部分看上去非常熟悉吧?实际上它和我们以前看到的counterServlet是一样的。不过,这个servlet创建了一个DataOutputStream并用它向调用者写回二进制数据,而不是返回格式化了的HTML。
实际上关键在于客户,也就是我们的Java应用程序是如何管理cookie信息的。这个Java应用程序的基本流程如下:
1.通过java.net.URLConnection类与servlet建立连接。
2.向servlet发出请求。
3.读入应答,如果还没有设置会话ID,就从应答中析取出会话ID。
4.在后面的所有对servlet的请求中设置cookie值。
图6.11显示了一个客户应用程序的源程序。

   package javaservlets.session;
      import java.io.*;
      /**
      * <p>This application shows how maintain cookies manually
      * by using a simple counter servlet
      */
      public class CounterApp
      {
      // The servlet url
      String m_url;
      
      // The value of the session cookie
      String m_cookie = null;
      
      /**
      * <p>Application entry point. This application requires
      * one parameter, which is the servlet URL
      */
      public static void main(String args[])
      {
      // Make sure we have an argument for the servlet URL
      if (args.length == 0) {
      System.out.println("\nServlet URL must be specified");
      return;
      }
         try {
          // Create a new object
      CounterApp app = new CounterApp(args[0]);
          // Get the count multiple times
      for (int i = 1; i <=5; i++) {
      int count = app.getCount();
      System.out.println("Pass " + i + " count=" 
      + count);
      }
      }
      catch (Exception ex) {
      ex.printStackTrace();
      }
      
      }
        /**
      * Construct a new CounterApp object
      * @param url The servlet url
      */
      public CounterApp(String url)
      {
      m_url = url;
      }
      
      /**
      * Invokes a counter servlet and returns the hit count
      * that was returned by the servlet
      */
      public int getCount() throws Exception
      {
      // Get the server URL 
      java.net.URL url = new java.net.URL(m_url);
      
      // Attempt to connect to the host
      java.net.URLConnection con = url.openConnection();
      
      // Set the session ID if necessary
      if (m_cookie != null) {
      con.setRequestProperty("cookie", m_cookie);
      }
      
      // Initialize the connection
      con.setUseCaches(false);
      con.setDoOutput(true);
      con.setDoInput(true);
         // Data will always be written to a byte array buffer so
      // that we can tell the server the length of the data
      ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
      
      // Create the output stream to be used to write the
      // data to our buffer
      DataOutputStream out = new DataOutputStream(byteOut);
      
      // Send any data to the servlet here
      
      // Flush the data to the buffer
      out.flush();
      
      // Get our buffer to be sent
      byte buf[] = byteOut.toByteArray();
      
      // Set the content that we are sending
      con.setRequestProperty("Content-type",
      "application/octet-stream");
      
      // Set the length of the data buffer we are sending
      con.setRequestProperty("Content-length",
      "" + buf.length);
      
      // Get the output stream to the server and send our
      // data buffer
      DataOutputStream dataOut =
      new DataOutputStream(con.getOutputStream());
      dataOut.write(buf);
      
      // Flush the output stream and close it
      dataOut.flush();
      dataOut.close();
      
      // Get the input stream we can use to read the response
      DataInputStream in =
      new DataInputStream(con.getInputStream());
      
      // Read the data from the server
      int count = in.readInt();
      
      // Close the input stream
      in.close();
         // Get the session cookie if we haven't already
      if (m_cookie == null) {
      String cookie = con.getHeaderField("set-cookie");
      if (cookie != null) {
      m_cookie = parseCookie(cookie);
      System.out.println("Setting session ID=" + m_cookie);
      }
      }
      
      return count;
      }
        /**
      * Parses the given cookie and returns the cookie key
      * and value. For simplicity the key/ value is assumed
      * to be before the first ';', as in:
      *
      * jrunsessionid=3509823408122; path=/
      *
      * @param rawCookie The raw cookie data
      * @return The key/value of the cookie
      */
      public String parseCookie(String raw)
      {
      String c = raw;
         if (raw != null) {
          // Find the first ';'
      int endIndex = raw.indexOf(";");
          // Found a ';', assume the key/value is prior
      if (endIndex >= 0) {
      c = raw.substring(0, endIndex);
      }
      }
      
      return c;
      }
      }
图6.11 CounterApp代码清单

  值得注意的是会话ID是如何从应答的“set-cookie”头中取得的,以及如何在请求中设置请求的“cookie”属性来设置cookie值。图6.12显示了调用这个应用程序后每一次从servlet中取得的计数器值。为了提高可读性,我把命令分成了两行,它们应该作为一行输入。
java javaservlets.session.CounterApp
http://larryboy/servlet/javaservlets.session.CounterJava
Setting session ID=jrunsessionid=917315535100303809
Pass 1 count=1
Pass 2 count=2
Pass 3 count=3
Pass 4 count=4
Pass 5 count=5
图6.12 应用程序CounterApp的运行结果

6.5 会话事件

  有些时候,我们需要获得一个对象被绑定到会话或者解除和会话绑定的消息:一个对象被绑定到会话上时,正是执行某种初始化操作,比如打开文件、启动数据库事务或记录某些统计信息的最好时机。而由于客户终止会话、会话超时或者servlet引擎超时造成一个对象解除和会话的绑定的时候,你可能想要执行一些清除操作,比如关闭文件、提交或回滚数据库事务以及记录某些统计信息。考虑到这些,Servlet API的作者提供了一个HttpSessionBindingListener接口。这个接口有两个方法必须实现:valueBound和valueUnbount。我想它们的名字就是对它们的最好的解释。为了更好地说明如何使用HttpSessionBindingListener接口,我们来看一个简单的servlet例子,这个servlet创建一个对象实例,并把它绑定在一个会话上。图6.13显示了它的源程序。

 package javaservlets.session;
    import javax.servlet.*;
      import javax.servlet.http.*;
      /**
      * <p>This is a simple servlet that binds an object that
      * implements the HttpSessionBindingListener interface.
      */
      public class Binder extends HttpServlet
      {
      /**
      * <p>Performs the HTTP GET operation
      *
      * @param req The request from the client
      * @param resp The response from the servlet
      */
      
      public void doGet(HttpServletRequest req,
      HttpServletResponse resp)
      throws ServletException, java.io.IOException
      {
      // Set the content type of the response
      resp.setContentType("text/html");
         // Get the PrintWriter to write the response
      java.io.PrintWriter out = resp.getWriter();
         // Get the session object for this client session.
      // The parameter indicates to create a session
      // object if one does not exist
      HttpSession session = req.getSession(true);// Create a new SessionObject
      SessionObject o = new SessionObject();
    
    // Put the new SessionObject into the session
      session.putValue("Binder.object", o);
      
      // Print a standard header
      out.println("<html>");
      out.println("<head>");
      out.println("<title>Session Binder</title>");
      out.println("</head>");
      out.println("<body>");
      out.println("Object bound to session " +
      session.getId());
      
      // Wrap up
      out.println("</body>");
      out.println("</html>");
      out.flush();
      }
图6.13 Binder.java代码清单

  这里实在没有什么新的东西,前面我们已经看到如何从请求中取得会话以及如何向会话中加入对象。不同在于那个加入到会话中的数据对象——SessionObject——实现了HttpSessionBindingListener接口。如图6.14所示。现在,一旦SessionObject被加入到会话之中,servlet引擎就会调用valueBound方法。同样,当SessionObject被从会话中删除时,servlet引擎就会调用valueUnbound方法。

 package javaservlets.session;

import javax.servlet.*;
import javax.servlet.http.*;

/**
* 

This object demonstrates the use of the * HttpSessionBindingListener interface. */ public class SessionObject implements HttpSessionBindingListener { /** * Called when this object is bound into a session. * @param event The event */ public void valueBound(HttpSessionBindingEvent event) { // Output the fact that we are being bound System.out.println("" + (new java.util.Date()) + " Binding " + event.getName() + " to session " + event.getSession().getId()); } /** * Called when this object is unbound from a session * @param event The event */ public void valueUnbound(HttpSessionBindingEvent event) { // Output the fact that we are being bound System.out.println("" + (new java.util.Date()) + " Unbinding " + event.getName() + " from session " + event.getSession().getId()); } } 

图6.14 SessionObject.java代码清单

  对Binder Servlet的调用会使一个SessionObject被绑定到当前会话。由于SessionObject实现了HttpSessionBindingListener接口,这时,valueBound方法被调用。在我们的实现中,我们仅仅是将一些信息输出到当前的标准输出流。根据servlet引擎的不同,这些输出可能会写入记录文件或者显示在控制台上。当SessionObject从会话中删除时,valueUnbound方法被调用。图6.15显示了从初始化SessionObject的绑定到会话由于超时而解除对象的绑定所产生的输出。  

6.6 小结

  在本章中,我们讨论了如何使用Servlet API进行会话管理。我们还了解了如何用会话标识来惟一标识客户,以及两种保持会话标识的方法:使用cookie和URL Rewriting。最后,我们学习了会话的绑定事件以及如何使用HttpSessionBindingListener接口来记录一个会话中绑定和解除绑定的对象。