次世代的會話管理專案 Spring Session

騰訊雲加社群發表於2019-01-19

歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~

本文來自雲+社群翻譯社,由Tnecesoc編譯。

會話管理一直是 Java 企業級應用的重要部分。不過在很長的一段時間裡,這一部分都被我們認為是一個已解決的問題,並且也沒有什麼重大的創新出現。

然而,微服務還有可橫向伸縮的雲原生應用這一現代趨勢揭露了現今的會話管理技術在設計上的一些缺陷,挑戰著我們在過去 20 多年來對這一設計得出的一些結論。

本文會演示Spring Session API 為了幫助我們克服以前的會話管理方式的一些侷限所採取的方法。我們將會先總結一下當前的會話管理技術的問題,然後深入探討 Spring Session 解決這些問題所採取的策略。最後,我們會總結 Spring Session 的工作方式以及在具體專案裡面的一些用法。

Spring Session 為企業級 Java 應用的會話管理領域帶來了革新,讓我們可以輕鬆做到:

  • 編寫可橫向伸縮的雲原生應用
  • 將會話狀態的儲存外放到專門的外部會話儲存裡,比如 Redis 或 Apache Geode,後者以獨立於應用程式伺服器的方式提供了高質量的儲存叢集
  • 在使用者通過 WebSocket 發出請求的時候保持 HttpSession 的線上狀態
  • 訪問來自非 Web 請求處理指令的會話資料,比如 JMS 訊息處理指令
  • 為每個瀏覽器建立多個會話提供支援,從而構建更豐富的終端使用者體驗
  • 控制在客戶端和伺服器間交換會話 ID 的方式,從而編寫在 HTTP 報文首部中提取會話 ID 而脫離對 Cookie 的依賴的 RESTul API

注意,Spring Session 專案其實並不依賴於 Spring 框架,因此我們甚至能在不使用 Spring 框架的專案裡面用到它。

傳統會話管理技術的問題

Spring Session 的目的是解決傳統的 JavaEE 會話管理技術的各種問題。下面就通過一些例子說明一些這方面的問題。

構建可橫向伸縮的雲原生應用程式

從雲原生應用架構的視角來看,一個應用應該可以通過在一個大型的虛擬機器池裡執行更多的 Linux 容器來部署更多的例項的方式來得到橫向的伸縮。比如,我們能很輕鬆地將一個這樣的應用的 war 檔案部署到 Cloud Foundry 或 Heroku 上的 Tomcat 裡面,然後在幾秒內擴充套件出 100 個應用程式例項,使得其中每個例項都有 1GB 的 RAM。我們還可以將雲平臺設定成會根據使用者需求自動增減應用程式例項的數量。

很多應用都會把 HTTP 會話狀態儲存在執行應用程式碼的 JVM 裡面。這很容易實現,而且存取的速度也很快。當一個應用例項加入或退出叢集的時候,HTTP 會話的儲存會在所有尚存的應用程式例項上重新進行平均的分配。在彈性雲環境中,我們會執行數以百計的應用例項,且例項數量可能隨時發生快速的增減變化。這就帶來了一些問題:

  • HTTP 會話儲存的重新分配會成為效能瓶頸;
  • 儲存大量會話所需的堆空間太大,會導致垃圾回收過程頻繁進行,並影響效能;
  • TCP 組播通常會被雲端的基礎架構所禁止,但會話管理器需要經常用它來發現加入或退出叢集的應用例項。

因此,將 HTTP 會話狀態儲存在執行應用程式碼的 JVM 之外的資料儲存中會更高效。例如可以設定並使用 Redis 來儲存上述的 100 個 Tomcat 例項裡面的會話狀態,那麼 Tomcat 例項數量的增減便不會影響到在 Redis 中的會話儲存的模式。另外,因為 Redis 是用 C 語言編寫的,所以它可以在沒有垃圾回收機制影響其執行的前提下,動用數百 GB 甚至 TB 數量級的記憶體。

對像 Tomcat 這樣的開源伺服器,找到使用外部資料儲存(如 Redis 或 Memcached)的會話管理技術的其他實現是很簡單的,但是使用起來的配置過程可能很複雜,並且每個應用伺服器的配置過程可能都不一樣。對如 WebSphere 和 Weblogic 之類的閉源產品,找到適合它們的會話管理技術的替代實現則通常是不可能的。

Spring Session 為設定外掛式的會話資料儲存提供了一種獨立於具體應用伺服器的方法,使得我們能在 Servlet 框架的範疇內實現這樣的儲存,而不用依賴於具體的應用伺服器的 API。這意味著 Spring Session 可以與所有實現了 Servlet 規範的應用伺服器(Tomcat,Jetty,WebSphere,WebLogic,JBoss)協同工作,並在所有應用伺服器上以完全相同且很容易的方式來進行配置。

我們還可以根據我們的需求選用最適合的外部會話資料儲存。這使得 Spring Session 也成了一個能幫助我們將傳統的 JavaEE 應用遷移到雲端並作為一個符合十二要素的應用的一個理想的遷移工具。

一個使用者,多個賬戶

假設你正在 example.com 上執行一個面向大眾的 Web 應用,其中一些人類使用者建立了多個帳號。例如,使用者 Jeff Lebowski 可能有兩個帳號 thedude@example.com 和 lebowski@example.com。跟其他 Java Web 應用程式一樣,你可以使用 HttpSession 來跟蹤各種會話狀態,比如當前登入的使用者。因此,當使用者想從 dude@example.com 切換到 lebowski@example.com 時,就必須登出當前賬號並重新登入。

使用 Spring Session 來為每個使用者配置多個 HTTP 會話就很簡單了。這時 Jeff Lebowski 無需登出和登入就可以在 thedude@example.com 和 lebowski@example.com 之間來回切換。

不同安全級別下的預覽

想象一下,你要構建一個具有複雜的自定義授權體系的 Web 應用,其中具有不同許可權的使用者會具有不同的應用 UI 樣式。

比如說,假設應用有四個安全級別:公開(public)、保密(confidential)、機密(secret)以及絕密(top secret)。在使用者登入到應用時,系統會識別這一使用者的安全級別,然後只對其顯示不高於其安全級別的資料。這樣,公開級別的使用者可以看到公開級別的文件;具有保密級別的使用者能看公開和保密級別的,以此類推。為了讓使用者介面更加友好,我們的應用也應該能讓使用者預覽應用的 UI 在較低的安全級別下的樣子。比如絕密級別使用者應該能在祕密模式下預覽應用的各項事物的外觀。

典型的 Web 應用會將當前使用者的身份及其安全級別或角色儲存在 HTTP 會話裡面。不過,由於 Web 應用的每個使用者只有一個會話,因此也只能通過登出再登入的方式來切換使用者的角色,或者實現一個使用者多個會話這一形式。

憑藉 Spring Session,我們就可以很輕鬆地給每個登入使用者建立多個相互獨立的會話,預覽功能的實現也會因此變得簡單。比如當前以絕密等級登入的使用者想要預覽機密等級下的應用時,就可以對其建立並使用一個新的安全級別為機密的會話。

在使用 Web Sockets 時保持登入狀態

再想象一個場景,在使用者通過 example.com 登入到我們的 Web 應用時,他們能使用通過 Websockets 工作的一個 HTML5 即時聊天客戶端進行對話。不過,根據 Servlet 規範,通過 Websockets 發出的請求不會更新會話的過期時間,因此在使用者進行聊天的時候,無論他們的聊天有多頻繁,會話也可能聊著聊著就沒了,然後 Websocket 連線也會因此關閉,聊天也就無法繼續了。

又是憑藉 Spring Session,我們可以很輕鬆地確保 Websocket 請求還有常規的 HTTP 請求都能更新會話的過期時間。

訪問對非 Web 請求的會話資料

再想象一下,我們的應用提供了兩種訪問方式,一個基於 HTTP 的 RESTful API,另一個是基於 RabbitMQ 的 AMQP 訊息。此時,執行處理 AMQP 訊息的的執行緒是無法訪問應用伺服器的 HttpSession 的,對此我們必須自己寫一個解決方案來訪問 HTTP 會話裡邊的資料。

還是憑藉 Spring Session,只要我們知道會話的 ID,就可以從應用程式的任意執行緒訪問 Spring Session。Spring Session 比以往的 Servlet HTTP 會話管理器有著功能更加豐富的 API,使得我們只需要知道會話 ID 就能定位我們想要找的會話。比如,我們可以用傳入訊息的使用者標識欄位來直接找到對應的會話。

Spring Session 的工作方式

現在傳統應用伺服器在 HTTP 會話管理方面的侷限性已經在不同情境中展示過了,我們再來看看 Spring Session 是如何解決這些問題的。

Spring Session 架構

在實現一個會話管理器的時候,有兩個關鍵問題必須得到解決:

  • 如何建立一個高效、可靠、高可用的會話資料儲存叢集?
  • 如何確定能夠哪個會話的例項與哪個傳入的請求(形式有 HTTP、WebSocket、AMQP 等)相關聯?

不過在本質上,有個更關鍵的問題是:如何跨越不同的請求協議來傳輸一個會話的 ID?

第一個問題對 Spring Session 來說已被各種高可用可伸縮的叢集儲存(Redis、Gemfire、Apache Geode 等)很好地解決了。因此 Spring Session 也應該定義一組標準介面來使得對底層資料儲存的訪問可以用不同的資料儲存來實現。Spring Session 在定義 SessionExpiringSession 這些基本的關鍵介面之外,也針對了不同資料儲存的訪問定義了關鍵介面 SessionRepository

  • org.springframework.session.Session 是定義會話基本功能的介面,例如屬性的設定和刪除。這個介面並不依賴於具體的底層技術,因此可以比 Servlet 裡面的 HttpSession 適用於更多的情況;
  • org.springframework.session.ExpiringSession 則擴充套件了 Session 介面。它提供了一些屬性,讓我們可以設定具有時效性的會話,並查詢這個會話是否已經過期。RedisSession 便是這個介面的一個實現範例。
  • org.springframework.session.SessionRepository 定義了建立,儲存,刪除和查詢會話的方法。將 Session 儲存到資料儲存的實際邏輯便寫在這一介面的具體實現中。例如 RedisOperationsSessionRepository 便是這個介面的一個實現,它使用 Redis 來實現了會話的建立、儲存以及刪除。

至於將請求關聯到特定會話例項的問題,Spring Session 則假定這一關聯的過程取決於特定的協議,因為客戶端和伺服器在請求 / 響應週期期間就需要對所傳輸的會話 ID 達成一致。比如,如果客戶端發來一個 HTTP 請求,那麼會話就可以通過 Cookie 或者 HTTP 報文首部來和請求相關聯。如果發來一個 HTTPS 請求,則可用 SSL 的 Session ID 欄位來講會話與請求相關聯。若發來的是 JMS 訊息,那也可以用訊息首部來儲存請求和響應間的會話 ID。

對 HTTP 協議的關聯操作,Spring 會話定義了一個 HttpSessionStrategy 介面,後者有將 Cookies 和會話關聯在一起的 CookieHttpSessionStrategy 和使用了自定義報文首部欄位來管理會話的 HeaderHttpSessionStrategy 兩種實現。

下面便詳細地介紹一下 Spring Session 在 HTTP 協議上的工作方式。

在本文釋出時(2015.11.10),Spring Session 1.0.2 在當前的 GA 發行版提供了使用 Redis 的 Spring Session 的一套實現,以及支援任何分散式的 Map(如 Hazelcast)的實現。其實,實現 Spring Session 針對某種資料儲存的支援是相對容易的,在開源社群裡已經有了很多這樣的實現。

基於 HTTP 的 Spring Session

基於 HTTP 的 Spring Session 是以一個標準 Servlet 過濾器(filter)的形式實現的。這一過濾器應該擷取所有的對 Web 應用的請求,並且也應該在諸多過濾器組成的鏈中排在第一個。Spring Session 的過濾器會負責確保所有後續的程式碼裡面對 javax.servlet.http.HttpServletRequest.getSession() 方法的呼叫都會呈遞給一個 Spinrg Session 的 HttpSession 例項,而不是應用伺服器預設提供的 HttpSession

要理解這點,最簡單的方法就是查閱 Spring Session 的實際原始碼。我們首先從用來實現 Spring Session 的標準 Servlet 擴充套件點(extension points)開始。

在 2001 年,Servlet 2.3 規範引入了 ServletRequestWrapper。該類的 Javadoc 稱 ServletRequestWrapper “為 ServletRequest 介面能讓開發者繼承它來適配一種特別的 Servlet 提供了一種便利的實現。該類採用了包裝器,或者說裝飾器模式。對該類的 ServletRequest 類的方法的呼叫會被傳至其封裝的一個請求物件裡去。” 下面這段從 Tomcat 裡抽出來的程式碼就展示了 ServletRequestWrapper 的實現方式。

public class ServletRequestWrapper implements ServletRequest {

    private ServletRequest request;

    /**
     * Creates a ServletRequest adaptor wrapping the given request object. 
     * 建立一個裝有給定的請求物件的 ServletRequest 介面卡
     * @throws java.lang.IllegalArgumentException if the request is null
     * 如果請求物件為空就會丟擲空指標異常
     */
    public ServletRequestWrapper(ServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");   
        }
        this.request = request;
    }

    public ServletRequest getRequest() {
        return this.request;
    }
    
    public Object getAttribute(String name) {
        return this.request.getAttribute(name);
    }

    // 為可讀性著想, 接下來的程式碼就略了
}

Servlt 2.3 規範還對 ServletRequestWrapper 定義了一個子類 HttpServletRequestWrapper。我們可以用它來快速地實現一個自定義的 HttpServletRequest。下面這段從 Tomcat 裡抽出來的程式碼就展示了 HttpServletRequestWrapper 這個類的實現方式。

public class HttpServletRequestWrapper extends ServletRequestWrapper 
    implements HttpServletRequest {

    public HttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    private HttpServletRequest _getHttpServletRequest() {
        return (HttpServletRequest) super.getRequest();
    }
  
    public HttpSession getSession(boolean create) {
     return this._getHttpServletRequest().getSession(create);
    }
   
    public HttpSession getSession() {
      return this._getHttpServletRequest().getSession();
    }
    
    // 為可讀性著想,接下來的程式碼就略了  
}

因此,我們就可以用這些包裝類來編寫一些擴充套件 HttpServletRequest 功能的程式碼,過載返回 HttpSession 的方法,使得後者返回的是我們儲存在外部儲存倉庫裡面的會話。這裡就給出一份從 Spring Session 專案提出來的原始碼就對應了這裡提到的東西。為了能對應這裡的解釋,原始碼裡面原本的註釋被我重寫了一下,在此不妨也看一看裡面的註釋。

/*
 * Spring Session 專案定義了一個繼承了標準 HttpServletRequestWrapper 的類
 * 它過載了 HttpServletRequest 裡面的所有跟會話有關的方法
 */
private final class SessionRepositoryRequestWrapper
   extends HttpServletRequestWrapper {

   private HttpSessionWrapper currentSession;
   private Boolean requestedSessionIdValid;
   private boolean requestedSessionInvalidated;
   private final HttpServletResponse response;
   private final ServletContext servletContext;

   /*
   * 構造方法這塊非常簡單
   * 它會接收並設定一些之後會用到的引數,
   * 然後完成對 HttpServletRequestWrapper 的代理
   */
   private SessionRepositoryRequestWrapper(
      HttpServletRequest request,
      HttpServletResponse response,
      ServletContext servletContext) {
     super(request);
     this.response = response;
     this.servletContext = servletContext;
   }

   /*
   * Spring Session 便在這裡用自己對返回儲存於外部資料來源的會話資料的實現
   * 取代了對應用伺服器提供的預設方法的代理呼叫.
   * 
   * 這裡的實現會先檢查它是不是已經有一個對應的會話. 
   * 若有那就返回之, 否則就會檢查當前的請求附帶的會話 ID 是否確實對應著一個會話
   * 若有, 那就用這個會話 ID 從 SessionRepository 裡邊載入這個會話;
   * 若外部資料來源裡沒這個會話, 或者這個會話 ID 沒對應的會話,
   * 那就建立一個新的會話, 並把它存在會話資料儲存裡面.
   */
   @Override
   public HttpSession getSession(boolean create) {
     if(currentSession != null) {
       return currentSession;
     }
     String requestedSessionId = getRequestedSessionId();
     if(requestedSessionId != null) {
       S session = sessionRepository.getSession(requestedSessionId);
       if(session != null) {
         this.requestedSessionIdValid = true;
         currentSession = new HttpSessionWrapper(session, getServletContext());
         currentSession.setNew(false);
         return currentSession;
       }
     }
     if(!create) {
       return null;
     }
     S session = sessionRepository.createSession();
     currentSession = new HttpSessionWrapper(session, getServletContext());
     return currentSession;
   }

   @Override
   public HttpSession getSession() {
     return getSession(true);
   }
}

Spring Session 同時定義了一個 ServletFilter 介面的實現類 SessionRepositoryFilter。這裡也會給出這個過濾器的實現的核心部分的原始碼,並且也會附上一些對應本文內容的註釋,不妨也看一看。

/*
 * SessionRepositoryFilter 是一個標準 ServletFilter 的實現.
 * 其目的是從它的基類擴充套件出一些功能來.
 */
public class SessionRepositoryFilter < S extends ExpiringSession >
    extends OncePerRequestFilter {

    /*
     * 這一方法就是核心部分.
     * 該方法會建立一個我們在上面介紹過的包裝請求的例項,
     * 然後拿這個包裝過的請求再過一遍過濾器鏈的剩餘部分.
     * 關鍵的地方在於,應用在執行位於這個過濾器之後的程式碼時,
     * 如果要獲取會話的資料, 那這個包裝過的請求就會返回 Spring Session
     * 所儲存在外部資料來源的 HttpServletSession 例項.
     */
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

        request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);

        SessionRepositoryRequestWrapper wrappedRequest =
          new SessionRepositoryRequestWrapper(request,response,servletContext);

        SessionRepositoryResponseWrapper wrappedResponse =
          new SessionRepositoryResponseWrapper(wrappedRequest, response);

        HttpServletRequest strategyRequest =
             httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);

        HttpServletResponse strategyResponse =
             httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);

        try {
            filterChain.doFilter(strategyRequest, strategyResponse);
        } finally {
            wrappedRequest.commitSession();
        }
    }
}

這一節的重點在於,基於 HTTP 的 Spring Session 其實也只是一個用了 Servlet 規範的標準特性來實現功能的經典的 Servlet 過濾器而已。因此,將現有的 Web 應用的 war 檔案改成使用 Spring Session 是應該可以不用改動已有程式碼的。然而,在應用裡面用了 javax.servlet.http.HttpSessionListener 的情況則是例外。Spring Session 1.0 並沒有對 HttpSessionListener 提供支援,不過 Spring Session 1.1 M1 版本則對其新增了支援。詳情見此

Spring Session 的設定

在 Web 專案裡面,Spring Session 的設定分為四步:

  • 設定在 Spring Session 中使用的資料儲存
  • 將 Spring Session 的 .jar 檔案新增到 Web 應用中
  • 將 Spring Session 的過濾器新增到 Web 應用的配置中
  • 設定從 Spring Session 到所選會話資料儲存的連線

Spring Session 內建了對 Redis 的支援。安裝和設定 redis 的詳細資訊見此

完成上述 Spring Session 的設定步驟的常見方式有兩種。一種是使用 Spring Boot 來自動設定 Spring Session。另外一種則是手動完成每一個配置步驟。

用 Maven 和 Gradle 等依賴管理工具可以很輕鬆地將 Spring Session 加入到應用的依賴專案裡面。比如說,如果你用的是 Spring Boot + Maven,那麼就可以在 pom.xml 裡面加上以下依賴專案:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
    <version>1.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

spring-boot-starter-redis 這一依賴專案會確保跟 redis 互動所需的 jar 包都包含在應用裡面,這樣便可以使用 Spring Boot 來進行自動的配置。至於 spring-session 這一依賴專案則對應 Spring Session 的 jar 包。

設定 Spring Session Servlet 過濾器的過程可以通過 Spring Boot 自動完成,只需要在 Spring Boot 的配置類裡面加上 @EnableRedisHttpSession 註解即可。就跟下面這段程式碼一樣:

@SpringBootApplication
@EnableRedisHttpSession
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

將下面這些配置資訊加到 Spring Boot 的 application.properties 檔案即可設定 Spring Session 到 Redis 的連線。

spring.redis.host=localhost
spring.redis.password=secret
spring.redis.port=6379

為了設定和 Redis 的連線,Spring Boot 提供了一套詳實的底層架構,使得我們可以在其中任意設定一種跟 Redis 建立連線的方式。你能在 Spring Session 還有 Spring Boot 裡面找到按部就班進行的指南

使用 web.xml 來設定傳統的 Web 應用去使用 Spring Session 的教程見此

設定傳統的不帶有 web.xml 的 war 檔案去使用 Spring Session 的教程見此

在預設情況下,Spring Session 會使用 HTTP cookie 來儲存會話 ID,但是我們也可以將 Spring Session 設定成使用自定義的 HTTP 報文首部欄位(例如 x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3

)來儲存會話 ID,而這在構建 RESTful API 的時候會非常有用。完整教程見此

Spring Session 的用法

在配置了 Spring Session 之後,我們就可以使用標準的 Servlet API 去和它進行互動了。比如下面這段程式碼就定義了一個使用標準 Servlet 會話 API 來訪問會話資料的 servlet。

@WebServlet("/example")
public class Example extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    // 使用標準的 servlet API 去獲取對應的會話資料
    // 這一會話資料就是 Spring Session 存在 Redis
    // 或是別的我們所指定的資料來源裡面的會話資料

    HttpSession session = request.getSession();
    String value = session.getAttribute(“someAttributeâ€);

  }
}

一個瀏覽器,多個會話

Spring Session 通過使用一個叫做 _s 的會話代號引數來跟蹤每個使用者的多個會話。假如有個傳入請求的 URL 是 http://example.com/doSomething?_s=0,那麼 Spring Session 就會讀取 _s 引數的值,然後便會認為這個請求對應的是預設的會話。

如果傳入請求的 URL 是 http://example.com/doSomething?_s=1,那麼 Spring Session 就會知道這個請求對應的會話的代號是 1。如果傳入請求沒有指定引數 _s,那麼 Spring Session 就會把它視為對應預設對話(即 _s = 0)。

為了讓每個瀏覽器都建立一個新的會話,我們只需像以前那樣呼叫 javax.servlet.http.HttpServletRequest.getSession(),然後 Spring Session 就會返回對應的會話,或者使用 Servlet 規範的語義建立一個新的會話。下表便給出了 getSession() 方法在同一瀏覽器的不同的 URL 引數下的具體表現形式:

HTTP 請求 URL 會話代號 getSession() 的具體表現
example.com/resource 0 如果存在與代號 0 相關聯的會話就返回之,否則就建立一個新會話,然後將其與代號 0 關聯起來
example.com/resource?_s=1 1 如果存在與代號 1 相關聯的會話就返回之,否則就建立一個新會話,然後將其與代號 1 關聯起來
example.com/resource?_s=0 0 如果存在與代號 0 相關聯的會話就返回之,否則就建立一個新會話,然後將其與代號 0 關聯起來
example.com/resource?_s=abc abc 如果存在與代號 abc 相關聯的會話就返回之,否則就建立一個新會話,然後將其與代號 abc 關聯起來

如上表所示,會話代號並不侷限於整數,只要與釋出給該使用者的所有其他會話別名不同,即可對一個一個新的會話。然而,整數型別的會話代號應該是最易用的,並且 Spring Session 也給出了 HttpSessionManager 來提供一些處理會話代號的實用方法。

我們可以通過 "org.springframework.session.web.HttpSessionManager" 這個屬性名來查詢相應屬性,進而訪問到 HttpSessionManager。下面這段程式碼就演示了獲得 HttpSessionManager 的引用的方法,以及這個實用方法類的一些主要的方法。

@WebServlet("/example")
public class Example extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest request,HttpServletResponse response)
  throws ServletException, IOException {

    /*
     * 通過使用 "org.springframework.session.web.http.HttpSessionManager"
     * 這一屬性名在請求屬性中查詢屬性
     * 來獲取一個 Spring Session 的 HttpSessionManager 的引用
     */

    HttpSessionManager sessionManager=(HttpSessionManager)request.getAttribute(
        "org.springframework.session.web.http.HttpSessionManager");

    /*
     * 用 HttpSessionManager 來找出 HTTP 請求所對應的會話代號.
     * 預設情況下這個會話代號會由 HTTP 請求的 URL 引數 _s 給出。
     * 比如 http://localhost:8080/example?_s=1 這個 URL
     * 就會讓這裡的 println() 方法列印 "Requested Session Alias is: 1"
     */
    String requestedSessionAlias=sessionManager.getCurrentSessionAlias(request);
    System.out.println("Requested Session Alias is:  " + requestedSessionAlias);

    /* 
     * 返回一個當前還沒被瀏覽器用在請求引數裡的唯一的會話代號.
     * 注意這一方法並不會建立一個新的會話, 
     * 建立新的會話還是要通過 request.getSession() 來進行.
     */
    String newSessionAlias = sessionManager.getNewSessionAlias(request);

    /* 
     * 使用剛剛得到的新會話代號構造一個 URL,
     * 使其含有 _s 這個引數.
     * 比如若 newSessionAlias 的值是 2,
     * 那麼這個方法就會返回 "/inbox?_s=3" 
     */
    String encodedURL = sessionManager.encodeURL("/inbox", newSessionAlias);
    System.out.println(encodedURL);

    /* 
     * 返回一個會話代號為鍵, 會話 ID 為值的 Map, 
     * 以便識別瀏覽器發來的請求所對應的會話.
     */
    Map <String, String> sessionIds = sessionManager.getSessionIds(request);
  }
}

結論

Spring Session 為企業級 Java 應用的會話管理領域帶來了革新,讓我們可以輕鬆做到:

  • 編寫可橫向伸縮的雲原生應用
  • 將會話狀態的儲存外放到專門的外部會話儲存裡,比如 Redis 或 Apache Geode,後者以獨立於應用程式伺服器的方式提供了高質量的儲存叢集
  • 在使用者通過 WebSocket 發出請求的時候保持 HttpSession 的線上狀態
  • 訪問來自非 Web 請求處理指令的會話資料,比如 JMS 訊息處理指令
  • 為每個瀏覽器建立多個會話提供支援,從而構建更豐富的終端使用者體驗
  • 控制在客戶端和伺服器間交換會話 ID 的方式,從而編寫在 HTTP 報文首部中提取會話 ID 而脫離對 Cookie 的依賴的 RESTul API

若你在尋找一種從傳統又笨重的應用伺服器中解放的方法,但又囿於對應用伺服器的會話儲存叢集功能的依賴,那麼 Spring Session 對像 Tomcat、Jetty 還有 Undertow 這樣的容器的輕量化來說是很好的一個選擇。

參考資料

Spring Session 專案

Spring Session 教程及指南

Websocket / HttpSession 超時互動

網路研討會:Spring Session 導論

問答
傳統Web應用程式和API中的身份驗證、授權和會話管理如何實現?
相關閱讀
架構設計之Spring Session分散式叢集會話管理 
Spring Session關鍵類原始碼分析
一個可以把web表單變成會話形式的開源框架

此文已由作者授權騰訊雲+社群釋出,原文連結:https://cloud.tencent.com/dev…

歡迎大家前往騰訊雲+社群或關注雲加社群微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~

相關文章