引自:spring-session(一)揭祕javascript
在開始spring-session揭祕以前,先作下熱腦(活動活動腦子)運動。主要從如下三個方面進行熱腦:php
在傳統單機web應用中,通常使用tomcat/jetty等web容器時,用戶的session都是由容器管理。瀏覽器使用cookie中記錄sessionId,容器根據sessionId判斷用戶是否存在會話session。這裏的限制是,session存儲在web容器中,被單臺服務器容器管理。html
可是網站主鍵演變,分佈式應用和集羣是趨勢(提升性能)。此時用戶的請求可能被負載分發至不一樣的服務器,此時傳統的web容器管理用戶會話session的方式即行不通。除非集羣或者分佈式web應用可以共享session,儘管tomcat等支持這樣作。可是這樣存在如下兩點問題:java
基於這些,必須提供新的可靠的集羣分佈式/集羣session的解決方案,突破traditional-session單機限制(即web容器session方式,下面簡稱traditional-session),spring-session應用而生。nginx
下圖展現了traditional-session和spring-session的區別git
傳統模式中,當request進入web容器,根據reqest獲取session時,若是web容器中存在session則返回,若是不存在,web容器則建立一個session。而後返回response時,將sessonId做爲response的head一併返回給客戶端或者瀏覽器。github
可是上節中說明了traditional-session的侷限性在於:單機session。在此限制的相反面,即將session從web容器中抽出來,造成獨立的模塊,以便分佈式應用或者集羣都能共享,即能解決。web
spring-session的核心思想在於此:將session從web容器中剝離,存儲在獨立的存儲服務器中。目前支持多種形式的session存儲器:Redis、Database、MogonDB等。session的管理責任委託給spring-session承擔。當request進入web容器,根據request獲取session時,由spring-session負責存存儲器中獲取session,若是存在則返回,若是不存在則建立並持久化至存儲器中。redis
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?
兩種方案均可以實現,可是顯然第一種更友好,且具備兼容性。spring-session正是第一種方案的實現。
實現第一種方案的關鍵點在於作到透明和兼容
讓人興奮的是,以上的需求在Servlet規範中的擴展性都是予以支持!Servlet規範中定義一系列的接口都是支持擴展,同時提供Filter支撐擴展點。建議閱讀《JavaTM Servlet Specification》。
熱腦活動結束,下面章節正式進入今天的主題:spring-session揭祕
主要從如下兩個方面來講spring-session:
spring-session在無需綁定web容器的狀況下提供對集羣session的支持。並提供對如下狀況的透明集成:
再詳細閱讀源碼以前先來看張圖,介紹下spring-session中的核心模塊以及之間的交互。
spring-session分爲如下核心模塊:
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使用,可謂威力驚人!
對於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。
/** * 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接口。
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的基本行爲:
ExipringSession:提供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中有兩個很是重要的成員屬性:
再來看下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的屬性:
再看下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的核心組件。
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。
這裏不禁好奇,爲何一個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有兩種方式:
當訪問時發現其過時,會產生過時事件,可是沒法保證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中的具體實現原理
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(); }
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); } }
// 根據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)的過時實現原理。
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部分介紹。
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。
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。
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中。
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整合,可謂威力無窮