【新年好】为什么要 spring-session?
前言
在開始 spring-session 揭秘之前,先做下熱腦(活動活動腦子)運動。主要從以下三個方面進行熱腦:
為什么要 spring-session
比較 traditional-session 方案和 spring-session 方案
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至SessionRepositoryRequestWrapperSessionRepositoryRequestWrapper?wrappedRequest?=?new?SessionRepositoryRequestWrapper(request,?response,?this.servletContext);//?包裝原始HttpServletResponse響應至SessionRepositoryResponseWrapperSessionRepositoryResponseWrapper?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?{//?提交sessionwrappedRequest.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?SessionRepositoryRequestWrapperextends?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中解析sessionIdString?requestedSessionId?=?getRequestedSessionId();//?請求的如果sessionId存在且當前request的attribute中的沒有session失效屬性//?則根據sessionId獲取spring?sessionif?(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?requestif?(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");}}//?判斷是否創建sessionif?(!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?sessionS?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()?{//?獲取當前sessionHttpSessionWrapper?wrappedSession?=?getCurrentSession();//?如果當前session為空,則刪除cookie中的相應的sessionIdif?(wrappedSession?==?null)?{if?(isInvalidateClientSession())?{SessionRepositoryFilter.this.httpSessionStrategy.onInvalidateSession(this,?this.response);}}else?{//?從HttpSession中獲取當前spring?sessionS?session?=?wrappedSession.getSession();//?持久化spring?session至存儲器SessionRepositoryFilter.this.sessionRepository.save(session);//?如果是新創建spring?session,sessionId到response的cookieif?(!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?SessionRepositoryResponseWrapperextends?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;}@Overrideprotected?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 的屬性:
保存 Session 中的屬性數據至 Redis 中
清空 delta 中數據,防止重復提交 Session 中的數據
更新過期時間至下一個過期時間間隔的時刻
再看下 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。
第一個 k-v 用來存儲 Session 的詳細信息,包括 Session 的過期時間間隔、最近的訪問時間、attributes 等等。這個 k 的過期時間為 Session 的最大過期時間 + 5 分鐘。如果默認的最大過期時間為 30 分鐘,則這個 k 的過期時間為 35 分鐘
第二個 k-v 用來表示 Session 在 Redis 中的過期,這個 k-v 不存儲任何有用數據,只是表示 Session 過期而設置。這個 k 在 Redis 中的過期時間即為 Session 的過期時間間隔
第三個 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()?{//?設置本地緩存為MapSessionthis(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持久化Sessionsession.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中沒有,則返回nullif?(entries.isEmpty())?{return?null;}//?根據Session信息,加載創建一個MapSession對象MapSession?loaded?=?loadSession(id,?entries);//??判斷是否允許過期獲取和Session是否過期if?(!allowExpired?&&?loaded.isExpired())?{return?null;}//?根據MapSession?new一個信息的RedisSession,此時isNew為falseRedisSession?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)?{//?獲取SessionRedisSession?session?=?getSession(sessionId,?true);if?(session?==?null)?{return;}cleanupPrincipalIndex(session);//?從過期集合中移除sessionIdthis.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);//?獲取相應別名的sessionIdreturn?sessionIds.get(sessionAlias); }接下來看下具體獲取 SessionIds 的具體過程:
public?String?getRequestedSessionId(HttpServletRequest?request)?{//?獲取當前請求的sessionId:session別名和sessionId映射Map<String,?String>?sessionIds?=?getSessionIds(request);//?獲取當前請求的Session別名String?sessionAlias?=?getCurrentSessionAlias(request);//?獲取相應別名的sessionIdreturn?sessionIds.get(sessionAlias); }public?Map<String,?String>?getSessionIds(HttpServletRequest?request)?{//?解析request中的cookie值List<String>?cookieValues?=?this.cookieSerializer.readCookieValues(request);//?獲取sessionIdString?sessionCookieValue?=?cookieValues.isEmpty()???"":?cookieValues.iterator().next();Map<String,?String>?result?=?new?LinkedHashMap<String,?String>();//?根據分詞器對sessionId進行分割,因為spring-session支持多session。默認情況只有一個sessionStringTokenizer?tokens?=?new?StringTokenizer(sessionCookieValue,?this.deserializationDelimiter);//?如果只有一個session,則設置默認別名為0if?(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的cookieCookie[]?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?decodeString?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());//?獲取cookieValueString?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);//?移除sessionIdsessionIds.remove(requestedAlias);String?cookieValue?=?createSessionCookieValue(sessionIds);//?寫入移除后的sessionIdthis.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,也是使用該cookieNameCookie?sessionCookie?=?new?Cookie(this.cookieName,?this.useBase64Encoding??base64Encode(actualCookieValue)?:?actualCookieValue);//?設置cookie的屬性:secure、path、domain、httpOnlysessionCookie.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 整合,可謂威力無窮。
作者:懷瑾握瑜
來源鏈接:
https://www.cnblogs.com/lxyit/p/9672097.html
總結
以上是生活随笔為你收集整理的【新年好】为什么要 spring-session?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Common-BeanUtils 使用
- 下一篇: Git 合并时 --no-ff 的作用