Session
Web服務器跟蹤客戶狀態通常有四種方法
1.建立含有跟蹤數據的隱藏字段
2.重寫包含額外參數的URL
3.使用持續的Cookie
4.使用Servlet API中的Session(會話)機制
Session的概念
Session用於跟蹤客戶的狀態。
Session指的是在一段時間內,單個客戶與Web服務器的一連串相關的交互過程。
在一個Session中,客戶可能會多次請求訪問同一個網頁,也有可能請求訪問各種不同的服務器資源。
Session的例子
例1:在電子郵件應用中,從一個客戶登錄到電子郵件系統開始,經過收信、寫信和發信等一系列操作,直至最后退出郵件系統,整個過程為一個Session。
例2:在購物網站應用中,從一個客戶開始購物,到最后結賬,整個過程為一個Session。
Session的運行機制
當一個Session開始時,Servlet容器將創建一個HttpSession對象,在HttpSession對象中可以存放客戶狀態的信息(例如購物車)。
Servlet容器為HttpSession分配一個唯一標識符,稱為Session ID。
Servlet容器把Session ID作為Cookie保存在客戶的瀏覽器中。
每次客戶發出HTTP請求時,Servlet容器可以從HttpServletRequest對象中讀取Session ID,然后根據Session ID找到相應的HttpSession對象,從而獲取客戶的狀態信息。
HttpSession接口
查看Java EE的文檔:javax.servlet.http.HttpSession
提供了一種方法,在用戶發送多個頁面請求或者訪問一個網站時,用於認證用戶,並且存儲一些用戶的信息。
Servlet容器使用HttpSession接口創建一個HTTP client和server之間的session。
這個session會持續一個特定的時間,跨越了用戶的多個連接或頁面請求。
一個session通常對應於一個用戶,這個用戶可能訪問了一個網站很多次。
服務器可以用很多方法維護session,比如使用cookies或者重寫URLs。
HttpSession接口允許用戶:
1.查看並且操縱session信息,比如session identifier,創建時間,上次訪問時間。
2.綁定objects到sessions,允許用戶信息在跨越多個連接時仍保持。
當應用存儲一個object到一個session,或者從session中移除一個object,session會檢查這個object是否實現了HttpSessionBindingListener接口,如果實現了這個接口,servlet會通知這個object,它已經被綁定或解除綁定到這個session。通知會在綁定方法完成時被發出。對於invalidate或者expire的session,通知會在invalidate或expire結束后被發出。
一個servlet應該可以處理客戶端選擇不加入session的情況,比如主動關掉cookies的情況。直到客戶端加入session,isNew()會返回一個true。
如果用戶不加入session,getSession將會在每次請求時返回一個不同的session,isNew()永遠返回true。
Session信息的范圍僅對當前web應用(ServletContext),所以在一個context里存儲的信息不會直接對其他context可見。
掌握HttpSession API
getId()
返回Session ID,即返回包含session唯一標識符的一個字符串。
session的標識符是由servlet容器賦予的,是依賴於實現的,不同的服務器產生標識符的算法是不一樣的。
測試:新建一個jsp,body部分如下:
<body> session id = <%= session.getId() %><br> </body>
啟動Tomcat,用瀏覽器訪問,可以看到session id的值,不同的瀏覽器會不同,同一個瀏覽器刷新、開啟新的窗口、新建標簽等,session id都不變,但是重啟瀏覽器之后,看到session id也會變化。
invalidate()
使當前的Session失效,Servlet容器會釋放HttpSession對象占用的資源。
在剛才的測試頁面加上這一句:
<body> session id = <%=session.getId()%><br> <% session.invalidate(); %> </body>
可以發現每刷新一次頁面id都會變一次。每次訪問頁面都會產生新的session,獲取id后立刻讓它失效。
如果交換上面兩句話,會有什么不同呢?
如下:
<body> <% session.invalidate(); %> session id = <%=session.getId()%><br> </body>
結果看起來並沒有什么不同,用瀏覽器訪問時還是每次刷新都會變換session id,這是因為每次用到的時候都生成了新的session對象。
這個invalidate()方法我們並不常用,一般是服務器用。
setAttribute(String name, Object value)和getAttribute(String name)
set方法將一對name/value鍵值對保存在HttpSession對象中;
get方法根據name參數返回保存在HttpSession對象中的屬性值。
isNew()
判斷是否是新創建的session,如果是新創建的session返回true,否則是false。
比如:
<body> session id = <%=session.getId()%><br> session isNew() = <%=session.isNew()%> </body>
第一次打開頁面時為true,刷新頁面后就為false。
如果客戶端不知道這個session或者客戶端選擇不加入session,這個方法返回true。
比如,如果server只用了基於cookie的session,而客戶端禁用了cookies,那么客戶端每一個請求都會產生新的session。
setMaxInactiveInterval(int interval)
設定一個Session可以處於不活動狀態的最大時間間隔,以秒為單位。
這里的活動是指客戶端向服務器端發送請求。
如果超過這個時間,session自動失效,servlet容器會invalidate這個session,客戶端會獲得新的session對象。
如果設置為0或負數,表示不限制Session處於不活動狀態的時間。
小於這個間隔,session不會失效,默認的間隔是30分鍾。
也可以在web.xml里面配置這個時間:
<session-config> <session-timeout>20</session-timeout> </session-config>
(注意此處時間是以分鍾為單位的)。
注意:程序里面的配值會覆蓋掉配值文件中的值,即程序中設置的值優先級更高。
Session的生命周期
當客戶第一次訪問Web應用中支持Session的某個網頁時,就會開始一個新的Session。
接下來當客戶瀏覽這個Web應用的不同網頁時,始終處於同一個Session中。
默認情況下,JSP網頁都是支持Session的,也可以通過以下語句顯式聲明支持Session:
<%@ page session="true">
Session結束生命周期
在以下情況中,Session將結束生命周期,Servlet容器會將Session所占用的資源釋放掉:
1.客戶端關閉瀏覽器(真的這樣嗎?);
2.Session過期;
3.服務器端調用了HttpSession的invalidate()方法。
Session過期
Session過期是指當Session開始后,在一段時間內客戶沒有和Web服務器交互,這個Session會失效,HttpSession的setMaxInactiveInterval(int interval)方法可以設置允許Session保持不活動狀態的時間(以秒為單位),如果超過這一時間,Session就會失效。
如何做到在瀏覽器關閉時刪除session
嚴格地講,做不到這一點,因為session是保存在服務器端的,客戶端僅存了保存session id的Cookies,者中Cookie稱為Session cookies(會話Cookie),Session cookies沒有設置maxAge,即該屬性為-1,表示這個cookie將會在瀏覽器退出時被刪除。Session Cookie保存在內存里,而不是硬盤中。
再次打開瀏覽器時,Session Cookie沒有了,發送請求時,服務器端將會生成新的Session,舊的Session被放在一邊,等待過期時被刪除,也即是說,服務器端會有一些沒有辦法再用到的舊的冗余session,靜靜等待死亡,所以瀏覽器關閉時並沒有刪除這些session。
可以做一點努力的辦法是在所有的客戶端頁面里使用javascript代碼window.onclose來監視瀏覽器的關閉動作,然后向服務器發送一個請求來刪除session。
但是對於瀏覽器崩潰或者強行殺死進程這些非常規手段仍然無能為力。實際上在項目中我們也不會這么做,而是讓服務器在Session過期時將其刪除。
實例1:郵件
登錄頁面:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'maillogin.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String username = ""; if (!session.isNew()) { username = (String) session.getAttribute("username"); if (null == username) { username = ""; } } %> <p> Session Id: <%=session.getId()%></p> <form action="mailcheck.jsp"> username: <input type="text" name="username" value="<%=username%>"> <input type="submit" value="submit"> </form> </body> </html>
登錄成功頁面:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'mailcheck.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String username = request.getParameter("username"); if (null != username) { session.setAttribute("username", username); } %> <p> Hello ! <%=username%></p> <p>你的郵箱有10封郵件</p> <a href="maillogin.jsp">轉向登錄</a> <a href="maillogout.jsp">注銷</a> </body> </html>
注銷頁面:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'maillogout.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String username = (String) session.getAttribute("username"); session.invalidate(); %> <!-- 上面兩句交換位置就會出現異常,因為session已經失效,不能獲取其中屬性 --> <%=username%>再見! <a href="maillogin.jsp">重新登錄</a> </body> </html>
實例2:
登錄頁面:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'login.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <% String authority = (String) request.getAttribute("authority"); %> <form action="UserLoginServlet" method="post"> username:<input type="text" name="username" value="<%=null == request.getAttribute("username") ? "" : request .getAttribute("username")%>"><br> password:<input type="password" name="password"><br> authority: <select name="authority"> <option value="1" <%="1".equals(authority) ? "selected='selected'" : ""%>>common user</option> <option value="2" <%="2".equals(authority) ? "selected='selected'" : ""%>>administrator</option> </select> <br> <input type="submit" value="submit"> </form> </body> </html>
登錄之后轉到的Servlet類:

package com.mengdd.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class UserLoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); String authority = req.getParameter("authority"); HttpSession session = req.getSession(); if ("1".equals(authority)) { // 普通用戶 if ("zhangsan".equals(username) && "123".equals(password)) { saveToSession(session, username, password, authority); } else { saveToRequest(req, username, password, authority); backToLogin(req, resp); } } else if ("2".equals(authority)) { // 系統管理員 if ("lisi".equals(username) && "456".equals(password)) { saveToSession(session, username, password, authority); } else { saveToRequest(req, username, password, authority); backToLogin(req, resp); } } else { // 其他情況,轉回登錄頁面 saveToRequest(req, username, password, authority); backToLogin(req, resp); } } // 將用戶信息放置到session里面 private void saveToSession(HttpSession session, String username, String password, String authority) { User user = new User(); user.setUsername(username); user.setPassword(password); user.setAuthority(authority); session.setAttribute("user", user); } private void saveToRequest(HttpServletRequest req, String username, String password, String authority) { req.setAttribute("username", username); req.setAttribute("password", password); req.setAttribute("authority", authority); } // 通過請求轉發,回到登錄頁面 private void backToLogin(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { RequestDispatcher requestDispatcher = req .getRequestDispatcher("session/login.jsp"); requestDispatcher.forward(req, resp); } }
其中User類:

package com.mengdd.servlet; public class User { private String username; private String password; private String authority; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getAuthority() { return authority; } public void setAuthority(String authority) { this.authority = authority; } }
關於這個還可以做很多練習,就不再贅述。
通過各種實例可見,用到session時,每個JSP頁面和Servlet類都要判斷session是否存在,會造成很多重復代碼和冗余工作,這個問題后面會介紹解決方法。
參考資料
聖思園張龍老師Java Web視頻教程。