一、JSP和Servlet中的Cookie
由于HTTP協(xié)議是無狀態(tài)協(xié)議(雖然Socket連接是有狀態(tài)的,但每次用HTTP協(xié)議進(jìn)行數(shù)據(jù)傳輸后就關(guān)閉的Socket連接,因此,HTTP協(xié)議并不會(huì)保存上一次的狀態(tài)),因此,如果要保存某些HTTP請(qǐng)求過程中所產(chǎn)生的數(shù)據(jù),就必須要有一種類似全局變量的機(jī)制保證數(shù)據(jù)在不同的HTTP請(qǐng)求之間共享。這就是下面要講的Session和Cookie。
Cookie是通過將數(shù)據(jù)保存在客戶端的硬盤(永久Cookie)或內(nèi)存(臨時(shí)Cookie)中來實(shí)現(xiàn)數(shù)據(jù)共享的一種機(jī)制。在Windows下,保存在這些Cookie數(shù)據(jù)的目錄一般是C:\Documents and Settings\Administrator\Cookies。每一個(gè)Cookie有一個(gè)超時(shí)時(shí)間,如果超過了這個(gè)時(shí)間,Cookie將自動(dòng)失效。可按如下方法來設(shè)置Cookie的超時(shí)時(shí)間:
Cookie cookie = new Cookie("key","value");cookie.setMaxAge(3600); // Cookie的超時(shí)間為3600秒,也就是1小時(shí)
response.addCookie(cookie);
如果不使用setMaxAge方法,Cookie的超時(shí)時(shí)間為-1,在這種情況下,Cookie就是臨時(shí)Cookie,也就是說這種Cookie實(shí)際上并不保存在客戶端硬盤上,而是保存在客戶端內(nèi)存中的。讀者可以在JSP中運(yùn)行如下代碼,看看是否會(huì)在上面提到的保存cookie的目錄中生成cookie文件:
Cookie cookie = new Cookie("key","value");response.addCookie(cookie);
實(shí)際上使用setMaxAge將超時(shí)時(shí)間設(shè)為任意的負(fù)數(shù)都會(huì)被客戶端瀏覽器認(rèn)為是臨時(shí)
Cookie,如下面的代碼將在客戶端內(nèi)存中保存一個(gè)臨時(shí)Cookie:
Cookie cookie = new Cookie("key","value");cookie.setMaxAge(-100); // 將cookie設(shè)為臨時(shí)Cookie
response.addCookie(cookie);
如果第一次將Cookie寫入客戶端(不管是硬盤還是內(nèi)存),在同一臺(tái)機(jī)器上第二次訪問
該網(wǎng)站的jsp頁(yè)面時(shí),會(huì)自動(dòng)將客戶端的cookie作為HTTP請(qǐng)求頭的Cookie字段值傳給服務(wù)端,如果有多個(gè)Cookie,中間用";"隔開。如下面的HTTP請(qǐng)求頭所示:
GET /test/First.jsp HTTP/1.1
HOST:localhost
...
Cookie:key1=value1;key2=value2
...
...
我們可以在JSP中使用如下的Java代碼來輸出Cookie字段的值:
out.println(request.getHeader("Cookie"));
如果在Servlet中輸出,必須得使用如下語(yǔ)句得到out,才能向客戶端瀏覽器輸出數(shù)據(jù):
PrintWriter out = response.getWriter();
雖然永久Cookie和臨時(shí)Cookie在第二次向服務(wù)端發(fā)出HTTP請(qǐng)求時(shí)生成Cookie字段,但它們還是有一定的區(qū)別的。永久Cookie在任意新開啟的IE窗口都可以生成Cookie。而臨時(shí)Cookie由于只保存在當(dāng)前IE窗口,因此,在新開啟的IE窗口,是不能生成Cookie字段的,也就是說,新窗口和舊窗口是不能共享臨時(shí)Cookie的。使用重定向機(jī)制彈出的新窗口也無法和舊窗口共享臨時(shí)Cookie。但在同一個(gè)窗口可以。如在一個(gè)IE窗口輸入http://localhost:8080/test/first.jsp,向內(nèi)存寫入一個(gè)臨時(shí)Cookie后,在同一個(gè)IE窗口輸入http://localhost:8080/test/second.jsp,瀏覽器在向服務(wù)端發(fā)送HTTP請(qǐng)求時(shí),自動(dòng)將當(dāng)前瀏覽器的臨時(shí)Cookie(也就是first.jsp所創(chuàng)建的Cookie)和永久Cookie作為HTTP請(qǐng)求頭的Cookie字段值發(fā)送給服務(wù)端。但是如果新啟一個(gè)IE窗口,由于新IE窗口沒有這個(gè)臨時(shí)Cookie,因此,second.jsp只發(fā)送了保存在硬盤上的永久Cookie。
二、Tomcat中的Servlet和Session
由于Cookie數(shù)存在保存在客戶端,這樣對(duì)于一些敏感數(shù)據(jù)會(huì)帶來一些風(fēng)險(xiǎn)。而且Cookie一般只能保存字符串等簡(jiǎn)單數(shù)據(jù)。并且大小限制在4KB。如果要保存比較復(fù)雜的數(shù)據(jù),Cookie可能顯得有些不合適。基于這些原因,我們自然會(huì)想到在服務(wù)端采用這種類似Cookie的機(jī)制來存儲(chǔ)數(shù)據(jù)。這就是我們這節(jié)要講的會(huì)話(Session)。而在一個(gè)客戶端和服務(wù)端的會(huì)話中所有的頁(yè)面可以共享為這個(gè)會(huì)話所建立的Session。
那么什么是會(huì)話呢?有很多人認(rèn)為會(huì)話就是在一臺(tái)機(jī)器上客戶端瀏覽器訪問某個(gè)域名所指向的服務(wù)端程序,就建立了一個(gè)客戶端到服務(wù)端的會(huì)話。然后關(guān)閉客戶端瀏覽器,會(huì)話就結(jié)束。其實(shí)這并不準(zhǔn)確。
首先讓我們先來看看Session的原理。Session和Cookie類似。所不同的是它是建立在服務(wù)端的對(duì)象。每一個(gè)Session對(duì)象一個(gè)會(huì)話。也許很多讀者看到這會(huì)有一個(gè)疑問。Session是如何同客戶端聯(lián)系在一起的呢?很多人在使用Session時(shí)并沒有感覺到這一點(diǎn)。其實(shí)這一切都是Web服務(wù)器,如Tomcat一手包辦的。那么Web服務(wù)器又是如何識(shí)別通過HTTP協(xié)議進(jìn)行連接的客戶端的呢?這就要用到第一節(jié)中所講的Cookie。在一般情況下,Session使用了臨時(shí)Cookie來識(shí)別某一個(gè)Session是否屬于某一個(gè)會(huì)話。在本文中以Tomcat為例來說明Session是如何工作的。
讓我們先假設(shè)某一個(gè)客戶端第一次訪問一個(gè)Servlet,在這個(gè)Servlet中使用了getSession來得到一個(gè)Session對(duì)象,也就是建立了一個(gè)會(huì)話,這個(gè)Servlet的代碼如下:
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class First extends HttpServlet
{public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{response.setContentType("text/html");HttpSession session = request.getSession();
session.setAttribute("key", "mySessionValue");PrintWriter out = response.getWriter();
out.println("The session has been generated!");out.flush();
out.close();
}
}
對(duì)于服務(wù)端的First來說,getSession方法主要做了兩件事:
1. 從客戶端的HTTP請(qǐng)求頭的Cookie字段中獲得一個(gè)尋找一個(gè)JSESSIONID的key,這個(gè)key的值是一個(gè)唯一字符串,類似于D5A5C79F3C8E8653BC8B4F0860BFDBCD 。
2. 如果Cookie中包含這個(gè)JSESSIONID,將key的值取出,在Tomcat的Session Map(用于保存Tomcat自啟動(dòng)以來的所有創(chuàng)建的Session)中查找,如果找到,將這個(gè)Session取出,如果未找到,創(chuàng)建一個(gè)HttpSession對(duì)象,并保存在Session Map中,以便下一次使用這個(gè)Key來獲得這個(gè)Session。
在服務(wù)器向客戶端發(fā)送響應(yīng)信息時(shí),如果是新創(chuàng)建的HttpSession對(duì)象,在響應(yīng)HTTP
頭中加了一個(gè)Set-Cookie字段,并將JSESSIONID和相應(yīng)的值反回給客戶端。如下面的HTTP響應(yīng)頭:
HTTP/1.1 200 OK
...
Set-Cookie: JSESSIONID=D5A5C79F3C8E8653BC8B4F0860BFDBCD
...
對(duì)于客戶端瀏覽器來說,并不認(rèn)識(shí)哪個(gè)Cookie是用于Session的,它只是將相應(yīng)的臨時(shí)Cookie和永久Cookie原封不動(dòng)地放到請(qǐng)求HTTP頭的Cookie字段中,發(fā)送給服務(wù)器。如果在IE中首次訪問服務(wù)端的First,這時(shí)在當(dāng)前IE窗口并沒有臨時(shí)Cookie,因此,在請(qǐng)求HTTP頭中就沒有Cookie字段,所以First在調(diào)用getSession方法時(shí)就未找到JSESSIONID,因此,就會(huì)新建一個(gè)HttpSession對(duì)象。并在Set-Cookie中將這個(gè)JSESSIONID返回。接下來我們使用另外一個(gè)Servlet:Second來獲得在First中所設(shè)置的Session數(shù)據(jù)。Second的代碼如下:
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class Second extends HttpServlet
{public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{response.setContentType("text/html");HttpSession session = request.getSession();
PrintWriter out = response.getWriter();
out.println(session.getAttribute("key"));out.flush();
out.close();
}
}
如果在同一個(gè)窗口來調(diào)用Second。這時(shí)客戶端已經(jīng)有了一個(gè)臨時(shí)Cookie,就是JSESSIONID,因此,會(huì)將這個(gè)Cookie放到HTTP頭的Cookie字段中發(fā)送給服務(wù)端。服務(wù)端在收到這個(gè)HTTP請(qǐng)求時(shí)就可以從Cookie中得到JSESSIONID的值,并從Session Map中找到這個(gè)Session對(duì)象,也就是getSession方法的返回值。因此,從技術(shù)層面上來說,所有擁有同一個(gè)Session ID的頁(yè)面都應(yīng)該屬于同一個(gè)會(huì)話。
如果我們?cè)谝粋€(gè)新的IE窗口調(diào)用Second,并不會(huì)得到mySessionValue。因?yàn)檫@時(shí)Second和First擁有了不同的Session ID,因此,它們并不屬于同一個(gè)會(huì)話。講到這,也許很多讀者眼前一亮。既然擁有同一個(gè)Session ID,就可以共享Session對(duì)象,那么我們可不可以使用永久Cookie將這個(gè)Session ID保存在Cookie文件中,這樣就算在新的IE窗口,也可以共享Session對(duì)象了。答案是肯定的。下面是新的First代碼:
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class First extends HttpServlet
{public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{response.setContentType("text/html");HttpSession session = request.getSession();
session.setMaxInactiveInterval(3600);
Cookie cookie = new Cookie("JSESSIONID", session.getId());cookie.setMaxAge(3600);
response.addCookie(cookie);
session.setAttribute("key", "mySessionValue");PrintWriter out = response.getWriter();
out.println("The session has been generated!");out.flush();
out.close();
}
}
在上面的代碼中使用了Cookie對(duì)象將JSESSIONID寫入了Cookie文件,并使用setMaxAge方法將Cookie超時(shí)時(shí)間設(shè)為3600秒(1小時(shí))。這樣只要訪問過First,從訪問時(shí)間算起,在1小時(shí)之內(nèi),在本機(jī)的任何IE窗口調(diào)用Second都會(huì)得到"mySessionValue"字符串。
三三、Tomcat中的JSP和Session
從本質(zhì)上講,JSP在運(yùn)行時(shí)已經(jīng)被編譯成相應(yīng)的Servlet了,因此,在JSP和Servlet中Session的使用方法應(yīng)該差不多。但還是有一些細(xì)小的差別。
如果我們使用過JSP就會(huì)發(fā)現(xiàn),在JSP中很多對(duì)象是不需要?jiǎng)?chuàng)建的,如out、session等。它們可以直接使用。如下面的JSP代碼所示:
<!-- MyJSP.jsp -->
<%@ page language="java" contentType="text/html; charset=GB18030"
pageEncoding="GB18030"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB18030">
<title>Insert title here</title>
</head>
<body>
<%
out.println(session.getId());
%>
</body>
</html>
在上面的JSP代碼中直接使用了out和session。而并不象Servlet里一樣用get方法來獲得相應(yīng)的對(duì)象實(shí)例。那么這是為什么呢?
由于JSP在第一次運(yùn)行時(shí)是被編譯成Servlet的,我們自然就會(huì)想到有可能是在編譯JSP時(shí)自動(dòng)創(chuàng)建了session和out對(duì)象。下面我們就來驗(yàn)證這一點(diǎn)。首先需要查看一下JSP被編譯成Servlet后的源代碼。這非常簡(jiǎn)單,如果我們使用的是Tomcat,只需要在Tomcat的安裝目錄中的work中找相應(yīng)的源程序即可。如一個(gè)名為MyJSP.jsp的文件首先被編譯成MyJSP_jsp.java(這就是由JSP生成的Servlet源程序文件),然后再由java將MyJSP_jsp.java編譯成MyJSP_jsp.class,最后Tomcat運(yùn)行的就是MyJSP_jsp.class。如上面的JSP程序被編譯成Servlet的部分源代碼如下:
package org.apache.jsp;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
public final class MyJSP_jsp extends org.apache.jasper.runtime.HttpJspBase
{
... ...
... ...
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
PageContext pageContext = null;
HttpSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;
try {
response.setContentType("text/html; charset=GB18030");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write("\r\n");
out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\r\n");
out.write("<html>\r\n");
out.write("\t<head>\r\n");
out.write("\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=GB18030\">\r\n");
out.write("\t\t<title>Insert title here</title>\r\n");
out.write("\t</head>\r\n");
out.write("\t<body>\r\n");
out.write("\t\t");
out.println(session.getId());
out.write("\r\n");
out.write("\r\n");
out.write("\t</body>\r\n");
out.write("</html>");
} catch (Throwable t) {
if (!(t instanceof SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try { out.clearBuffer(); } catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
我們可以看到上面的代碼中的_jspService方法類似于HttpServlet中的service方法,在方法的開始部分首先建立了session、application、out等對(duì)象實(shí)例。然后將MyJSP.jsp中的HTML通過out輸出到客戶端。我們要注意上面的黑體字的語(yǔ)句:out.println(session.getId());,JSP編譯器自動(dòng)將JSP中的<% ... %>中包含的Java代碼原封不動(dòng)地插入到_jspService中。由于是在創(chuàng)建對(duì)象實(shí)例后插入,因此,就可以直接使用session、out等對(duì)象了。
如果我們想做進(jìn)一步的實(shí)驗(yàn),可以直接使用javac來編譯MyJSP_jsp.java,為了方便其間,首先建立一個(gè)c.cmd文件,它的內(nèi)容如下:
javac -classpath
D:\tools\apache-tomcat-6.0.13\lib\servlet-api.jar;D:\tools\apache-tomcat-6.0.13\lib\jsp-api.jar;D:\tools\apache-tomcat-6.0.13\lib\annotations-api.jar;D:\tools\apache-tomcat-6.0.13\lib\catalina.jar;D:\tools\apache-tomcat-6.0.13\lib\jasper.jar;D:\tools\apache-tomcat-6.0.13\lib\el-api.jar %1
其中D:\tools\apache-tomcat-6.0.13是tomcat的安裝目錄,讀者可以將其設(shè)為自己的機(jī)器上的tomcat安裝目錄
在編譯時(shí)可直接使用c MyJSP_jsp.java進(jìn)行編譯,這時(shí)tomcat就直接運(yùn)行我們編譯生成的MyJSP_jsp.class了。
從上面的代碼我們還可以了解一點(diǎn),在JSP無論使用還是不使用session,都會(huì)使用getSession方法創(chuàng)建一個(gè)Session對(duì)象,而Servlet必須顯式地調(diào)用才會(huì)建立Session對(duì)象。
注:通過直接編譯java文件運(yùn)行jsp,需要清除一下tomcat的緩存,一般需要重啟一下tomcat。
四、隨心所欲使用Session
(1) 使用url傳遞session id
在上面講過,在默認(rèn)情況下session是依靠客戶端的cookie來實(shí)現(xiàn)的。但如果客戶端瀏覽器不支持cookie或?qū)?span lang="EN-US">cookie功能關(guān)閉,那就就意味著無法通過cookie來實(shí)現(xiàn)session了。在這種情況下,我們還可以有另一種選擇,就是通過url來傳遞session id。
對(duì)于Tomcat來說,需要使用jsessionid作為key來傳遞session id。但具體如何傳呢?可能有很多人認(rèn)為會(huì)是如下的格式:
http://localhost:8080/test/MyJSP.jsp?jsessionid= D5A5C79F3C8E8653BC8B4F0860BFDBCD
但實(shí)驗(yàn)上面的url并不好使。其實(shí)最直接的方法我們可以看一下Tomcat的源程序是如何寫的,首先下載tomcat的源程序,然后找到CoyoteAdapter.java文件,并打開。在其中找到parseSessionId方法,這個(gè)方法是用來從url中提取Session id的。我們可以不必了解這個(gè)方法的全部代碼,只看一下開頭就可以。代碼片段如下:
ByteChunk uriBC = req.requestURI().getByteChunk();
int semicolon = uriBC.indexOf(match, 0, match.length(), 0);
if (semicolon > 0) {...}
上面代碼中的uriBC就是請(qǐng)求的url,第二行在這個(gè)url中查找match字符串,再在CoyoteAdapter.java中查找一個(gè)match字符串,match變量的初值如下:
private static final String match =
";" + Globals. SESSION_PARAMETER_NAME + "=";
從上面代碼可以看出,match開頭是一個(gè)";"字符,而SESSION_PARAMETER_NAME是一個(gè)常量,值就是"jsessionid",因此可以斷定,MyJSP.jsp后跟的是";",并不是"?",因此,正確的url如下:
http://localhost:8080/test/MyJSP.jsp;jsessionid= D5A5C79F3C8E8653BC8B4F0860BFDBCD
通過使用上述方法甚至可以在不同的機(jī)器上獲得同一個(gè)session對(duì)象。
在CoyoteAdapter.java文件中還有一個(gè)parseSessionCookiesId方法,這個(gè)方法將從HTTP請(qǐng)求頭中提取session id。我們中postParseRequest方法中可以看到將調(diào)用的parseSessionId方法,在最后調(diào)用了parseSessionCookiesId方法,因此,我們可以斷定,tomcat將考慮url中的session id,然后再讀取Cookie字段中的session id。還有就是在postParseRequest方法的最后部分有一個(gè)response.sendRedirect(redirectPath);,在調(diào)完它后,就直接return了。而沒有執(zhí)行到parseSessionCookiesId,因此,使用重定向并不能通過HTTP頭的cookie字段共享session。只能通過url來傳遞session id。
(2) 將tomcat的cookie支持關(guān)閉
如果我們只想使用url來支持session,可以直接將tomcat的cookie功能關(guān)閉。我們可
以修改conf中的context.xml文件,加入一個(gè)cookies="false"即可,內(nèi)容如下:
<!-- The contents of this file will be loaded for each web application -->
<Context cookies = "false">
... ...
... ...
</Context>
重啟tomcat后,就算客戶端支持cookie,tomcat也不會(huì)考慮HTTP請(qǐng)求頭的cookie字段。
(3) 在IE中控制Cookie
在IE中也可以將Cookie關(guān)閉,啟動(dòng)IE,在工具->Internet選項(xiàng)->穩(wěn)私->高級(jí)中選中"覆蓋自動(dòng)cookie處理"選項(xiàng)。并按圖1選擇:

圖1
對(duì)于下面的選項(xiàng)"總是允許會(huì)話cookie",如果不選,在本機(jī)將允許會(huì)話cookie,也就是通過localhost訪問,在遠(yuǎn)程將不允許會(huì)話cookie。我們也可以通過在工具->Internet選項(xiàng)->穩(wěn)私->站點(diǎn)來對(duì)某個(gè)網(wǎng)站來允許和拒絕cookie。