Re:從零開始的Spring Session(一)

 

 

Session和Cookie這兩個概念,在學習java web開發之初,大多數人就已經接觸過了。最近在研究跨域單點登陸的實現時,發現對於Session和Cookie的瞭解,並非很深刻,因此打算寫兩篇文章記錄一下本身的理解。在咱們的應用集成Spring Session以前,先補充一點Session和Cookie的關鍵知識。javascript

Session與Cookie基礎

因爲http協議是無狀態的協議,爲了可以記住請求的狀態,因而引入了Session和Cookie的機制。咱們應該有一個很明確的概念,那就是Session是存在於服務器端的,在單體式應用中,他是由tomcat管理的,存在於tomcat的內存中,當咱們爲了解決分佈式場景中的session共享問題時,引入了redis,其共享內存,以及支持key自動過時的特性,很是契合session的特性,咱們在企業開發中最經常使用的也就是這種模式。可是隻要你願意,也能夠選擇存儲在JDBC,Mongo中,這些,spring都提供了默認的實現,在大多數狀況下,咱們只須要引入配置便可。而Cookie則是存在於客戶端,更方便理解的說法,能夠說存在於瀏覽器。Cookie並不經常使用,至少在我不長的web開發生涯中,並無什麼場景須要我過多的關注Cookie。http協議容許從服務器返回Response時攜帶一些Cookie,而且同一個域下對Cookie的數量有所限制,以前說過Session的持久化依賴於服務端的策略,而Cookie的持久化則是依賴於本地文件。雖說Cookie並不經常使用,可是有一類特殊的Cookie倒是咱們須要額外關注的,那即是與Session相關的sessionId,他是真正維繫客戶端和服務端的橋樑。php

代碼示例

用戶發起請求,服務器響應請求,並作一些用戶信息的處理,隨後返回響應給用戶;用戶再次發起請求,攜帶sessionId,服務器便可以識別,這個用戶就是以前請求的那個。html

使用Springboot編寫一個很是簡單的服務端,來加深對其的理解。需求很簡單,當瀏覽器訪問localhost:8080/test/cookie?browser=xxx時,若是沒有獲取到session,則將request中的browser存入session;若是獲取到session,便將session中的browser值輸出。順便將request中的全部cookie打印出來。前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
public class CookieController {

@RequestMapping("/test/cookie")
public String cookie(@RequestParam("browser") String browser, HttpServletRequest request, HttpSession session) {
//取出session中的browser
Object sessionBrowser = session.getAttribute("browser");
if (sessionBrowser == null) {
System.out.println("不存在session,設置browser=" + browser);
session.setAttribute("browser", browser);
} else {
System.out.println("存在session,browser=" + sessionBrowser.toString());
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + " : " + cookie.getValue());
}
}
return "index";
}
}

咱們沒有引入其餘任何依賴,看看原生的session機制是什麼。html5

1 使用chrome瀏覽器,訪問localhost:8080/test/cookie?browser=chrome,控制檯輸出以下:java

1
Session Info:	不存在session,設置browser=chrome

既沒有session,也沒有cookie,咱們將browser=chrome設置到session中。nginx

再次訪問一樣的端點,控制檯輸出以下:git

1
2
Session Info:	存在session,browser=chrome
Cookie Info: JSESSIONID : 4CD1D96E04FC390EA6C60E8C40A636AF

屢次訪問以後,控制檯依舊打印出一樣的信息。github

稍微解讀下這個現象,能夠驗證一些結論。當服務端往session中保存一些數據時,Response中自動添加了一個Cookie:JSESSIONID:xxxx,再後續的請求中,瀏覽器也是自動的帶上了這個Cookie,服務端根據Cookie中的JSESSIONID取到了對應的session。這驗證了一開始的說法,客戶端服務端是經過JSESSIONID進行交互的,而且,添加和攜帶key爲JSESSIONID的Cookie都是tomcat和瀏覽器自動幫助咱們完成的,這很關鍵。web

2 使用360瀏覽器,訪問localhost:8080/test/cookie?browser=360

第一次訪問:

1
Session Info:	不存在session,設置browser=360

後續訪問:

1
2
Session Info:	存在session,browser=360
Cookie Info: JSESSIONID : 320C21A645A160C4843D076204DA2F40

爲何要再次使用另外一個瀏覽器訪問呢?先賣個關子,咱們最起碼能夠得出結論,不一樣瀏覽器,訪問是隔離的,甚至從新打開同一個瀏覽器,JSESSIONID也是不一樣的。另外能夠嘗試把保存session的操做注視掉,則能夠發現Response中就不會返回JSESSIONID了,即這是一次無狀態的請求。

安全問題

其實上述的知識點,都是很是淺顯的,之因此囉嗦一句,是爲了引出這一節的內容,以及方便觀察後續咱們引入Spring Session以後的發生的變化。

還記得上一節的代碼示例中,咱們使用了兩個瀏覽器:

  • chrome瀏覽器訪問時,JSESSIONID爲4CD1D96E04FC390EA6C60E8C40A636AF,後端session記錄的值爲:browser=chrome。
  • 360瀏覽器訪問時,JSESSIONID爲320C21A645A160C4843D076204DA2F40,後端session記錄的值爲:browser=360。

咱們使用chrome插件Edit this Cookie,將chrome瀏覽器中的JSESSIONID修改成360瀏覽器中的值

EditThisCookieEditThisCookie

一樣訪問原來的端點:localhost:8080/test/cookie?browser=chrome,獲得的輸出以下:

1
2
存在session,browser=360
JSESSIONID : 320C21A645A160C4843D076204DA2F40

證明了一點,存放在客戶端的Cookie的確是存在安全問題的,咱們使用360的JSESSIONID「騙」過了服務器。畢竟,服務器只能經過Cookie中的JSESSIONID來辨別身份。(這提示咱們不要在公共場合保存Cookie信息,如今的瀏覽器在保存Cookie時一般會讓你肯定一次)

下一篇文章,將正式講解如何在應用中集成Spring Session。

https://www.cnkirito.moe/spring-session-1/

 

 

This guide describes how to configure Spring Session to use custom cookies with Java Configuration. The guide assumes you have already set up Spring Session in your project.

  You can find the completed guide in the Custom Cookie sample application.

Once you have set up Spring Session, you can customize how the session cookie is written by exposing a CookieSerializer as a Spring bean. Spring Session comes with DefaultCookieSerializer. Exposing the DefaultCookieSerializer as a Spring bean augments the existing configuration when you use configurations like @EnableRedisHttpSession. The following example shows how to customize Spring Session’s cookie:

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID"); 
        serializer.setCookiePath("/"); 
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); 
        return serializer;
    }
  We customize the name of the cookie to be JSESSIONID.
  We customize the path of the cookie to be / (rather than the default of the context root).
  We customize the domain name pattern (a regular expression) to be ^.?\\.(\\w\\.[a-z]+)$. This allows sharing a session across domains and applications. If the regular expression does not match, no domain is set and the existing domain is used. If the regular expression matches, the first grouping is used as the domain. This means that a request to https://child.example.com sets the domain to example.com. However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and, thus, still works in development without any changes being necessary for production.

 

https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-custom-cookie.html

 

 

spring-session(一)揭祕

前言

在開始spring-session揭祕以前,先作下熱腦(活動活動腦子)運動。主要從如下三個方面進行熱腦:

  1. 爲何要spring-session
  2. 比較traditional-session方案和spring-session方案
  3. JSR340規範與spring-session的透明繼承

一.爲何要spring-session

在傳統單機web應用中,通常使用tomcat/jetty等web容器時,用戶的session都是由容器管理。瀏覽器使用cookie中記錄sessionId,容器根據sessionId判斷用戶是否存在會話session。這裏的限制是,session存儲在web容器中,被單臺服務器容器管理。

可是網站主鍵演變,分佈式應用和集羣是趨勢(提升性能)。此時用戶的請求可能被負載分發至不一樣的服務器,此時傳統的web容器管理用戶會話session的方式即行不通。除非集羣或者分佈式web應用可以共享session,儘管tomcat等支持這樣作。可是這樣存在如下兩點問題:

  • 須要侵入web容器,提升問題的複雜
  • web容器之間共享session,集羣機器之間勢必要交互耦合

基於這些,必須提供新的可靠的集羣分佈式/集羣session的解決方案,突破traditional-session單機限制(即web容器session方式,下面簡稱traditional-session),spring-session應用而生。

二.比較traditional-session方案和spring-session方案

下圖展現了traditional-session和spring-session的區別

 

 

傳統模式中,當request進入web容器,根據reqest獲取session時,若是web容器中存在session則返回,若是不存在,web容器則建立一個session。而後返回response時,將sessonId做爲response的head一併返回給客戶端或者瀏覽器。

可是上節中說明了traditional-session的侷限性在於:單機session。在此限制的相反面,即將session從web容器中抽出來,造成獨立的模塊,以便分佈式應用或者集羣都能共享,即能解決。

spring-session的核心思想在於此:將session從web容器中剝離,存儲在獨立的存儲服務器中。目前支持多種形式的session存儲器:Redis、Database、MogonDB等。session的管理責任委託給spring-session承擔。當request進入web容器,根據request獲取session時,由spring-session負責存存儲器中獲取session,若是存在則返回,若是不存在則建立並持久化至存儲器中。

三.JSR340規範與spring-session的透明繼承

JSR340是Java Servlet 3.1的規範提案,其中定義了大量的api,包括:servlet、servletRequest/HttpServletRequest/HttpServletRequestWrapper、servletResponse/HttpServletResponse/HttpServletResponseWrapper、Filter、Session等,是標準的web容器須要遵循的規約,如tomcat/jetty/weblogic等等。

在平常的應用開發中,develpers也在頻繁的使用servlet-api,好比:

如下的方式獲取請求的session:

HttpServletRequest request = ... HttpSession session = request.getSession(false);

其中HttpServletRequest和HttpSession都是servlet規範中定義的接口,web容器實現的標準。那若是引入spring-session,要如何獲取session?

  • 遵循servlet規範,一樣方式獲取session,對應用代碼無侵入且對於developers透明化
  • 全新實現一套session規範,定義一套新的api和session管理機制

兩種方案均可以實現,可是顯然第一種更友好,且具備兼容性。spring-session正是第一種方案的實現。

實現第一種方案的關鍵點在於作到透明和兼容

  • 接口適配:仍然使用HttpServletRequest獲取session,獲取到的session仍然是HttpSession類型——適配器模式
  • 類型包裝加強:Session不能存儲在web容器內,要外化存儲——裝飾模式

讓人興奮的是,以上的需求在Servlet規範中的擴展性都是予以支持!Servlet規範中定義一系列的接口都是支持擴展,同時提供Filter支撐擴展點。建議閱讀《JavaTM Servlet Specification》。

熱腦活動結束,下面章節正式進入今天的主題:spring-session揭祕

Spring Session探索

主要從如下兩個方面來講spring-session:

  • 特色
  • 工做原理

一.特色

spring-session在無需綁定web容器的狀況下提供對集羣session的支持。並提供對如下狀況的透明集成:

  • HttpSession:允許替換web容器的HttpSession
  • WebSocket:使用WebSocket通訊時,提供Session的活躍
  • WebSession:允許以應用中立的方式替換webflux的webSession

二.工做原理

再詳細閱讀源碼以前先來看張圖,介紹下spring-session中的核心模塊以及之間的交互。

 

 

spring-session分爲如下核心模塊:

  • SessionRepositoryFilter:Servlet規範中Filter的實現,用來切換HttpSession至Spring Session,包裝HttpServletRequest和HttpServletResponse
  • HttpServerletRequest/HttpServletResponse/HttpSessionWrapper包裝器:包裝原有的HttpServletRequest、HttpServletResponse和Spring Session,實現切換Session和透明繼承HttpSession的關鍵之所在
  • Session:Spring Session模塊
  • SessionRepository:管理Spring Session的模塊
  • HttpSessionStrategy:映射HttpRequst和HttpResponse到Session的策略
1. SessionRepositoryFilter

SessionRepositoryFilter是一個Filter過濾器,符合Servlet的規範定義,用來修改包裝請求和響應。這裏負責包裝切換HttpSession至Spring Session的請求和響應。

@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 設置SessionRepository至Request的屬性中 request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); // 包裝原始HttpServletRequest至SessionRepositoryRequestWrapper SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); // 包裝原始HttpServletResponse響應至SessionRepositoryResponseWrapper SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper( wrappedRequest, response); // 設置當前請求的HttpSessionStrategy策略 HttpServletRequest strategyRequest = this.httpSessionStrategy .wrapRequest(wrappedRequest, wrappedResponse); // 設置當前響應的HttpSessionStrategy策略 HttpServletResponse strategyResponse = this.httpSessionStrategy .wrapResponse(wrappedRequest, wrappedResponse); try { filterChain.doFilter(strategyRequest, strategyResponse); } finally { // 提交session wrappedRequest.commitSession(); } }

以上是SessionRepositoryFilter的核心操做,每一個HttpRequest進入,都會被該Filter包裝成切換Session的請求很響應對象。

Tips:責任鏈模式
Filter是Servlet規範中的很是重要的組件,在tomcat的實現中使用了責任鏈模式,將多個Filter組織成鏈式調用。Filter的做用就是在業務邏輯執行先後對請求和響應作修改配置。配合HttpServletRequestWrapper和HttpServletResponseWrapper使用,可謂威力驚人!

2. SessionRepositoryRequestWrapper

對於developers獲取HttpSession的api

HttpServletRequest request = ...; HttpSession session = request.getSession(true);

在spring session中request的實際類型SessionRepositoryRequestWrapper。調用SessionRepositoryRequestWrapper的getSession方法會觸發建立spring session,而非web容器的HttpSession。

SessionRepositoryRequestWrapper用來包裝原始的HttpServletRequest實現HttpSession切換至Spring Session。是透明Spring Session透明集成HttpSession的關鍵。

private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper { private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class .getName(); // 當前請求sessionId有效 private Boolean requestedSessionIdValid; // 當前請求sessionId無效 private boolean requestedSessionInvalidated; private final HttpServletResponse response; private final ServletContext servletContext; private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) { // 調用HttpServletRequestWrapper構造方法,實現包裝 super(request); this.response = response; this.servletContext = servletContext; } }

SessionRepositoryRequestWrapper繼承Servlet規範中定義的包裝器HttpServletRequestWrapper。HttpServletRequestWrapper是Servlet規範api提供的用於擴展HttpServletRequest的擴張點——即裝飾器模式,能夠經過重寫一些api達到功能點的加強和自定義。

Tips:裝飾器模式
裝飾器模式(包裝模式)是對功能加強的一種絕佳模式。實際利用的是面向對象的多態性實現擴展。Servlet規範中開放此HttpServletRequestWrapper接口,是讓developers自行擴展實現。這種使用方式和jdk中的FilterInputStream/FilterInputStream一模一樣。

HttpServletRequestWrapper中持有一個HttpServletRequest對象,而後實現HttpServletRequest接口的全部方法,全部方法實現中都是調用持有的HttpServletRequest對象的相應的方法。繼承HttpServletRequestWrapper 能夠對其重寫。SessionRepositoryRequestWrapper繼承HttpServletRequestWrapper,在構造方法中將原有的HttpServletRequest經過調用super完成對HttpServletRequestWrapper中持有的HttpServletRequest初始化賦值,而後重寫和session相關的方法。這樣就保證SessionRepositoryRequestWrapper的其餘方法調用都是使用原有的HttpServletRequest的數據,只有session相關的是重寫的邏輯。

Tips:
這裏的設計是否很精妙!一切都多虧與Servlet規範設計的的巧妙啊!

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

重寫HttpServletRequest的getSession()方法,調用有參數getSession(arg)方法,默認爲true,表示當前reques沒有session時建立session。繼續看下有參數getSession(arg)的重寫邏輯.

@Override public HttpSessionWrapper getSession(boolean create) { // 從當前請求的attribute中獲取session,若是有直接返回 HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } // 獲取當前request的sessionId,這裏使用了HttpSessionStrategy // 決定怎樣將Request映射至Session,默認使用Cookie策略,即從cookies中解析sessionId String requestedSessionId = getRequestedSessionId(); // 請求的若是sessionId存在且當前request的attribute中的沒有session失效屬性 // 則根據sessionId獲取spring session if (requestedSessionId != null && getAttribute(INVALID_SESSION_ID_ATTR) == null) { S session = getSession(requestedSessionId); // 若是spring session不爲空,則將spring session包裝成HttpSession並 // 設置到當前Request的attribute中,防止同一個request getsession時頻繁的到存儲器 //中獲取session,提升性能 if (session != null) { this.requestedSessionIdValid = true; currentSession = new HttpSessionWrapper(session, getServletContext()); currentSession.setNew(false); setCurrentSession(currentSession); return currentSession; } // 若是根據sessionId,沒有獲取到session,則設置當前request屬性,此sessionId無效 // 同一個請求中獲取session,直接返回無效 else { // This is an invalid session id. No need to ask again if // request.getSession is invoked for the duration of this request if (SESSION_LOGGER.isDebugEnabled()) { SESSION_LOGGER.debug( "No session found by id: Caching result for getSession(false) for this HttpServletRequest."); } setAttribute(INVALID_SESSION_ID_ATTR, "true"); } } // 判斷是否建立session if (!create) { return null; } if (SESSION_LOGGER.isDebugEnabled()) { SESSION_LOGGER.debug( "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + SESSION_LOGGER_NAME, new RuntimeException( "For debugging purposes only (not an error)")); } // 根據sessionRepository建立spring session S session = SessionRepositoryFilter.this.sessionRepository.createSession(); // 設置session的最新訪問時間 session.setLastAccessedTime(System.currentTimeMillis()); // 包裝成HttpSession透明化集成 currentSession = new HttpSessionWrapper(session, getServletContext()); // 設置session至Requset的attribute中,提升同一個request訪問session的性能 setCurrentSession(currentSession); return currentSession; }

再來看下spring session的持久化。上述SessionRepositoryFilter在包裝HttpServletRequest後,執行FilterChain中使用finally保證請求的Session始終session會被提交,此提交操做中將sesionId設置到response的head中並將session持久化至存儲器中。

持久化只持久spring session,並非將spring session包裝後的HttpSession持久化,由於HttpSession不過是包裝器,持久化沒有意義。

/** * Uses the HttpSessionStrategy to write the session id to the response and * persist the Session. */ private void commitSession() { // 獲取當前session HttpSessionWrapper wrappedSession = getCurrentSession(); // 若是當前session爲空,則刪除cookie中的相應的sessionId if (wrappedSession == null) { if (isInvalidateClientSession()) { SessionRepositoryFilter.this.httpSessionStrategy .onInvalidateSession(this, this.response); } } else { // 從HttpSession中獲取當前spring session S session = wrappedSession.getSession(); // 持久化spring session至存儲器 SessionRepositoryFilter.this.sessionRepository.save(session); // 若是是新建立spring session,sessionId到response的cookie if (!isRequestedSessionIdValid() || !session.getId().equals(getRequestedSessionId())) { SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session, this, this.response); } } }

再來看下包裝的響應SessionRepositoryResponseWrapper。

3.SessionRepositoryResponseWrapper

/** * Allows ensuring that the session is saved if the response is committed. * * @author Rob Winch * @since 1.0 */ private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper { private final SessionRepositoryRequestWrapper request; /** * Create a new {@link SessionRepositoryResponseWrapper}. * @param request the request to be wrapped * @param response the response to be wrapped */ SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) { super(response); if (request == null) { throw new IllegalArgumentException("request cannot be null"); } this.request = request; } @Override protected void onResponseCommitted() { this.request.commitSession(); } }

上面的註釋已經很是詳細,這裏再也不贅述。這裏只講述爲何須要包裝原始的響應。從註釋上能夠看出包裝響應時爲了:確保若是響應被提交session可以被保存

這裏我有點疑惑:在上述的SessionRepositoryFilter.doFilterInternal方法中不是已經request.commitSession()了嗎,FilterChain執行完或者異常後都會執行Finally中的request.commitSession。爲何這裏仍然須要包裝響應,爲了確保session可以保存,包裝器中的onResponseCommitted方法能夠看出也是作了一次request.commitSession()。難道這不是畫蛇添足?

Tips
若是有和我相同疑問的同窗,那就說明咱們的基礎都不紮實,對Servlet仍然沒有一個清楚全面的認識。對於此問題,我特地在github上提了issuse:Why is the request.commitSession() method called repeatedly?

可是在提完issue後的回家路上,我思考了下response能夠有流方式的寫,會不會在response.getOutStream寫的時候已經將響應所有返回到客戶端,這時響應結束。

在家中是,spring sesion做者大大已經回覆了個人issue:

Is this causing you problems? The reason is that we need to ensure that the session is created before the response is committed. If the response is already committed there will be no way to track the session (i.e. a cookie cannot be written to the response to keep track of which session id).

他的意思是:咱們須要在response被提交以前確保session被建立。若是response已經被提交,將沒有辦法追蹤session(例如:沒法將cookie寫入response以跟蹤哪一個session id)。

在此以前我又閱讀了JavaTM Servlet Specification,規範中這樣解釋Response的flushBuffer接口:

The isCommitted method returns a boolean value indicating whether any response bytes have been returned to the client. The flushBuffer method forces content in the buffer to be written to the client.

而且看了ServletResponse的flushBuffer的javadocs:

/** * Forces any content in the buffer to be written to the client. A call to * this method automatically commits the response, meaning the status code * and headers will be written. * * @throws IOException if an I/O occurs during the flushing of the response * * @see #setBufferSize * @see #getBufferSize * @see #isCommitted * @see #reset */ public void flushBuffer() throws IOException; 

結合以上兩點,一旦response執行flushBuffer方法,迫使Response中在Buffer中任何數據都會被返回至client端。這個方法自動提交響應中的status code和head。那麼若是不包裝請求,監聽flushBuffer事件在提交response前,將session寫入response和持久化session,將致使做者大大說的沒法追蹤session。

SessionRepositoryResponseWrapper繼承父類OnCommittedResponseWrapper,其中flushBuffer方法以下:

/** * Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked * before calling the superclass <code>flushBuffer()</code>. * @throws IOException if an input or output exception occurred */ @Override public void flushBuffer() throws IOException { doOnResponseCommitted(); super.flushBuffer(); } /** * Calls <code>onResponseCommmitted()</code> with the current contents as long as * {@link #disableOnResponseCommitted()} was not invoked. */ private void doOnResponseCommitted() { if (!this.disableOnCommitted) { onResponseCommitted(); disableOnResponseCommitted(); } }

重寫HttpServletResponse方法,監聽response commit,當發生response commit時,能夠在commit以前寫session至response中並持久化session。

Tips:
spring mvc中HttpMessageConverters使用到的jackson即調用了outstream.flushBuffer(),當使用@ResponseBody時。

以上作法當然合理,可是如此重複操做兩次commit,存在兩次persist session?
這個問題後面涉及SessionRepository時再詳述!

再看SessionRepository以前,先來看下spring session中的session接口。

3.Session接口

spring-session和tomcat中的Session的實現模式上有很大不一樣,tomcat中直接對HttpSession接口進行實現,而spring-session中則抽象出單獨的Session層接口,讓後再使用適配器模式將Session適配層Servlet規範中的HttpSession。spring-sesion中關於session的實現和適配整個UML類圖以下:

 

 

Tips:適配器模式
spring-session單獨抽象出Session層接口,能夠應對多種場景下不一樣的session的實現,而後經過適配器模式將Session適配成HttpSession的接口,精妙至極!

Session是spring-session對session的抽象,主要是爲了鑑定用戶,爲Http請求和響應提供上下文過程,該Session能夠被HttpSession、WebSocket Session,非WebSession等使用。定義了Session的基本行爲:

  • getId:獲取sessionId
  • setAttribute:設置session屬性
  • getAttribte:獲取session屬性

ExipringSession:提供Session額外的過時特性。定義瞭如下關於過時的行爲:

  • setLastAccessedTime:設置最近Session會話過程當中最近的訪問時間
  • getLastAccessedTime:獲取最近的訪問時間
  • setMaxInactiveIntervalInSeconds:設置Session的最大閒置時間
  • getMaxInactiveIntervalInSeconds:獲取最大閒置時間
  • isExpired:判斷Session是否過時

MapSession:基於java.util.Map的ExpiringSession的實現

RedisSession:基於MapSession和Redis的ExpiringSession實現,提供Session的持久化能力

先來看下MapSession的代碼源碼片斷

public final class MapSession implements ExpiringSession, Serializable { /** * Default {@link #setMaxInactiveIntervalInSeconds(int)} (30 minutes). */ public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800; private String id; private Map<String, Object> sessionAttrs = new HashMap<String, Object>(); private long creationTime = System.currentTimeMillis(); private long lastAccessedTime = this.creationTime; /** * Defaults to 30 minutes. */ private int maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

MapSession中持有HashMap類型的變量sessionAtts用於存儲Session設置屬性,好比調用的setAttribute方法的k-v就存儲在該HashMap中。這個和tomcat內部實現HttpSession的方式相似,tomcat中使用了ConcurrentHashMap存儲。

其中lastAccessedTime用於記錄最近的一次訪問時間,maxInactiveInterval用於記錄Session的最大閒置時間(過時時間-針對沒有Request活躍的狀況下的最大時間,即相對於最近一次訪問後的最大閒置時間)。

public void setAttribute(String attributeName, Object attributeValue) { if (attributeValue == null) { removeAttribute(attributeName); } else { this.sessionAttrs.put(attributeName, attributeValue); } }

setAttribute方法極其簡單,null時就移除attributeName,不然put存儲。

重點熟悉RedisSession如何實現Session的行爲:setAttribute、persistence等。

/** * A custom implementation of {@link Session} that uses a {@link MapSession} as the * basis for its mapping. It keeps track of any attributes that have changed. When * {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()} * is invoked all the attributes that have been changed will be persisted. * * @author Rob Winch * @since 1.0 */ final class RedisSession implements ExpiringSession { private final MapSession cached; private Long originalLastAccessTime; private Map<String, Object> delta = new HashMap<String, Object>(); private boolean isNew; private String originalPrincipalName;

首先看javadocs,對於閱讀源碼,學會看javadocs很是重要!

基於MapSession的基本映射實現的Session,可以追蹤發生變化的全部屬性,當調用saveDelta方法後,變化的屬性將被持久化!

在RedisSession中有兩個很是重要的成員屬性:

  • cached:其實是一個MapSession實例,用於作本地緩存,每次在getAttribute時無需從Redis中獲取,主要爲了improve性能
  • delta:用於跟蹤變化數據,作持久化

再來看下RedisSession中最爲重要的行爲saveDelta——持久化Session至Redis中:

/** * Saves any attributes that have been changed and updates the expiration of this * session. */ private void saveDelta() { // 若是delta爲空,則Session中沒有任何數據須要存儲 if (this.delta.isEmpty()) { return; } String sessionId = getId(); // 使用spring data redis將delta中的數據保存至Redis中 getSessionBoundHashOperations(sessionId).putAll(this.delta); String principalSessionKey = getSessionAttrNameKey( FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); String securityPrincipalSessionKey = getSessionAttrNameKey( SPRING_SECURITY_CONTEXT); if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) { if (this.originalPrincipalName != null) { String originalPrincipalRedisKey = getPrincipalKey( this.originalPrincipalName); RedisOperationsSessionRepository.this.sessionRedisOperations .boundSetOps(originalPrincipalRedisKey).remove(sessionId); } String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this); this.originalPrincipalName = principal; if (principal != null) { String principalRedisKey = getPrincipalKey(principal); RedisOperationsSessionRepository.this.sessionRedisOperations .boundSetOps(principalRedisKey).add(sessionId); } } // 清空delta,表明沒有任何須要持久化的數據。同時保證 //SessionRepositoryFilter和SessionRepositoryResponseWrapper的onResponseCommitted //只會持久化一次Session至Redis中,解決前面提到的疑問 this.delta = new HashMap<String, Object>(this.delta.size()); // 更新過時時間,滾動至下一個過時時間間隔的時刻 Long originalExpiration = this.originalLastAccessTime == null ? null : this.originalLastAccessTime + TimeUnit.SECONDS .toMillis(getMaxInactiveIntervalInSeconds()); RedisOperationsSessionRepository.this.expirationPolicy .onExpirationUpdated(originalExpiration, this); } 

從javadoc中能夠看出,saveDelta用於存儲Session的屬性:

  1. 保存Session中的屬性數據至Redis中
  2. 清空delta中數據,防止重複提交Session中的數據
  3. 更新過時時間至下一個過時時間間隔的時刻

再看下RedisSession中的其餘行爲

// 設置session的存活時間,即最大過時時間。先保存至本地緩存,而後再保存至delta public void setMaxInactiveIntervalInSeconds(int interval) { this.cached.setMaxInactiveIntervalInSeconds(interval); this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds()); flushImmediateIfNecessary(); } // 直接從本地緩存獲取過時時間 public int getMaxInactiveIntervalInSeconds() { return this.cached.getMaxInactiveIntervalInSeconds(); } // 直接從本地緩存中獲取Session中的屬性 @SuppressWarnings("unchecked") public Object getAttribute(String attributeName) { return this.cached.getAttribute(attributeName); } // 保存Session屬性至本地緩存和delta中 public void setAttribute(String attributeName, Object attributeValue) { this.cached.setAttribute(attributeName, attributeValue); this.delta.put(getSessionAttrNameKey(attributeName), attributeValue); flushImmediateIfNecessary(); }

除了MapSession和RedisSession還有JdbcSession、MongoExpiringSession,感興趣的讀者能夠自行閱讀。

下面看SessionRepository的邏輯。SessionRepository是spring session中用於管理spring session的核心組件。

4. SessionRepository

A repository interface for managing {@link Session} instances.

javadoc中描述SessionRepository爲管理spring-session的接口實例。抽象出:

S createSession(); void save(S session); S getSession(String id); void delete(String id);

建立、保存、獲取、刪除Session的接口行爲。根據Session的不一樣,分爲不少種Session操做倉庫。

 

 

這裏重點介紹下RedisOperationsSessionRepository。在詳細介紹其以前,瞭解下RedisOperationsSessionRepository的數據存儲細節。

當建立一個RedisSession,而後存儲在Redis中時,RedisSession的存儲細節以下:

spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000

Redis會爲每一個RedisSession存儲三個k-v。

  1. 第一個k-v用來存儲Session的詳細信息,包括Session的過時時間間隔、最近的訪問時間、attributes等等。這個k的過時時間爲Session的最大過時時間 + 5分鐘。若是默認的最大過時時間爲30分鐘,則這個k的過時時間爲35分鐘
  2. 第二個k-v用來表示Session在Redis中的過時,這個k-v不存儲任何有用數據,只是表示Session過時而設置。這個k在Redis中的過時時間即爲Session的過時時間間隔
  3. 第三個k-v存儲這個Session的id,是一個Set類型的Redis數據結構。這個k中的最後的1439245080000值是一個時間戳,根據這個Session過時時刻滾動至下一分鐘而計算得出。

這裏不禁好奇,爲何一個RedisSession卻如此複雜的存儲。關於這個能夠參考spring-session做者本人在github上的兩篇回答:

Why does Spring Session use spring:session:expirations?

Clarify Redis expirations and cleanup task

簡單描述下,爲何RedisSession的存儲用到了三個Key,而非一個Redis過時Key。
對於Session的實現,須要支持HttpSessionEvent,即Session建立、過時、銷燬等事件。當應用用監聽器設置監聽相應事件,Session發生上述行爲時,監聽器可以作出相應的處理。
Redis的強大之處在於支持KeySpace Notifiction——鍵空間通知。便可以監視某個key的變化,如刪除、更新、過時。當key發生上述行爲是,以即可以接受到變化的通知作出相應的處理。具體詳情能夠參考:
Redis Keyspace Notifications

可是Redis中帶有過時的key有兩種方式:

  • 當訪問時發現其過時
  • Redis後臺逐步查找過時鍵

當訪問時發現其過時,會產生過時事件,可是沒法保證key的過時時間抵達後當即生成過時事件。具體能夠參考:Timing of expired events

spring-session爲了可以及時的產生Session的過時時的過時事件,因此增長了:

spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000

spring-session中有個定時任務,每一個整分鐘都會查詢相應的spring:session:expirations:整分鐘的時間戳中的過時SessionId,而後再訪問一次這個SessionId,即spring:session:sessions:expires:SessionId,以便可以讓Redis及時的產生key過時事件——即Session過時事件。

接下來再看下RedisOperationsSessionRepository中的具體實現原理

createSession方法:
public RedisSession createSession() { // new一個RedisSession實例 RedisSession redisSession = new RedisSession(); // 若是設置的最大過時時間不爲空,則設置RedisSession的過時時間 if (this.defaultMaxInactiveInterval != null) { redisSession.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval); } return redisSession; }

再來看下RedisSession的構造方法:

/** * Creates a new instance ensuring to mark all of the new attributes to be * persisted in the next save operation. */ RedisSession() { // 設置本地緩存爲MapSession this(new MapSession()); // 設置Session的基本屬性 this.delta.put(CREATION_TIME_ATTR, getCreationTime()); this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds()); this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime()); // 標記Session的是否爲新建立 this.isNew = true; // 持久化 flushImmediateIfNecessary(); }
save方法:
public void save(RedisSession session) { // 調用RedisSession的saveDelta持久化Session session.saveDelta(); // 若是Session爲新建立,則發佈一個Session建立的事件 if (session.isNew()) { String sessionCreatedKey = getSessionCreatedChannel(session.getId()); this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); session.setNew(false); } }
getSession方法:
// 根據SessionId獲取Session,這裏的false表明的參數 // 指:若是Session已通過期,是否仍然獲取返回 public RedisSession getSession(String id) { return getSession(id, false); }

在有些狀況下,Session過時,仍然須要可以獲取到Session。這裏先來看下getSession(String id, boolean allowExpired):

private RedisSession getSession(String id, boolean allowExpired) { // 根據SessionId,從Redis獲取到持久化的Session信息 Map<Object, Object> entries = getSessionBoundHashOperations(id).entries(); // 若是Redis中沒有,則返回null if (entries.isEmpty()) { return null; } // 根據Session信息,加載建立一個MapSession對象 MapSession loaded = loadSession(id, entries); // 判斷是否容許過時獲取和Session是否過時 if (!allowExpired && loaded.isExpired()) { return null; } // 根據MapSession new一個信息的RedisSession,此時isNew爲false RedisSession result = new RedisSession(loaded); // 設置最新的訪問時間 result.originalLastAccessTime = loaded.getLastAccessedTime(); return result; }

這裏須要注意的是loaded.isExpired()和loadSession。loaded.isExpired判斷Session是否過時,若是過時返回null:

public boolean isExpired() { // 根據當前時間判斷是否過時 return isExpired(System.currentTimeMillis()); } boolean isExpired(long now) { // 若是maxInactiveInterval小於0,表示Session永不過時 if (this.maxInactiveInterval < 0) { return false; } // 最大過時時間單位轉換爲毫秒 // 當前時間減去Session的最大有效期間隔以獲取理論上有效的上一次訪問時間 // 而後在與實際的上一次訪問時間進行比較 // 若是大於,表示理論上的時間已經在實際的訪問時間以後,那麼表示Session已通過期 return now - TimeUnit.SECONDS .toMillis(this.maxInactiveInterval) >= this.lastAccessedTime; }

loadSession中,將Redis中存儲的Session信息轉換爲MapSession對象,以便從Session中獲取屬性時可以從內存直接獲取提升性能:

private MapSession loadSession(String id, Map<Object, Object> entries) { MapSession loaded = new MapSession(id); for (Map.Entry<Object, Object> entry : entries.entrySet()) { String key = (String) entry.getKey(); if (CREATION_TIME_ATTR.equals(key)) { loaded.setCreationTime((Long) entry.getValue()); } else if (MAX_INACTIVE_ATTR.equals(key)) { loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue()); } else if (LAST_ACCESSED_ATTR.equals(key)) { loaded.setLastAccessedTime((Long) entry.getValue()); } else if (key.startsWith(SESSION_ATTR_PREFIX)) { loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue()); } } return loaded; }

至此,能夠看出spring-session中request.getSession(false)的過時實現原理。

delete方法:
public void delete(String sessionId) { // 獲取Session RedisSession session = getSession(sessionId, true); if (session == null) { return; } cleanupPrincipalIndex(session); // 從過時集合中移除sessionId this.expirationPolicy.onDelete(session); String expireKey = getExpiredKey(session.getId()); // 刪除session的過時鍵 this.sessionRedisOperations.delete(expireKey); // 設置session過時 session.setMaxInactiveIntervalInSeconds(0); save(session); }

至此RedisOperationsSessionRepository的核心原理就介紹完畢。可是RedisOperationsSessionRepository中還包括關於Session事件的處理和清理Session的定時任務。這部份內容在後述的SessionEvent部分介紹。

5. HttpSessionStrategy

A strategy for mapping HTTP request and responses to a {@link Session}.

從javadoc中能夠看出,HttpSessionStrategy是創建Request/Response和Session之間的映射關係的策略。

Tips:策略模式
策略模式是一個傳神的神奇模式,是java的多態很是典型應用,是開閉原則、迪米特法則的具體體現。將同類型的一系列的算法封裝在不一樣的類中,經過使用接口注入不一樣類型的實現,以達到的高擴展的目的。通常是定義一個策略接口,按照不一樣的場景實現各自的策略。

該策略接口中定義一套策略行爲:

// 根據請求獲取SessionId,即創建請求至Session的映射關係 String getRequestedSessionId(HttpServletRequest request); // 對於新建立的Session,通知客戶端 void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response); // 對於session無效,通知客戶端 void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);

以下UML類圖:

 

 

 

這裏主要介紹CookieHttpSessionStrategy,這個也是默認的策略,能夠查看spring-session中類SpringHttpSessionConfiguration,在註冊SessionRepositoryFilter Bean時默認採用CookieHttpSessionStrategy:

@Bean public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter( SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>( sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) { sessionRepositoryFilter.setHttpSessionStrategy( (MultiHttpSessionStrategy) this.httpSessionStrategy); } else { sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy); } return sessionRepositoryFilter; }

下面來分析CookieHttpSessionStrategy的原理。該策略使用Cookie來映射Request/Response至Session。即request/requset的head中cookie存儲SessionId,當請求至web服務器,能夠解析請求head中的cookie,而後獲取sessionId,根據sessionId獲取spring-session。當建立新的session或者session過時,將相應的sessionId寫入response的set-cookie或者從respose中移除sessionId。

getRequestedSessionId方法
public String getRequestedSessionId(HttpServletRequest request) { // 獲取當前請求的sessionId:session別名和sessionId映射 Map<String, String> sessionIds = getSessionIds(request); // 獲取當前請求的Session別名 String sessionAlias = getCurrentSessionAlias(request); // 獲取相應別名的sessionId return sessionIds.get(sessionAlias); }

接下來看下具體獲取SessionIds的具體過程:

public String getRequestedSessionId(HttpServletRequest request) { // 獲取當前請求的sessionId:session別名和sessionId映射 Map<String, String> sessionIds = getSessionIds(request); // 獲取當前請求的Session別名 String sessionAlias = getCurrentSessionAlias(request); // 獲取相應別名的sessionId return sessionIds.get(sessionAlias); } public Map<String, String> getSessionIds(HttpServletRequest request) { // 解析request中的cookie值 List<String> cookieValues = this.cookieSerializer.readCookieValues(request); // 獲取sessionId String sessionCookieValue = cookieValues.isEmpty() ? "" : cookieValues.iterator().next(); Map<String, String> result = new LinkedHashMap<String, String>(); // 根據分詞器對sessionId進行分割,由於spring-session支持多session。默認狀況只有一個session StringTokenizer tokens = new StringTokenizer(sessionCookieValue, this.deserializationDelimiter); // 若是隻有一個session,則設置默認別名爲0 if (tokens.countTokens() == 1) { result.put(DEFAULT_ALIAS, tokens.nextToken()); return result; } // 若是有多個session,則創建別名和sessionId的映射 while (tokens.hasMoreTokens()) { String alias = tokens.nextToken(); if (!tokens.hasMoreTokens()) { break; } String id = tokens.nextToken(); result.put(alias, id); } return result; } public List<String> readCookieValues(HttpServletRequest request) { // 獲取request的cookie Cookie[] cookies = request.getCookies(); List<String> matchingCookieValues = new ArrayList<String>(); if (cookies != null) { for (Cookie cookie : cookies) { // 若是是以SESSION開頭,則表示是SessionId,畢竟cookie不僅有sessionId,還有可能存儲其餘內容 if (this.cookieName.equals(cookie.getName())) { // 決策是否須要base64 decode String sessionId = this.useBase64Encoding ? base64Decode(cookie.getValue()) : cookie.getValue(); if (sessionId == null) { continue; } if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) { sessionId = sessionId.substring(0, sessionId.length() - this.jvmRoute.length()); } // 存入list中 matchingCookieValues.add(sessionId); } } } return matchingCookieValues; }

再來看下獲取當前request對應的Session的別名方法getCurrentSessionAlias

public String getCurrentSessionAlias(HttpServletRequest request) { // 若是session參數爲空,則返回默認session別名 if (this.sessionParam == null) { return DEFAULT_ALIAS; } // 從request中獲取session別名,若是爲空則返回默認別名 String u = request.getParameter(this.sessionParam); if (u == null) { return DEFAULT_ALIAS; } if (!ALIAS_PATTERN.matcher(u).matches()) { return DEFAULT_ALIAS; } return u; }

spring-session爲了支持多session,才弄出多個session別名。當時通常應用場景都是一個session,都是默認的session別名0。

上述獲取sessionId和別名映射關係中,也是默認別名0。這裏返回別名0,因此返回當前請求對應的sessionId。

onNewSession方法
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) { // 從當前request中獲取已經寫入Cookie的sessionId集合 Set<String> sessionIdsWritten = getSessionIdsWritten(request); // 判斷是否包含,若是包含,表示該sessionId已經寫入過cookie中,則直接返回 if (sessionIdsWritten.contains(session.getId())) { return; } // 若是沒有寫入,則加入集合,後續再寫入 sessionIdsWritten.add(session.getId()); Map<String, String> sessionIds = getSessionIds(request); String sessionAlias = getCurrentSessionAlias(request); sessionIds.put(sessionAlias, session.getId()); // 獲取cookieValue String cookieValue = createSessionCookieValue(sessionIds); //將cookieValue寫入Cookie中 this.cookieSerializer .writeCookieValue(new CookieValue(request, response, cookieValue)); }

sessionIdsWritten主要是用來記錄已經寫入Cookie的SessionId,防止SessionId重複寫入Cookie中。

onInvalidateSession方法
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) { // 從當前request中獲取sessionId和別名映射 Map<String, String> sessionIds = getSessionIds(request); // 獲取別名 String requestedAlias = getCurrentSessionAlias(request); // 移除sessionId sessionIds.remove(requestedAlias); String cookieValue = createSessionCookieValue(sessionIds); // 寫入移除後的sessionId this.cookieSerializer .writeCookieValue(new CookieValue(request, response, cookieValue)); }

繼續看下具體的寫入writeCookieValue原理:

public void writeCookieValue(CookieValue cookieValue) { // 獲取request/respose和cookie值 HttpServletRequest request = cookieValue.getRequest(); HttpServletResponse response = cookieValue.getResponse(); String requestedCookieValue = cookieValue.getCookieValue(); String actualCookieValue = this.jvmRoute == null ? requestedCookieValue : requestedCookieValue + this.jvmRoute; // 構造servlet規範中的Cookie對象,注意這裏cookieName爲:SESSION,表示爲Session, // 上述的從Cookie中讀取SessionId,也是使用該cookieName Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding ? base64Encode(actualCookieValue) : actualCookieValue); // 設置cookie的屬性:secure、path、domain、httpOnly sessionCookie.setSecure(isSecureCookie(request)); sessionCookie.setPath(getCookiePath(request)); String domainName = getDomainName(request); if (domainName != null) { sessionCookie.setDomain(domainName); } if (this.useHttpOnlyCookie) { sessionCookie.setHttpOnly(true); } // 若是cookie值爲空,則失效 if ("".equals(requestedCookieValue)) { sessionCookie.setMaxAge(0); } else { sessionCookie.setMaxAge(this.cookieMaxAge); } // 寫入cookie到response中 response.addCookie(sessionCookie); }

至此,CookieHttpSessionStrategy介紹結束。

因爲篇幅過長,關於spring-session event和RedisOperationSessionRepository清理session而且產生過時事件的部分後續文章介紹。

總結

spring-session提供集羣環境下HttpSession的透明集成。spring-session的優點在於開箱即用,具備較強的設計模式。且支持多種持久化方式,其中RedisSession較爲成熟,與spring-data-redis整合,可謂威力無窮。

http://www.javashuo.com/article/p-uhnxejue-dn.html

 

spring-session(一)揭祕續篇

上一篇文章中介紹了Spring-Session的核心原理,Filter,Session,Repository等等,傳送門:spring-session(一)揭祕

這篇繼上一篇的原理逐漸深刻Spring-Session中的事件機制原理的探索。衆所周知,Servlet規範中有對HttpSession的事件的處理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,能夠查看Package javax.servlet

在Spring-Session中也有相應的Session事件機制實現,包括Session建立/過時/刪除事件。

本文主要從如下方面探索Spring-Session中事件機制

  • Session事件的抽象
  • 事件的觸發機制

Note:
這裏的事件觸發機制只介紹基於RedissSession的實現。基於內存Map實現的MapSession不支持Session事件機制。其餘的Session實現這裏也不作關注。

一.Session事件的抽象

先來看下Session事件抽象UML類圖,總體掌握事件之間的依賴關係。

 

 

Session Event最頂層是ApplicationEvent,即Spring上下文事件對象。由此能夠看出Spring-Session的事件機制是基於Spring上下文事件實現。

抽象的AbstractSessionEvent事件對象提供了獲取Session(這裏的是指Spring Session的對象)和SessionId。

基於事件的類型,分類爲:

  1. Session建立事件
  2. Session刪除事件
  3. Session過時事件

Tips:
Session銷燬事件只是刪除和過時事件的統一,並沒有實際含義。

事件對象只是對事件自己的抽象,描述事件的屬性,如:

  1. 獲取事件產生的源:getSource獲取事件產生源
  2. 獲取相應事件特性:getSession/getSessoinId獲取時間關聯的Session

下面再深刻探索以上的Session事件是如何觸發,從事件源到事件監聽器的鏈路分析事件流轉過程。

二.事件的觸發機制

閱讀本節前,讀者應該瞭解Redis的Pub/Sub和KeySpace Notification,若是還不是很瞭解,傳送門Redis Keyspace NotificationsPub/Sub

上節中也介紹Session Event事件基於Spring的ApplicationEvent實現。先簡單認識spring上下文事件機制:

 

 

  • ApplicationEventPublisher實現用於發佈Spring上下文事件ApplicationEvent
  • ApplicationListener實現用於監聽Spring上下文事件ApplicationEvent
  • ApplicationEvent抽象上下文事件

那麼在Spring-Session中必然包含事件發佈者ApplicationEventPublisher發佈Session事件和ApplicationListener監聽Session事件。

能夠看出ApplicationEventPublisher發佈一個事件:

@FunctionalInterface public interface ApplicationEventPublisher { /** * Notify all <strong>matching</strong> listeners registered with this * application of an application event. Events may be framework events * (such as RequestHandledEvent) or application-specific events. * @param event the event to publish * @see org.springframework.web.context.support.RequestHandledEvent */ default void publishEvent(ApplicationEvent event) { publishEvent((Object) event); } /** * Notify all <strong>matching</strong> listeners registered with this * application of an event. * <p>If the specified {@code event} is not an {@link ApplicationEvent}, * it is wrapped in a {@link PayloadApplicationEvent}. * @param event the event to publish * @since 4.2 * @see PayloadApplicationEvent */ void publishEvent(Object event); }

ApplicationListener用於監聽相應的事件:

@FunctionalInterface public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /** * Handle an application event. * @param event the event to respond to */ void onApplicationEvent(E event); }

Tips:
這裏使用到了發佈/訂閱模式,事件監聽器能夠監聽感興趣的事件,發佈者能夠發佈各類事件。不過這是內部的發佈訂閱,即觀察者模式。

Session事件的流程實現以下:

 

 

上圖展現了Spring-Session事件流程圖,事件源來自於Redis鍵空間通知,在spring-data-redis項目中抽象MessageListener監聽Redis事件源,而後將其傳播至spring應用上下文發佈者,由發佈者發佈事件。在spring上下文中的監聽器Listener便可監聽到Session事件。

由於二者是Spring框架提供的對Spring的ApplicationEvent的支持。Session Event基於ApplicationEvent實現,必然也有其相應發佈者和監聽器的的實現。

Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。全部關於RedisSession的管理操做都是由其實現,因此Session的產生源是RedisOperationSessionRepository。

在RedisOperationSessionRepository中持有ApplicationEventPublisher對象用於發佈Session事件。

private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { @Override public void publishEvent(ApplicationEvent event) { } @Override public void publishEvent(Object event) { } };

可是該ApplicationEventPublisher是空實現,實際實現是在應用啓動時由Spring-Session自動配置。在spring-session-data-redis模塊中RedisHttpSessionConfiguration中有關於建立RedisOperationSessionRepository Bean時將調用set方法將ApplicationEventPublisher配置。

@Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer { private ApplicationEventPublisher applicationEventPublisher; @Bean public RedisOperationsSessionRepository sessionRepository() { RedisTemplate<Object, Object> redisTemplate = createRedisTemplate(); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository( redisTemplate); // 注入依賴 sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); if (this.defaultRedisSerializer != null) { sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); } sessionRepository .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); if (StringUtils.hasText(this.redisNamespace)) { sessionRepository.setRedisKeyNamespace(this.redisNamespace); } sessionRepository.setRedisFlushMode(this.redisFlushMode); return sessionRepository; } // 注入上下文中的ApplicationEventPublisher Bean @Autowired public void setApplicationEventPublisher( ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } }

在進行自動配置時,將上下文中的ApplicationEventPublisher的注入,實際上即ApplicationContext對象。

Note:
考慮篇幅緣由,以上的RedisHttpSessionConfiguration至展現片斷。

對於ApplicationListener是由應用開發者自行實現,註冊成Bean便可。當有Session Event發佈時,便可監聽。

/** * session事件監聽器 * * @author huaijin */ @Component public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> { private static final String CURRENT_USER = "currentUser"; @Override public void onApplicationEvent(SessionDeletedEvent event) { Session session = event.getSession(); UserVo userVo = session.getAttribute(CURRENT_USER); System.out.println("Current session's user:" + userVo.toString()); } }

以上部分探索了Session事件的發佈者和監聽者,可是核心事件的觸發發佈則是由Redis的鍵空間通知機制觸發,當有Session建立/刪除/過時時,Redis鍵空間會通知Spring-Session應用。

RedisOperationsSessionRepository實現spring-data-redis中的MessageListener接口。

/** * Listener of messages published in Redis. * * @author Costin Leau * @author Christoph Strobl */ public interface MessageListener { /** * Callback for processing received objects through Redis. * * @param message message must not be {@literal null}. * @param pattern pattern matching the channel (if specified) - can be {@literal null}. */ void onMessage(Message message, @Nullable byte[] pattern); }

該監聽器即用來監聽redis發佈的消息。RedisOperationsSessionRepositorys實現了該Redis鍵空間消息通知監聽器接口,實現以下:

public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener { @Override @SuppressWarnings("unchecked") public void onMessage(Message message, byte[] pattern) { // 獲取該消息發佈的redis通道channel byte[] messageChannel = message.getChannel(); // 獲取消息體內容 byte[] messageBody = message.getBody(); String channel = new String(messageChannel); // 若是是由Session建立通道發佈的消息,則是Session建立事件 if (channel.startsWith(getSessionCreatedChannelPrefix())) { // 從消息體中載入Session Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer .deserialize(message.getBody()); // 發佈建立事件 handleCreated(loaded, channel); return; } // 若是消息體不是以過時鍵前綴,直接返回。由於spring-session在redis中的key命名規則: // "${namespace}:sessions:expires:${sessionId}",如: // session.example:sessions:expires:a5236a19-7325-4783-b1f0-db9d4442db9a // 因此判斷過時或者刪除的鍵是否爲spring-session的過時鍵。若是不是,多是應用中其餘的鍵的操做,因此直接return String body = new String(messageBody); if (!body.startsWith(getExpiredKeyPrefix())) { return; } // 根據channel判斷鍵空間的事件類型del或者expire時間 boolean isDeleted = channel.endsWith(":del"); if (isDeleted || channel.endsWith(":expired")) { int beginIndex = body.lastIndexOf(":") + 1; int endIndex = body.length(); // Redis鍵空間消息通知內容即操做的鍵,spring-session鍵中命名規則: // "${namespace}:sessions:expires:${sessionId}",如下是根據規則解析sessionId String sessionId = body.substring(beginIndex, endIndex); // 根據sessionId加載session RedisSession session = getSession(sessionId, true); if (session == null) { logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId); return; } if (logger.isDebugEnabled()) { logger.debug("Publishing SessionDestroyedEvent for session " + sessionId); } cleanupPrincipalIndex(session); // 發佈Session delete事件 if (isDeleted) { handleDeleted(session); } else { // 不然發佈Session expire事件 handleExpired(session); } } } }

下續再深刻每種事件產生的前世此生。

1.Session建立事件的觸發

 

 

  1. 由RedisOperationSessionRepository向Redis指定通道${namespace}:event:created:${sessionId}發佈一個message
  2. MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道${namespace}:event:created:${sessionId}的消息
  3. 將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher發佈SessionCreateEvent
  5. ApplicationListener監聽SessionCreateEvent,執行相應邏輯

RedisOperationSessionRepository中保存一個Session時,判斷Session是否新建立。
若是新建立,則向

@Override public void save(RedisSession session) { session.saveDelta(); // 判斷是否爲新建立的session if (session.isNew()) { // 獲取redis指定的channel:${namespace}:event:created:${sessionId}, // 如:session.example:event:created:82sdd-4123-o244-ps123 String sessionCreatedKey = getSessionCreatedChannel(session.getId()); // 向該通道發佈session數據 this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); // 設置session爲非新建立 session.setNew(false); } }

該save方法的調用是由HttpServletResponse提交時——即返回客戶端響應調用,上篇文章已經詳解,這裏再也不贅述。關於RedisOperationSessionRepository實現MessageListener上述已經介紹,這裏一樣再也不贅述。

Note:
這裏有點繞。我的認爲RedisOperationSessionRepository發佈建立而後再自己監聽,主要是考慮分佈式或者集羣環境中SessionCreateEvent事件的處理。

2.Session刪除事件的觸發

Tips:
刪除事件中使用到了Redis KeySpace Notification,建議先了解該技術。

 

 

  1. 由RedisOperationSessionRepository刪除Redis鍵空間中的指定Session的過時鍵,Redis鍵空間會向**__keyevent@*:del**的channel發佈刪除事件消息
  2. MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道**__keyevent@*:del**的消息
  3. 將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher發佈SessionDeleteEvent
  5. ApplicationListener監聽SessionDeleteEvent,執行相應邏輯

當調用HttpSession的invalidate方法讓Session失效時,即會調用RedisOperationSessionRepository的deleteById方法刪除Session的過時鍵。

/** * Allows creating an HttpSession from a Session instance. * * @author Rob Winch * @since 1.0 */ private final class HttpSessionWrapper extends HttpSessionAdapter<S> { HttpSessionWrapper(S session, ServletContext servletContext) { super(session, servletContext); } @Override public void invalidate() { super.invalidate(); SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true; setCurrentSession(null); clearRequestedSessionCache(); // 調用刪除方法 SessionRepositoryFilter.this.sessionRepository.deleteById(getId()); } }

上篇中介紹了包裝Spring Session爲HttpSession,這裏再也不贅述。這裏重點分析deleteById內容:

@Override public void deleteById(String sessionId) { // 若是session爲空則返回 RedisSession session = getSession(sessionId, true); if (session == null) { return; } cleanupPrincipalIndex(session); this.expirationPolicy.onDelete(session); // 獲取session的過時鍵 String expireKey = getExpiredKey(session.getId()); // 刪除過時鍵,redis鍵空間產生del事件消息,被MessageListener即 // RedisOperationSessionRepository監聽 this.sessionRedisOperations.delete(expireKey); session.setMaxInactiveInterval(Duration.ZERO); save(session); }

後續流程同SessionCreateEvent流程。

3.Session失效事件的觸發

Session的過時事件流程比較特殊,由於Redis的鍵空間通知的特殊性,Redis鍵空間通知不能保證過時鍵的通知的及時性。

 

 

  1. RedisOperationsSessionRepository中有個定時任務方法每整分運行訪問整分Session過時鍵集合中的過時sessionId,如:spring:session:expirations:1439245080000。觸發Redis鍵空間會向**__keyevent@*:expired**的channel發佈過時事件消息
  2. MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道**__keyevent@*:expired**的消息
  3. 將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher發佈SessionDeleteEvent
  5. ApplicationListener監聽SessionDeleteEvent,執行相應邏輯
@Scheduled(cron = "0 * * * * *") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }

定時任務每整分運行,執行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy實例,是RedisSession過時策略。

public void cleanExpiredSessions() { // 獲取當前時間戳 long now = System.currentTimeMillis(); // 時間滾動至整分,去掉秒和毫秒部分 long prevMin = roundDownMinute(now); if (logger.isDebugEnabled()) { logger.debug("Cleaning up sessions expiring at " + new Date(prevMin)); } // 根據整分時間獲取過時鍵集合,如:spring:session:expirations:1439245080000 String expirationKey = getExpirationKey(prevMin); // 獲取全部的全部的過時session Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 刪除過時Session鍵集合 this.redis.delete(expirationKey); // touch訪問全部已通過期的session,觸發Redis鍵空間通知消息 for (Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); touch(sessionKey); } }

將時間戳滾動至整分

static long roundDownMinute(long timeInMs) {
    Calendar date = Calendar.getInstance(); date.setTimeInMillis(timeInMs); // 清理時間錯的秒位和毫秒位 date.clear(Calendar.SECOND); date.clear(Calendar.MILLISECOND); return date.getTimeInMillis(); }

獲取過時Session的集合

String getExpirationKey(long expires) { return this.redisSession.getExpirationsKey(expires); } // 如:spring:session:expirations:1439245080000 String getExpirationsKey(long expiration) { return this.keyPrefix + "expirations:" + expiration; }

調用Redis的Exists命令,訪問過時Session鍵,觸發Redis鍵空間消息

/** * By trying to access the session we only trigger a deletion if it the TTL is * expired. This is done to handle * https://github.com/spring-projects/spring-session/issues/93 * * @param key the key */ private void touch(String key) { this.redis.hasKey(key); }

總結

至此Spring-Session的Session事件通知模塊就已經很清晰:

  1. Redis鍵空間Session事件源:Session建立通道/Session刪除通道/Session過時通道
  2. Spring-Session中的RedisOperationsSessionRepository消息監聽器監聽Redis的事件類型
  3. RedisOperationsSessionRepository負責將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher將其包裝成ApplicationEvent類型的Session Event發佈
  5. ApplicationListener監聽Session Event,處理相應邏輯

http://www.javashuo.com/article/p-aqthueky-cc.html

 

spring-session(二)與spring-boot整合實戰

沒有設置這個,好奇怪:

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 604800)

前兩篇介紹了spring-session的原理,這篇在理論的基礎上再實戰。
spring-boot整合spring-session的自動配置可謂是開箱即用,極其簡潔和方便。這篇文章即介紹spring-boot整合spring-session,這裏只介紹基於RedisSession的實戰。

原理篇是基於spring-session v1.2.2版本,考慮到RedisSession模塊與spring-session v2.0.6版本的差別很小,且可以與spring-boot v2.0.0兼容,因此實戰篇是基於spring-boot v2.0.0基礎上配置spring-session。

源碼請戮session-example

實戰

搭建spring-boot工程這裏飄過,傳送門:https://start.spring.io/

配置spring-session

引入spring-session的pom配置,因爲spring-boot包含spring-session的starter模塊,因此pom中依賴:

<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>

編寫spring boot啓動類SessionExampleApplication

/** * 啓動類 * * @author huaijin */ @SpringBootApplication public class SessionExampleApplication { public static void main(String[] args) { SpringApplication.run(SessionExampleApplication.class, args); } }

配置application.yml

spring: session: redis: flush-mode: on_save namespace: session.example cleanup-cron: 0 * * * * * store-type: redis timeout: 1800 redis: host: localhost port: 6379 jedis: pool: max-active: 100 max-wait: 10 max-idle: 10 min-idle: 10 database: 0
除了配置spring.session.timeout外,還要配置:

 

 和

 

 配置cookieMaxAge後,前端瀏覽器都會有Max-Age和Expires數據



 

 


編寫controller

編寫登陸控制器,登陸時建立session,並將當前登陸用戶存儲sesion中。登出時,使session失效。

/** * 登陸控制器 * * @author huaijin */ @RestController public class LoginController { private static final String CURRENT_USER = "currentUser"; /** * 登陸 * * @param loginVo 登陸信息 * * @author huaijin */ @PostMapping("/login.do") public String login(@RequestBody LoginVo loginVo, HttpServletRequest request) { UserVo userVo = UserVo.builder().userName(loginVo.getUserName()) .userPassword(loginVo.getUserPassword()).build(); HttpSession session = request.getSession(); session.setAttribute(CURRENT_USER, userVo); System.out.println("create session, sessionId is:" + session.getId()); return "ok"; } /** * 登出 * * @author huaijin */ @PostMapping("/logout.do") public String logout(HttpServletRequest request) { HttpSession session = request.getSession(false); session.invalidate(); return "ok"; } }

編寫查詢控制器,在登陸建立session後,使用將sessionId置於cookie中訪問。若是沒有session將返回錯誤。

/** * 查詢 * * @author huaijin */ @RestController @RequestMapping("/session") public class QuerySessionController { @GetMapping("/query.do") public String querySessionId(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return "error"; } System.out.println("current's user is:" + session.getId() + "in session"); return "ok"; } }
編寫Session刪除事件監聽器

Session刪除事件監聽器用於監聽登出時使session失效的事件源。

/** * session事件監聽器 * * @author huaijin */ @Component public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> { private static final String CURRENT_USER = "currentUser"; @Override public void onApplicationEvent(SessionDeletedEvent event) { Session session = event.getSession(); UserVo userVo = session.getAttribute(CURRENT_USER); System.out.println("invalid session's user:" + userVo.toString()); } }
驗證測試

編寫spring-boot測試類,測試controller,驗證spring-session是否生效。

/** * 測試Spring-Session: * 1.登陸時建立session * 2.使用sessionId能正常訪問 * 3.session過時銷燬,可以監聽銷燬事件 * * @author huaijin */ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class SpringSessionTest { @Autowired private MockMvc mockMvc; @Test public void testLogin() throws Exception { LoginVo loginVo = new LoginVo(); loginVo.setUserName("admin"); loginVo.setUserPassword("admin@123"); String content = JSON.toJSONString(loginVo); // mock登陸 ResultActions actions = this.mockMvc.perform(post("/login.do") .content(content).contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andExpect(content().string("ok")); String sessionId = actions.andReturn() .getResponse().getCookie("SESSION").getValue(); // 使用登陸的sessionId mock查詢 this.mockMvc.perform(get("/session/query.do") .cookie(new Cookie("SESSION", sessionId))) .andExpect(status().isOk()).andExpect(content().string("ok")); // mock登出 this.mockMvc.perform(post("/logout.do") .cookie(new Cookie("SESSION", sessionId))) .andExpect(status().isOk()).andExpect(content().string("ok")); } }

測試類執行結果:

create session, sessionId is:429cb0d3-698a-475a-b3f1-09422acf2e9c current's user is:429cb0d3-698a-475a-b3f1-09422acf2e9cin session invalid session's user:UserVo{userName='admin', userPassword='admin@123'

登陸時建立Session,存儲當前登陸用戶。而後在以登陸響應返回的SessionId查詢用戶。最後再登出使Session過時。

spring-boot整合spring-session自動配置原理

前兩篇文章介紹spring-session原理時,總結spring-session的核心模塊。這節中探索spring-boot中自動配置如何初始化spring-session的各個核心模塊。

spring-boot-autoconfigure模塊中包含了spinrg-session的自動配置。包org.springframework.boot.autoconfigure.session中包含了spring-session的全部自動配置項。

其中RedisSession的核心配置項是RedisHttpSessionConfiguration類。

@Configuration @ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class }) @ConditionalOnMissingBean(SessionRepository.class) @ConditionalOnBean(RedisConnectionFactory.class) @Conditional(ServletSessionCondition.class) @EnableConfigurationProperties(RedisSessionProperties.class) class RedisSessionConfiguration { @Configuration public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration { // 加載application.yml或者application.properties中自定義的配置項: // 命名空間:用於做爲session redis key的一部分 // flushmode:session寫入redis的模式 // 定時任務時間:即訪問redis過時鍵的定時任務的cron表達式 @Autowired public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) { Duration timeout = sessionProperties.getTimeout(); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } setRedisNamespace(redisSessionProperties.getNamespace()); setRedisFlushMode(redisSessionProperties.getFlushMode()); setCleanupCron(redisSessionProperties.getCleanupCron()); } } }

RedisSessionConfiguration配置類中嵌套SpringBootRedisHttpSessionConfiguration繼承了RedisHttpSessionConfiguration配置類。首先看下該配置類持有的成員。

@Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer { // 默認的cron表達式,application.yml能夠自定義配置 static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; // session的有效最大時間間隔, application.yml能夠自定義配置 private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; // session在redis中的命名空間,主要爲了區分session,application.yml能夠自定義配置 private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE; // session寫入Redis的模式,application.yml能夠自定義配置 private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE; // 訪問過時Session集合的定時任務的定時時間,默認是每整分運行任務 private String cleanupCron = DEFAULT_CLEANUP_CRON; private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); // spring-data-redis的redis鏈接工廠 private RedisConnectionFactory redisConnectionFactory; // spring-data-redis的RedisSerializer,用於序列化session中存儲的attributes private RedisSerializer<Object> defaultRedisSerializer; // session時間發佈者,默認注入的是AppliationContext實例 private ApplicationEventPublisher applicationEventPublisher; // 訪問過時session鍵的定時任務的調度器 private Executor redisTaskExecutor; private Executor redisSubscriptionExecutor; private ClassLoader classLoader; private StringValueResolver embeddedValueResolver; }

該配置類中初始化了RedisSession的最爲核心模塊之一RedisOperationsSessionRepository。

@Bean public RedisOperationsSessionRepository sessionRepository() { // 建立RedisOperationsSessionRepository RedisTemplate<Object, Object> redisTemplate = createRedisTemplate(); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository( redisTemplate); // 設置Session Event發佈者。若是對此迷惑,傳送門:https://www.cnblogs.com/lxyit/p/9719542.html sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); if (this.defaultRedisSerializer != null) { sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); } // 設置默認的Session最大有效期間隔 sessionRepository .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); // 設置命名空間 if (StringUtils.hasText(this.redisNamespace)) { sessionRepository.setRedisKeyNamespace(this.redisNamespace); } // 設置寫redis的模式 sessionRepository.setRedisFlushMode(this.redisFlushMode); return sessionRepository; }

同時也初始化了Session事件監聽器MessageListener模塊

@Bean public RedisMessageListenerContainer redisMessageListenerContainer() { // 建立MessageListener容器,這屬於spring-data-redis範疇,略過 RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(this.redisConnectionFactory); if (this.redisTaskExecutor != null) { container.setTaskExecutor(this.redisTaskExecutor); } if (this.redisSubscriptionExecutor != null) { container.setSubscriptionExecutor(this.redisSubscriptionExecutor); } // 模式訂閱redis的__keyevent@*:expired和__keyevent@*:del通道, // 獲取redis的鍵過時和刪除事件通知 container.addMessageListener(sessionRepository(), Arrays.asList(new PatternTopic("__keyevent@*:del"), new PatternTopic("__keyevent@*:expired"))); // 模式訂閱redis的${namespace}:event:created:*通道,當該向該通道發佈消息, // 則MessageListener消費消息並處理 container.addMessageListener(sessionRepository(), Collections.singletonList(new PatternTopic( sessionRepository().getSessionCreatedChannelPrefix() + "*"))); return container; }

上篇文章中介紹到的spring-session event事件原理,spring-session在啓動時監聽Redis的channel,使用Redis的鍵空間通知處理Session的刪除和過時事件和使用Pub/Sub模式處理Session建立事件。

關於RedisSession的存儲管理部分已經初始化,可是spring-session的另外一個基礎設施模塊SessionRepositoryFilter是在RedisHttpSessionConfiguration父類SpringHttpSessionConfiguration中初始化。

@Bean public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter( SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>( sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver); return sessionRepositoryFilter; }

spring-boot整合spring-session配置的層次:

RedisSessionConfiguration |_ _ SpringBootRedisHttpSessionConfiguration |_ _ RedisHttpSessionConfiguration |_ _ SpringHttpSessionConfiguration

回顧思考spring-boot自動配置spring-session,很是合理。

  • SpringHttpSessionConfiguration是spring-session自己的配置類,與spring-boot無關,畢竟spring-session也能夠整合單純的spring項目,只須要使用該spring-session的配置類便可。
  • RedisHttpSessionConfiguration用於配置spring-session的Redission,畢竟spring-session還支持其餘的各類session:Map/JDBC/MogonDB等,將其從SpringHttpSessionConfiguration隔離開來,遵循開閉原則和接口隔離原則。可是其必須依賴基礎的SpringHttpSessionConfiguration,因此使用了繼承。RedisHttpSessionConfiguration是spring-session和spring-data-redis整合配置,須要依賴spring-data-redis。
  • SpringBootRedisHttpSessionConfiguration纔是spring-boot中關鍵配置
  • RedisSessionConfiguration主要用於處理自定義配置,將application.yml或者application.properties的配置載入。

Tips:
配置類也有至關強的設計模式。遵循開閉原則:對修改關閉,對擴展開放。遵循接口隔離原則:變化的就要單獨分離,使用不一樣的接口隔離。SpringHttpSessionConfiguration和RedisHttpSessionConfiguration的設計深深體現這兩大原則。

參考

Spring Session

http://www.javashuo.com/article/p-zxxoxqfw-cp.html

相關文章
相關標籤/搜索