JSP/Servlet:Servlet/JSP會(huì )話(huà)跟蹤機制 (1)
一、Servlet的會(huì )話(huà)管理機制
根據設計,HTTP是一種無(wú)狀態(tài)的協(xié)議。它意味著(zhù)Web應用并不了解有關(guān)同一用戶(hù)以前請求的信息。維持會(huì )話(huà)狀態(tài)信息的方法之一是使用Servlet或者JSP容器提供的會(huì )話(huà)跟蹤功能。Servlet API規范定義了一個(gè)簡(jiǎn)單的HttpSession接口,通過(guò)它我們可以方便地實(shí)現會(huì )話(huà)跟蹤。
HttpSession接口提供了存儲和返回標準會(huì )話(huà)屬性的方法。標準會(huì )話(huà)屬性如會(huì )話(huà)標識符、應用數據等,都以“名字-值”對的形式保存。簡(jiǎn)而言之,HttpSession接口提供了一種把對象保存到內存、在同一用戶(hù)的后繼請求中提取這些對象的標準辦法。在會(huì )話(huà)中保存數據的方法是setAttribute(String s, Object o),從會(huì )話(huà)提取原來(lái)所保存對象的方法是getAttribute(String s)。
在HTTP協(xié)議中,當用戶(hù)不再活動(dòng)時(shí)不存在顯式的終止信號。由于這個(gè)原因,我們不知道用戶(hù)是否還要再次返回,如果不采取某種方法解決這個(gè)問(wèn)題,內存中會(huì )積累起大量的HttpSession對象。
為此,Servlet采用“超時(shí)限制”的辦法來(lái)判斷用戶(hù)是否還在訪(fǎng)問(wèn):如果某個(gè)用戶(hù)在一定的時(shí)間之內沒(méi)有發(fā)出后繼請求,則該用戶(hù)的會(huì )話(huà)被作廢,他的HttpSession對象被釋放。會(huì )話(huà)的默認超時(shí)間隔由Servlet容器定義。這個(gè)值可以通過(guò)getMaxInactiveInterval方法獲得,通過(guò)setMaxInactiveInterval方法修改,這些方法中的超時(shí)時(shí)間以秒計。如果會(huì )話(huà)的超時(shí)時(shí)間值設置成-1,則會(huì )話(huà)永不超時(shí)。Servlet可以通過(guò)getLastAccessedTime方法獲得當前請求之前的最后一次訪(fǎng)問(wèn)時(shí)間。
要獲得HttpSession對象,我們可以調用HttpServletRequest對象的getSession方法。為了正確地維持會(huì )話(huà)狀態(tài),我們必須在發(fā)送任何應答內容之前調用getSession方法。
用戶(hù)會(huì )話(huà)既可以用手工方法作廢,也可以自動(dòng)作廢。作廢會(huì )話(huà)意味著(zhù)從內存中刪除HttpSession對象以及它的數據。例如,如果一定時(shí)間之內(默認30分鐘)用戶(hù)不再發(fā)送請求,Java Web Server自動(dòng)地作廢他的會(huì )話(huà)。
Servlet/JSP會(huì )話(huà)跟蹤機制有著(zhù)一定的局限,比如:
? 會(huì )話(huà)對象保存在內存之中,占用了可觀(guān)的資源。
? 會(huì )話(huà)跟蹤依賴(lài)于Cookie。由于各種原因,特別是安全上的原因,一些用戶(hù)關(guān)閉了Cookie。
? 會(huì )話(huà)跟蹤要用到服務(wù)器創(chuàng )建的會(huì )話(huà)標識符。在多個(gè)Web服務(wù)器以及多個(gè)JVM的環(huán)境中,Web服務(wù)器不能識別其他服務(wù)器創(chuàng )建的會(huì )話(huà)標識符,會(huì )話(huà)跟蹤機制無(wú)法發(fā)揮作用。
要深入理解會(huì )話(huà)跟蹤機制,首先我們必須理解在Servlet/JSP容器中會(huì )話(huà)如何運作。
二、會(huì )話(huà)標識符
每當新用戶(hù)請求一個(gè)使用了HttpSession對象的JSP頁(yè)面,JSP容器除了發(fā)回應答頁(yè)面之外,它還要向瀏覽器發(fā)送一個(gè)特殊的數字。這個(gè)特殊的數字稱(chēng)為“會(huì )話(huà)標識符”,它是一個(gè)唯一的用戶(hù)標識符。此后,HttpSession對象就駐留在內存之中,等待同一用戶(hù)返回時(shí)再次調用它的方法。
在客戶(hù)端,瀏覽器保存會(huì )話(huà)標識符,并在每一個(gè)后繼請求中把這個(gè)會(huì )話(huà)標識符發(fā)送給服務(wù)器。會(huì )話(huà)標識符告訴JSP容器當前請求不是用戶(hù)發(fā)出的第一個(gè)請求,服務(wù)器以前已經(jīng)為該用戶(hù)創(chuàng )建了HttpSession對象。此時(shí),JSP容器不再為用戶(hù)創(chuàng )建新的HttpSession對象,而是尋找具有相同會(huì )話(huà)標識符的HttpSession對象,然后建立該HttpSession對象和當前請求的關(guān)聯(lián)。
會(huì )話(huà)標識符以Cookie的形式在服務(wù)器和瀏覽器之間傳送。如果瀏覽器不支持Cookie又如何呢?此時(shí),對服務(wù)器的后繼請求將不會(huì )帶有會(huì )話(huà)標識符。結果,JSP容器認為該請求來(lái)自一個(gè)新用戶(hù),它會(huì )再創(chuàng )建一個(gè)HttpSession對象,而以前創(chuàng )建的HttpSession對象仍舊駐留在內存中,但該用戶(hù)以前的會(huì )話(huà)信息卻丟失了。
另外,Servlet/JSP容器只認可它自己創(chuàng )建的會(huì )話(huà)標識符。如果同一Web應用在“Web農場(chǎng)”(Web farm)的多臺服務(wù)器上運行,則必須存在這樣一種機制:保證來(lái)自同一用戶(hù)的請求總是被定向到處理該用戶(hù)第一次請求的服務(wù)器。
三、偽會(huì )話(huà)管理機制
如前所述,基于Cookie的會(huì )話(huà)管理技術(shù)面臨著(zhù)種種問(wèn)題。下面我們要設計一種新的會(huì )話(huà)管理機制來(lái)解決這些問(wèn)題。這種會(huì )話(huà)管理機制稱(chēng)為“偽會(huì )話(huà)”(Pseudo Session)機制,它具有如下特點(diǎn):
? 對象和數據不是保存在內存中,而是以文本文件形式保存。每一個(gè)文本文件與一個(gè)特定的用戶(hù)關(guān)聯(lián),文件的名字就是會(huì )話(huà)的標識符。因此,文件名字必須是唯一的。
? 文本文件保存在一個(gè)專(zhuān)用的目錄中,所有Web服務(wù)器都可以訪(fǎng)問(wèn)這個(gè)目錄。因此,偽會(huì )話(huà)可以用于Web農場(chǎng)。
? 會(huì )話(huà)標識符不作為Cookie發(fā)送,而是直接編碼到URL里面。因此,采用偽會(huì )話(huà)技術(shù)要求修改所有的超級鏈接,包括HTML表單的ACTION屬性。
此外,實(shí)現偽會(huì )話(huà)管理機制時(shí)我們還要考慮到以下幾點(diǎn):
? 它應該與應用無(wú)關(guān),其他想要實(shí)現同樣功能的開(kāi)發(fā)者應該能夠方便地重用它。
? 考慮到安全原因,應該有一種為會(huì )話(huà)標識符生成隨機數字的辦法。
? 為了作廢過(guò)期的會(huì )話(huà),應該設定一個(gè)超時(shí)值。同一個(gè)用戶(hù),如果他超過(guò)一定的時(shí)間之后再次返回,他將獲得一個(gè)新的會(huì )話(huà)標識符。此舉能夠防止未經(jīng)授權的用戶(hù)冒用其他人的會(huì )話(huà)。
? 應該有一種收集過(guò)期會(huì )話(huà)并刪除相應文本文件的機制。
? 如果用戶(hù)使用已經(jīng)過(guò)期的會(huì )話(huà)標識符再次訪(fǎng)問(wèn)服務(wù)器,即使這個(gè)會(huì )話(huà)標識符的文本文件還沒(méi)有刪除,系統也不應該允許用戶(hù)使用原來(lái)的會(huì )話(huà)。
? 同時(shí),應該存在一種更新會(huì )話(huà)文本文件最后改動(dòng)時(shí)間的機制,使得用戶(hù)在會(huì )話(huà)過(guò)期時(shí)限之前返回時(shí)會(huì )話(huà)總是保持最新且合法的狀態(tài)數據。
四、實(shí)現偽會(huì )話(huà)管理機制
下面所介紹的工程稱(chēng)為PseudoSession,它是偽會(huì )話(huà)機制一個(gè)很簡(jiǎn)單的實(shí)現??紤]到移植性,我們以JavaBean的形式實(shí)現它。PseudoSessionBean的完整代碼可以從本文后面下載。
PseudoSessionBean擁有如下域(Field):
public String path;public long timeOut;
path是保存所有會(huì )話(huà)文本文件的目錄。如果Web服務(wù)器的數量在一個(gè)以上,這個(gè)目錄必須允許所有服務(wù)器訪(fǎng)問(wèn)。然而,為了防止用戶(hù)直接訪(fǎng)問(wèn)這些文本文件,這個(gè)路徑應該不允許用戶(hù)直接訪(fǎng)問(wèn)。解決這個(gè)問(wèn)題的一種方法是使用Web網(wǎng)站根之外的目錄。
timeOut是用戶(hù)的最后一個(gè)請求到會(huì )話(huà)過(guò)期作廢之間的時(shí)間。在PseudoSessionBean的代碼清單中,timeOut設置成了以毫秒表示的20分鐘,這是一個(gè)比較合理的超時(shí)時(shí)間值。對于任何用戶(hù),如果他在這個(gè)超時(shí)時(shí)間之后才繼續發(fā)出請求,他將得到一個(gè)新的會(huì )話(huà)標識符。
PseudoSessionBean有4個(gè)方法:getSessionID,setValue,getValue,deleteAllInvalidSessions。
4.1 getSessionID方法
getSessionID方法的聲明如下:
public String getSessionID(HttpServletRequest request)
這個(gè)方法應該在每一個(gè)JSP頁(yè)面的開(kāi)頭調用。它完成如下任務(wù):
? 如果用戶(hù)是第一次訪(fǎng)問(wèn),則為該用戶(hù)設定一個(gè)新的會(huì )話(huà)標識符。
? 檢查URL所帶會(huì )話(huà)標識符的合法性。如果會(huì )話(huà)標識符已經(jīng)過(guò)期,則getSessionID方法返回一個(gè)新的會(huì )話(huà)標識符。
下面我們來(lái)看看getSessionID方法的工作過(guò)程。
String sessionId = request.getParameter("sessionId");
validSessionIdFound是一個(gè)標記,用于指示會(huì )話(huà)標識符是否合法。validSessionIdFound的初始值是false。
boolean validSessionIdFound = false;
long類(lèi)型的now變量包含請求出現時(shí)的服務(wù)器時(shí)間。該變量用于確定用戶(hù)會(huì )話(huà)的合法性。
long now = System.currentTimeMillis();
如果找到了會(huì )話(huà)標識符,則getSessionID方法檢查它的合法性。檢查過(guò)程如下:
? 一個(gè)合法的會(huì )話(huà)標識符必須有對應的同名文本文件。
? 文件的最后修改時(shí)間加上timeOut應該大于當前時(shí)間。
? 如果存在與會(huì )話(huà)對應的文本文件,但文件已經(jīng)過(guò)期,則原來(lái)的文件被刪除。
? 把合法會(huì )話(huà)標識符所對應文本文件的最后修改日期改為now。
這些任務(wù)主要借助File對象完成,創(chuàng )建File對象的參數就是會(huì )話(huà)文本文件的路徑:
if (sessionId!=null) {
File f = new File(path + sessionId);
if (f.exists()) {
if (f.lastModified() + timeOut > now) { // 會(huì )話(huà)合法// 使用setLastModified時(shí),如果文件已經(jīng)被其他程序鎖定,// 程序不會(huì )產(chǎn)生任何異常,但文件數據不會(huì )改變f.setLastModified(now);validSessionIdFound = true; } else { // 會(huì )話(huà)已經(jīng)過(guò)期 // 刪除文件f.delete(); }} // end if (f.exists) } // end if (sessionId!=null)
如果不存在合法的會(huì )話(huà)標識符,則getSessionID方法生成一個(gè)會(huì )話(huà)標識符以及相應的文本文件:
if (!validSessionIdFound) { sessionId = Long.toString(now); // 創(chuàng )建文件 File f = new File(path + sessionId); try {f.createNewFile(); } catch (IOException ioe) {}} // end of if !validSessionIdFound
程序保證文件名字隨機性的方法非常簡(jiǎn)單:把當前的系統時(shí)間直接轉換成會(huì )話(huà)標識符。對于那些涉及敏感數據的應用,我們應該考慮運用更安全的隨機數生成器來(lái)生成會(huì )話(huà)標識符。
綜上所述,getSessionID并不總是返回新的合法會(huì )話(huà)標識符:它返回的標識符可能與傳遞給它的標識符相同,也可能是新創(chuàng )建的會(huì )話(huà)標識符。
為了保證JSP頁(yè)面擁有合法的會(huì )話(huà)標識符以便調用setValue、getValue方法,每個(gè)JSP頁(yè)面都必須在開(kāi)頭位置調用getSesstionID方法。
4.2 setValue方法
setValue方法保存value字符串以及與它關(guān)聯(lián)的字符串名字。這種“名字-值”對很容易使人想起Dictionary對象。setValue方法要求在第一個(gè)參數中提供合法的會(huì )話(huà)標識符,它假定在自己被調用之前getSessionID方法已經(jīng)執行,經(jīng)過(guò)檢驗的合法會(huì )話(huà)標識符必然存在,因此它不再對傳入的會(huì )話(huà)標識符進(jìn)行合法性檢驗。
setValue方法按如下規則保存名字-值對:
? 如果與value值關(guān)聯(lián)的name以前還沒(méi)有保存過(guò),則新的名字-值對加入到文本文件的末尾。
? 如果value字符串關(guān)聯(lián)的name值以前已經(jīng)保存過(guò),則原來(lái)保存的值被新的value值替換。
setValue方法按照如下格式保存名字-值對,注意“名字”是大小寫(xiě)敏感的:
name-1 value-1name-2 value-2name-3 value-3...name-n value-n
setValue方法的聲明如下:
public void setValue(String sessionId, String name, String value)
setValue方法首先尋找與當前會(huì )話(huà)對應的文本文件。如果不能找到文本文件,則setValue方法不做任何事情直接返回。如果找到了會(huì )話(huà)文本文件,setValue方法讀取文本文件的各個(gè)行,然后比較讀入的行與name:如果讀入的文本行開(kāi)頭與name一樣,則說(shuō)明該名字已經(jīng)保存,setValue方法將替換該行后面的值;如果name不能與讀入的文本行匹配,則這行文本被直接復制到一個(gè)臨時(shí)文件。
這部分功能的實(shí)現代碼如下:
try { FileReader fr = new FileReader(path + sessionId); BufferedReader br = new BufferedReader(fr); FileWriter fw = new FileWriter(path + sessionId + ".tmp"); BufferedWriter bw = new BufferedWriter(fw); String s; while ((s = br.readLine()) != null)if (!s.startsWith(name + " ")) { bw.write(s); bw.newLine();} bw.write(name + " " + value); bw.newLine(); bw.close(); br.close(); fw.close(); bw.close(); . . .}catch (FileNotFoundException e) {}catch (IOException e) { System.out.println(e.toString());}
原來(lái)文本文件中的所有行復制到臨時(shí)文件之后,setValue方法刪除原來(lái)的文本文件,然后把臨時(shí)文件改成會(huì )話(huà)文本文件的名字:
File f = new File(path + sessionId + ".tmp");File dest = new File(path + sessionId);dest.delete();f.renameTo(dest);
4.3 getValue方法
getValue方法用于提取原來(lái)保存在偽會(huì )話(huà)中的數據。正如setValue方法,getValue方法也要求傳入一個(gè)合法的會(huì )話(huà)標識符,而且getValue方法不再對傳入的會(huì )話(huà)標識符進(jìn)行合法性檢查。getValue方法的第二個(gè)參數是待提取數據的name,返回值是與指定name關(guān)聯(lián)的value。
getValue方法的聲明如下:
public String getValue(String sessionId, String name)
getValue方法的基本執行過(guò)程如下:首先找到會(huì )話(huà)文本文件,然后按行讀入直至找到與name匹配的文本行;找到匹配的文本行之后,getValue方法返回該行保存的value;如果不能找到,getValue方法返回null。
4.4 deleteAllInvalidSessions方法
deleteAllInvalidSessions方法刪除那些與已經(jīng)過(guò)期的會(huì )話(huà)關(guān)聯(lián)的文本文件。由于調用getSessionID方法時(shí)過(guò)期的會(huì )話(huà)文本文件會(huì )被刪除,deleteAllInvalidSessions方法并不是關(guān)鍵的方法。什么時(shí)候調用這個(gè)方法由應用自己決定。例如,我們可以編寫(xiě)一個(gè)專(zhuān)用的后臺程序,由這個(gè)程序每天一次清除所有過(guò)期的文本文件。最簡(jiǎn)單的辦法是在JSP文件末尾調用deleteAllInvalidSessions方法,但如果網(wǎng)站比較繁忙,重復地調用deleteAllInvalidSessions方法將降低整個(gè)網(wǎng)站的響應能力。一種明智的做法是:編寫(xiě)一個(gè)在訪(fǎng)問(wèn)量較少的時(shí)候自動(dòng)進(jìn)行清理的后臺程序。
deleteAllInvalidSessions方法的聲明如下:
public void deleteAllInvalidSessions()
它首先把所有會(huì )話(huà)文本文件的名字讀入files字符串數組:
File dir = new File(path); String[] files = dir.list();
deleteAllInvalidSessions方法比較文本文件的最后修改時(shí)間(加上超時(shí)時(shí)間)和系統當前時(shí)間,確定會(huì )話(huà)是否過(guò)期。long類(lèi)型的變量now用于保存系統的當前時(shí)間。
long now = System.currentTimeMillis();
接下來(lái),deleteAllInvalidSessions方法通過(guò)循環(huán)訪(fǎng)問(wèn)files數組,依次檢查每個(gè)文件的lastModified屬性。所有與過(guò)期會(huì )話(huà)關(guān)聯(lián)的文件都將被刪除:
for (int i=0; i
五、應用實(shí)例
編譯好PseudoSessionBean這個(gè)JavaBean之后,我們就可以利用偽會(huì )話(huà)管理機制來(lái)管理Web應用的會(huì )話(huà)狀態(tài)信息了。由于不必再使用服務(wù)器的會(huì )話(huà)管理機制,我們可以在page指令中把session屬性設置為false關(guān)閉默認的JSP/Servlet會(huì )話(huà)管理功能。
< %@ page session="false" %>
然后,我們用JSP的
< jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />
在上面這個(gè)
< % String sessionId = PseudoSessionId.getSessionID(request);%>
為了說(shuō)明PseudoSessionBean的應用,下面我們來(lái)看兩個(gè)JSP頁(yè)面,它們是index.jsp和secondPage.jsp。index.jsp頁(yè)面在偽會(huì )話(huà)變量中保存用戶(hù)的名字,而secondPage.jsp則提取這個(gè)用戶(hù)名字。
index.jsp頁(yè)面的代碼如下:
< %@ page session="false" contentType="text/html;charset=gb2312" %>
< jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />
< % String sessionId = PseudoSessionId.getSessionID(request);%>
< html>
< head>
< title>偽會(huì )話(huà)< /title>
< /head>
< body>
< h1>偽會(huì )話(huà)管理機制< /h1>
< % String userName = "bulbul"; PseudoSessionId.setValue(sessionId, "userName", userName);%>
< a href="/secondPage.jsp?sessionId=<";%=sessionId%>>點(diǎn)擊此處
< form method="post" action=anotherPage.jsp?sessionId=< %=sessionId%>>
輸入數據:< input type="text" name="sample">
< input type="submit" name="Submit" value="Submit">
< /form>
< /body>
< /html>
< % PseudoSessionId.deleteAllInvalidSessions();%>
注意,包括
評論