1. 程式人生 > >Java Servlet 實戰入門教程-09-servlet HttpSession

Java Servlet 實戰入門教程-09-servlet HttpSession

Servlet Session 跟蹤

HTTP 是一種"無狀態"協議,這意味著每次客戶端檢索網頁時,客戶端開啟一個單獨的連線到 Web 伺服器,伺服器會自動不保留之前客戶端請求的任何記錄。

但是仍然有以下三種方式來維持 Web 客戶端和 Web 伺服器之間的 session 會話:

Cookies

一個 Web 伺服器可以分配一個唯一的 session 會話 ID 作為每個 Web 客戶端的 cookie,對於客戶端的後續請求可以使用接收到的 cookie 來識別。

這可能不是一個有效的方法,因為很多瀏覽器不支援 cookie,所以我們建議不要使用這種方式來維持 session 會話。

隱藏的表單欄位

一個 Web 伺服器可以傳送一個隱藏的 HTML 表單欄位,以及一個唯一的 session 會話 ID,如下所示:

<input type="hidden" name="sessionid" value="12345">

該條目意味著,當表單被提交時,指定的名稱和值會被自動包含在 GET 或 POST 資料中。

每次當 Web 瀏覽器傳送回請求時,session_id 值可以用於保持不同的 Web 瀏覽器的跟蹤。

這可能是一種保持 session 會話跟蹤的有效方式,但是點選常規的超文字連結(<A HREF...>)不會導致表單提交,因此隱藏的表單欄位也不支援常規的 session 會話跟蹤。

URL 重寫

您可以在每個 URL 末尾追加一些額外的資料來標識 session 會話,伺服器會把該 session 會話識別符號與已儲存的有關 session 會話的資料相關聯。

例如,http://w3cschool.cc/file.htm;sessionid=12345,session 會話識別符號被附加為 sessionid=12345,識別符號可被 Web 伺服器訪問以識別客戶端。

URL 重寫是一種更好的維持 session 會話的方式,它在瀏覽器不支援 cookie 時能夠很好地工作,但是它的缺點是會動態生成每個 URL 來為頁面分配一個 session 會話 ID,即使是在很簡單的靜態 HTML 頁面中也會如此。

  • 容器怎麼知道 cookie 被禁用了的?

當容器看到 HttpSession session = request.getSession(); 就知道我們需要開啟一個會話,此時容器並不關心是否 cookie 被禁用, 而是想客戶端返回響應時,同時嘗試 cookie 和 URL 重寫。

  • 生效的實際方式

必須是你對 URL 進行了編碼。

encodeURL() 或者 encodeDirectURL(),其他的事情都會交給容器去完成。

HttpSession 物件

除了上述的三種方式,Servlet 還提供了 HttpSession 介面,該介面提供了一種跨多個頁面請求或訪問網站時識別使用者以及儲存有關使用者資訊的方式。

Servlet 容器使用這個介面來建立一個 HTTP 客戶端和 HTTP 伺服器之間的 session 會話。

會話持續一個指定的時間段,跨多個連線或頁面請求。

您會通過呼叫 HttpServletRequest 的公共方法 getSession() 來獲取 HttpSession 物件,如下所示:

HttpSession session = request.getSession();

建立一個 session

當會話僅是一個未來的且還沒有被建立時會話被認為是新的。

因為 HTTP是一種基於請求-響應的協議,直到客戶端“加入”到 HTTP 會話之前它都被認為是新的。

當會話跟蹤資訊返回到伺服器指示會話已經建立時客戶端加入到會話。直到客戶端加入到會話,不能假定下一個來自客戶端的請求被識別為同一會話。

如果以下之一是 true,會話被認為是新的:

  1. 客戶端還不知道會話

  2. 客戶端選擇不加入會話。

這些條件定義了 servlet 容器沒有機制能把一個請求與之前的請求相關聯的情況。

servlet開發人員必須設計他的應用以便處理客戶端沒有,不能,或不會加入會話的情況。

與每個會話相關聯是一個包含唯一識別符號的字串,也被稱為會話ID。

會話 ID 的值能通過呼叫 javax.servlet.http.HttpSession.getId() 獲取, 且能在建立後通過呼叫 javax.servlet.http.HttpServletRequest.changeSessionId() 改變。

會話範圍

HttpSession 物件必須被限定在應用級別。

底層的機制,如使用 cookie 建立會話,不同的上下文可以是相同,但所引用的物件,包括包括該物件中的屬性,決不能在容器上下文之間共享。

用一個例子來說明該要求: 如果 servlet 使用 RequestDispatcher 來呼叫另一個 Web 應用的 servlet,任何建立的會話和被呼叫 servlet 所見的必須不同於來自呼叫會話所見的。

此外,一個上下文的會話在請求進入那個上下文時必須是可恢復的,不管是直接訪問它們關聯的上下文還是在請求目標分派時建立的會話。

繫結屬性到會話

servlet 可以按名稱繫結物件屬性到 HttpSession 實現,任何繫結到會話的物件可用於任意其他的 servlet,其屬於同一個 ServletContext 且處理屬於相同會話中的請求。

一些物件可能需要在它們被放進會話或從會話中移除時得到通知。

這些資訊可以從 HttpSessionBindingListener 介面實現的物件中獲取。

這個介面定義了以下方法,用於標識一個物件被繫結到會話或從會話解除繫結時。

  1. valueBound

  2. valueUnbound

在物件對 HttpSession 介面的 getAttribute 方法可用之前 valueBound 方法必須被呼叫。

在物件對 HttpSession 介面的 getAttribute 方法不可用之後 valueUnbound 方法必須被呼叫。

會話超時

在 HTTP 協議中,當客戶端不再處於活動狀態時沒有顯示的終止訊號。

這意味著當客戶端不再處於活躍狀態時可以使用的唯一機制是超時時間。

Servlet 容器定義了預設的會話超時時間,且可以通過 HttpSession 介面的 getMaxInactiveInterval 方法獲取。

開發人員可以使用 HttpSession 介面的 setMaxInactiveInterval() 改變超時時間。

這些方法的超時時間以秒為單位。根據定義,如果超時時間設定為 0 或更小的值,會話將永不過期。會話不會失效,直到所有 servlet 使用的會話已經退出其 service 方法。一旦會話已失效,新的請求必須不能看到該會話。

最後訪問時間

HttpSession 介面的 getLastAccessedTime() 方法允許 servlet 確定在當前請求之前的會話的最後訪問時間。

當會話中的請求是 servlet 容器第一個處理時該會話被認為是訪問了。

HttpSession 方法

重要會話語義

多執行緒問題

在同一時間多個 servlet 執行請求的執行緒可能都有到同一會話的活躍訪問。容器必須確保,以一種執行緒安全的方式維護表示會話屬性的內部資料結構。開發人員負責執行緒安全的訪問屬性物件本身。這樣將防止併發訪問HttpSession物件內的屬性集合,消除了應用程式導致破壞集合的機會。

客戶端語義

由於 cookie 或 SSL 證書通常由 Web 瀏覽器程序控制,且不與瀏覽器的任意特定視窗關聯,從客戶端應用程式發起的到 servlet 容器的請求可能在同一會話。

為了最大的可移植性,開發人員應該假定客戶端所有視窗參與同一會話。

分散式環境

在一個標識為分散式的應用程式中,會話中的所有請求在同一時間必須僅被一個 JVM 處理。

容器必須能夠使用適當的 setAttribute 或 putValue 方法把所有物件放入到 HttpSession 類例項。

以下限制被強加來滿足這些條件:

  1. 容器必須接受實現了 Serializable 介面的物件。

  2. 容器可以選擇支援其他指定物件儲存在 HttpSession 中,如Enterprise JavaBeans 元件和事務的引用。

  3. 由特定容器的設施處理會話遷移。

當分散式 servlet 容器不支援必需的會話遷移儲存物件機制時容器必須丟擲 IllegalArgumentException。

分散式 servlet 容器必須支援遷移的物件實現 Serializable 的必要機制。

這些限制意味著開發人員確保除在非分散式容器中遇到的問題沒有額外的併發問題。

容器供應商可以確保可擴充套件性和服務質量的功能,如負載平衡和故障轉移通過把會話物件和它的內容從分散式系統的任意一個活躍節點移動到系統的一個不同的節點上。

如果分散式容器持久化或遷移會話提供服務質量特性,它們不限制使用原生的 JVM 序列化機制用於序列化 HttpSession 和它們的屬性。

如果開發人員實現 session 屬性上的 readObject 和 writeObject 方法,他們也不能保證容器將呼叫這些方法,但保證 Serializable 結束它們的屬性將被儲存。

容器必須在遷移會話時通知實現了 HttpSessionActivationListener 的所有會話屬性。它們必須在序列化會話之前通知鈍化監聽器,在反序列化之後通知啟用監聽器。

寫分散式應用的開發人員應該意識到容器可能執行在多個 Java 虛擬機器中,開發人員不能依賴靜態變數儲存應用狀態。他們應該用企業 Bean 或資料庫儲存這種狀態。

程式碼實戰

程式碼

  • SessionTrack.java
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/session/track")
public class SessionTrack extends HttpServlet {

    private static final long serialVersionUID = -604801085378034646L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 編碼的設定一定要放在 resp.getXXX(), resp.setXXX() 之前
        resp.setContentType("text/plain;charset=UTF-8");
        resp.setCharacterEncoding("UTF-8");

        // 如果不存在 session 會話,則建立一個 session 物件
        HttpSession httpSession = req.getSession(true);
        // 獲取 session 建立時間
        long createdTime = httpSession.getCreationTime();
        // 獲取該網頁的最後一次訪問時間
        long lastAccessedTime = httpSession.getLastAccessedTime();

        // 檢查網頁上是否有新的訪問者
        if (httpSession.isNew()){
            httpSession.setAttribute(SessionKey.VISIT_COUNT, 1);
        } else {
            int visitCount = (Integer)httpSession.getAttribute(SessionKey.VISIT_COUNT);
            visitCount++;
            httpSession.setAttribute(SessionKey.VISIT_COUNT, visitCount);
        }

        PrintWriter printWriter = resp.getWriter();
        printWriter.println("sessionId:" + httpSession.getId());
        printWriter.println("建立時間:" + createdTime);
        printWriter.println("最後訪問時間:" + lastAccessedTime);
        printWriter.println("訪問總數:" + httpSession.getAttribute(SessionKey.VISIT_COUNT));
    }

    private interface SessionKey {
        /**
         * 訪問總計
         */
        String VISIT_COUNT = "visitCount";
    }
}

訪問

sessionId:B9F7ADD0900C9057485DDDB13DBCAAE6
建立時間:1538641292767
最後訪問時間:1538641321823
訪問總數:1

再次訪問,訪問總數會依次遞增

刪除會話

當您完成了一個使用者的 session 會話資料,您有以下幾種選擇:

移除一個特定的屬性

您可以呼叫 removeAttribute(String name) 方法來刪除與特定的鍵相關聯的值。

刪除整個 session 會話:

您可以呼叫 invalidate() 方法來丟棄整個 session 會話。

設定 session 會話過期時間

您可以呼叫 setMaxInactiveInterval(int interval) 方法來單獨設定 session 會話超時。

登出使用者

如果使用的是支援 servlet 2.4 的伺服器,您可以呼叫 logout() 來登出 Web 伺服器的客戶端,並把屬於所有使用者的所有 session 會話設定為無效。

web.xml 配置

如果您使用的是 Tomcat,除了上述方法,您還可以在 web.xml 檔案中配置 session 會話超時,如下所示:

<session-config>
  <session-timeout>15</session-timeout>
</session-config>

上面例項中的超時時間是以分鐘為單位,將覆蓋 Tomcat 中預設的 30 分鐘超時時間。

在一個 Servlet 中的 getMaxInactiveInterval() 方法會返回 session 會話的超時時間,以秒為單位。

所以,如果在 web.xml 中配置 session 會話超時時間為 15 分鐘,那麼 getMaxInactiveInterval() 會返回 900。

程式碼示例

  • SessionInvalid.java

直接訪問,會報錯說 session 已經無效。

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/session/invalid")
public class SessionInvalid extends HttpServlet {

    private static final long serialVersionUID = 7165264630729082806L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 編碼的設定一定要放在 resp.getXXX(), resp.setXXX() 之前
        resp.setContentType("text/plain;charset=UTF-8");
        resp.setCharacterEncoding("UTF-8");

        // 如果不存在 session 會話,則建立一個 session 物件
        HttpSession httpSession = req.getSession(true);
        httpSession.setAttribute(SessionKey.VISIT_COUNT, 1);
        // 銷燬當前會話
        httpSession.invalidate();

        PrintWriter printWriter = resp.getWriter();
        // 再次回去將會報錯:java.lang.IllegalStateException: getAttribute: Session already invalidated
        printWriter.println("訪問總數:" + httpSession.getAttribute(SessionKey.VISIT_COUNT));
    }

    private interface SessionKey {
        /**
         * 訪問總計
         */
        String VISIT_COUNT = "visitCount";
    }
}

參考資料