spring-session之一:簡介、使用及實現原理

一、背景

http session(企業)一直都是我們做集群時需要解決的一個難題,我們知道HttpSession是通過Servlet容器創建和管理的,像Tomcat/Jetty都是保存在內存中的。而如果我們把web服務器搭建成分布式的集群,然后利用LVS或Nginx做負載均衡,那么來自同一用戶的Http請求將有可能被分發到兩個不同的web站點中去。那么問題就來了,如何保證不同的web站點能夠共享同一份session數據呢?
最簡單的想法就是把session數據保存到內存以外的一個統一的地方,例如Memcached/Redis等數據庫中。那么問題又來了,如何替換掉Servlet容器創建和管理HttpSession的實現呢?
(1)設計一個Filter,利用HttpServletRequestWrapper,實現自己的 getSession()方法,接管創建和管理Session數據的工作。spring-session就是通過這樣的思路實現的。
(2)利用Servlet容器提供的插件功能,自定義HttpSession的創建和管理策略,并通過配置的方式替換掉默認的策略。不過這種方式有個缺點,就是需要耦合Tomcat/Jetty等Servlet容器的代碼。這方面其實早就有開源項目了,例如memcached-session-manager,以及tomcat-redis-session-manager。暫時都只支持Tomcat6/Tomcat7。
(3)或者通過nginx之類的負載均衡做ip_hash,路由到特定的服務器上。 此策略會出現單點故障問題。

二、spring-session簡介?

Spring Session是Spring的項目之一,GitHub地址:https://github.com/spring-projects/spring-session。Spring Session把servlet容器實現的httpSession替換為spring-session,專注于解決session管理問題。Spring Session提供了集群Session(Clustered Sessions)功能,默認采用外置的Redis來存儲Session數據,以此來解決Session共享的問題。

spring-session提供對用戶session管理的一系列api和實現。提供了很多可擴展、透明的封裝方式用于管理httpSession/WebSocket的處理。

三、支持功能?

1)輕易把session存儲到第三方存儲容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多種存儲session的容器的方式。這樣可以獨立于應用服務器的方式提供高質量的集群。
2)同一個瀏覽器同一個網站,支持多個session問題。?從而能夠很容易地構建更加豐富的終端用戶體驗。
3)Restful API,不依賴于cookie。可通過header來傳遞jessionID 。控制session id如何在客戶端和服務器之間進行交換,這樣的話就能很容易地編寫Restful API,因為它可以從HTTP 頭信息中獲取session id,而不必再依賴于cookie。
4)WebSocket和spring-session結合,同步生命周期管理。當用戶使用WebSocket發送請求的時候,能夠保持HttpSession處于活躍狀態。
5)在非Web請求的處理代碼中,能夠訪問session數據,比如在JMS消息的處理代碼中。

  需要說明的很重要的一點就是,Spring Session的核心項目并不依賴于Spring框架,所以,我們甚至能夠將其應用于不使用Spring框架的項目中。Spring Session提供了一種獨立于應用服務器的方案,這種方案能夠在Servlet規范之內配置可插拔的session數據存儲,不依賴于任何應用服務器的特定API。這就意味著Spring Session能夠用于實現了servlet規范的所有應用服務器之中(Tomcat、Jetty、 WebSphere、WebLogic、JBoss等),它能夠非常便利地在所有應用服務器中以完全相同的方式進行配置。我們還可以選擇任意最適應需求的外部session數據存儲。這使得Spring Session成為一個很理想的遷移工具,幫助我們將傳統的JavaEE應用轉移到云中,使其成為滿足如下:

3.1、每個用戶有多個賬號

假設我們在example.com上運行面向公眾的Web應用,在這個應用中有些用戶會創建多個賬號。例如,用戶Jeff Lebowski可能會有兩個賬戶thedude@example.com和lebowski@example.com。和其他Java Web應用一樣,我們會使用HttpSession來跟蹤應用的狀態,如當前登錄的用戶。所以,當用戶希望從thedude@example.com切換到lebowski@example.com時,他必須要首先退出,然后再重新登錄回來。

借助Spring Session,為每個用戶配置多個HTTP session會非常容易,這樣用戶在thedude@example.com和lebowski@example.com之間切換的時候,就不需要退出和重新登錄了。

3.2、多級別的安全預覽

假設我們正在構建的Web應用有一個復雜、自定義的權限功能,其中應用的UI會基于用戶所授予的角色和權限實現自適應。

例如,假設應用有四個安全級別:public、confidential、secret和top secret。當用戶登錄應用之后,系統會判斷用戶所具有的最高安全級別并且只會顯示該級別和該級別之下的數據。所以,具有public權限的用戶只能看到public級別的文檔,具有secret權限的用戶能夠看到public、confidential和secret級別的文檔,諸如此類。為了保證用戶界面更加友好,應用程序應該允許用戶預覽在較低的安全級別條件下頁面是什么樣子的。例如,top secret權限的用戶能夠將應用從top secret模式切換到secret模式,這樣就能站在具有secret權限用戶的視角上,查看應用是什么樣子的。

典型的Web應用會將當前用戶的標識及其角色保存在HTTP session中,但因為在Web應用中,每個登錄的用戶只能有一個session,因此除了用戶退出并重新登錄進來,我們并沒有辦法在角色之間進行切換,除非我們為每個用戶自行實現多個session的功能。

借助Spring Session,可以很容易地為每個登錄用戶創建多個session,這些session之間是完全獨立的,因此實現上述的預覽功能是非常容易的。例如,當前用戶以top secret角色進行了登錄,那么應用可以創建一個新的session,這個session的最高安全角色是secret而不是top secret,這樣的話,用戶就可以在secret模式預覽應用了。

3.3、當使用Web Socket的時候保持登錄狀態

假設用戶登錄了example.com上的Web應用,那么他們可以使用HTML5的chat客戶端實現聊天的功能,這個客戶端構建在websocket之上。按照servlet規范,通過websocket傳入的請求并不能保持HTTP session處于活躍狀態,所以當用戶在聊天的過程中,HTTP session的倒數計時器會在不斷地流逝。即便站在用戶的立場上,他們一直在使用應用程序,HTTP session最終也可能會出現過期。當HTTP session過期時,websocket連接將會關閉。

借助Spring Session,對于系統中的用戶,我們能夠很容易地實現websocket請求和常規的HTTP請求都能保持HTTP session處于活躍狀態。

3.4、非Web請求訪問Session數據

假設我們的應用提供了兩種訪問方式:一種使用基于HTTP的REST API,而另一種使用基于RabbitMQ的AMQP消息。執行消息處理代碼的線程將無法訪問應用服務器的HttpSession,所以我們必須要以一種自定義的方案來獲取HTTP session中的數據,這要通過自定義的機制來實現。

通過使用Spring Session,只要我們能夠知道session的id,就可以在應用的任意線程中訪問Spring Session。因此,Spring Session具備比Servlet HTTP session管理器更為豐富的API,只要知道了session id,我們就能獲取任意特定的session。例如,在一個傳入的消息中可能會包含用戶id的header信息,借助它,我們就可以直接獲取session了。

四、Spring Session是如何運行的

我們已經討論了在傳統的應用服務器中,HTTP session管理存在不足的各種場景,接下來看一下Spring Session是如何解決這些問題的。

4.1、Spring Session的架構

當實現session管理器的時候,有兩個必須要解決的核心問題。首先,如何創建集群環境下高可用的session,要求能夠可靠并高效地存儲數據。其次,不管請求是HTTP、WebSocket、AMQP還是其他的協議,對于傳入的請求該如何確定該用哪個session實例。實質上,關鍵問題在于:在發起請求的協議上,session id該如何進行傳輸?

Spring Session認為第一個問題,也就是在高可用可擴展的集群中存儲數據已經通過各種數據存儲方案得到了解決,如Redis、GemFire以及Apache Geode等等,因此,Spring Session定義了一組標準的接口,可以通過實現這些接口間接訪問底層的數據存儲。Spring Session定義了如下核心接口:Session、ExpiringSession以及SessionRepository,針對不同的數據存儲,它們需要分別實現。

  • org.springframework.session.Session接口定義了session的基本功能,如設置和移除屬性。這個接口并不關心底層技術,因此能夠比servlet HttpSession適用于更為廣泛的場景中。
  • org.springframework.session.ExpiringSession擴展了Session接口,它提供了判斷session是否過期的屬性。RedisSession是這個接口的一個樣例實現。
  • org.springframework.session.SessionRepository定義了創建、保存、刪除以及檢索session的方法。將Session實例真正保存到數據存儲的邏輯是在這個接口的實現中編碼完成的。例如,RedisOperationsSessionRepository就是這個接口的一個實現,它會在Redis中創建、存儲和刪除session。

Spring Session認為將請求與特定的session實例關聯起來的問題是與協議相關的,因為在請求/響應周期中,客戶端和服務器之間需要協商同意一種傳遞session id的方式。例如,如果請求是通過HTTP傳遞進來的,那么session可以通過HTTP cookie或HTTP Header信息與請求進行關聯。如果使用HTTPS的話,那么可以借助SSL session id實現請求與session的關聯。如果使用JMS的話,那么JMS的Header信息能夠用來存儲請求和響應之間的session id。

對于HTTP協議來說,Spring Session定義了HttpSessionStrategy接口以及兩個默認實現,即CookieHttpSessionStrategyHeaderHttpSessionStrategy,其中前者使用HTTP cookie將請求與session id關聯,而后者使用HTTP header將請求與session關聯。

?

4.2、Spring Session對HTTP的支持

Spring Session對HTTP的支持是通過標準的servlet filter來實現的,這個filter必須要配置為攔截所有的web應用請求,并且它應該是filter鏈中的第一個filter。Spring Session filter會確保隨后調用javax.servlet.http.HttpServletRequestgetSession()方法時,都會返回Spring Session的HttpSession實例,而不是應用服務器默認的HttpSession。

如果要理解它的話,最簡單的方式就是查看Spring Session實際所使用的源碼。首先,我們了解一下標準servlet擴展點的一些背景知識,在實現Spring Session的時候會使用這些知識。

4.2.1、Spring Session對filer的request,response的裝飾

在2001年,Servlet 2.3規范引入了ServletRequestWrapper。它的javadoc文檔這樣寫道,ServletRequestWrapper“提供了ServletRequest接口的便利實現,開發人員如果希望將請求適配到Servlet的話,可以編寫它的子類。這個類實現了包裝(Wrapper)或者說是裝飾(Decorator)模式。對方法的調用默認會通過包裝的請求對象來執行”。如下的代碼樣例抽取自Tomcat,展現了ServletRequestWrapper是如何實現的。

javax.servlet-api-3.1.0.jar

package javax.servlet;
public class ServletRequestWrapper implements ServletRequest {private ServletRequest request;/*** 創建ServletRequest適配器,它包裝了給定的請求對象。* @throws java.lang.IllegalArgumentException if the request is null*/public ServletRequestWrapper(ServletRequest request) {if (request == null) {throw new IllegalArgumentException("Request cannot be null");   }this.request = request;}public ServletRequest getRequest() {return this.request;}//...
}

Servlet 2.3規范還定義了HttpServletRequestWrapper,它是ServletRequestWrapper的子類,能夠快速提供HttpServletRequest的自定義實現,如下的代碼是從Tomcat抽取出來的,展現了HttpServletRequesWrapper類是如何運行的。

javax.servlet-api-3.1.0.jar

package javax.servlet.http;
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {/** * Constructs a request object wrapping the given request.* @throws java.lang.IllegalArgumentException if the request is null*/public HttpServletRequestWrapper(HttpServletRequest request) {super(request);}private HttpServletRequest _getHttpServletRequest() {return (HttpServletRequest) super.getRequest();}//...
}

所以,借助這些包裝類就能編寫代碼來擴展HttpServletRequest,重載返回HttpSession的方法,讓它返回由外部存儲所提供的實現。如下的代碼是從Spring Session項目中提取出來的,但是我將原來的注釋替換為我自己的注釋,用來在本文中解釋代碼,所以在閱讀下面的代碼片段時,請留意注釋。

spring-session-1.3.1.RELEASE.jar

package org.springframework.session.web.http;    private final class SessionRepositoryRequestWrapperextends HttpServletRequestWrapper {private Boolean requestedSessionIdValid;private boolean requestedSessionInvalidated;private final HttpServletResponse response;private final ServletContext servletContext;/*** 注意,這個構造器非常簡單,它接受稍后會用到的參數,并且委托給它所擴展的HttpServletRequestWrapper*/private SessionRepositoryRequestWrapper(HttpServletRequest request,HttpServletResponse response, ServletContext servletContext) {super(request);this.response = response;this.servletContext = servletContext;}/*** 使用HttpSessionStrategy寫sessionid到返回對象,同時調用外部存儲設備持久化session信息* sessionRepository相當于DAO,有關于session持久化的4個方法*/private void commitSession() {HttpSessionWrapper wrappedSession = getCurrentSession();if (wrappedSession == null) {if (isInvalidateClientSession()) {SessionRepositoryFilter.this.httpSessionStrategy.onInvalidateSession(this, this.response);}}else {S session = wrappedSession.getSession();SessionRepositoryFilter.this.sessionRepository.save(session);if (!isRequestedSessionIdValid()|| !session.getId().equals(getRequestedSessionId())) {SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,this, this.response);}}}/*** 在這里,Spring Session項目不再將調用委托給應用服務器,而是實現自己的邏輯,返回由外部數據存儲作為支撐的HttpSession實例。* 基本的實現是,先檢查是不是已經有session了。* 如果有的話,就將currentSession返回,* 否則的話,它會檢查當前的請求中是否有session id。* 如果有的話,將會根據這個session id,從它的SessionRepository中加載session。* 如果session repository中沒有session,或者在當前請求中,* 沒有當前session id與請求關聯的話,* 那么它會創建一個新的session,并將其持久化到session repository中。*/@Overridepublic HttpSessionWrapper getSession(boolean create) {HttpSessionWrapper currentSession = getCurrentSession();if (currentSession != null) {return currentSession;}String requestedSessionId = getRequestedSessionId();if (requestedSessionId != null&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {S session = getSession(requestedSessionId);if (session != null) {this.requestedSessionIdValid = true;currentSession = new HttpSessionWrapper(session, getServletContext());currentSession.setNew(false);setCurrentSession(currentSession);return currentSession;}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");}}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)"));}S session = SessionRepositoryFilter.this.sessionRepository.createSession();session.setLastAccessedTime(System.currentTimeMillis());currentSession = new HttpSessionWrapper(session, getServletContext());setCurrentSession(currentSession);return currentSession;}@Overridepublic ServletContext getServletContext() {if (this.servletContext != null) {return this.servletContext;}// Servlet 3.0+return super.getServletContext();}@Overridepublic HttpSessionWrapper getSession() {return getSession(true);}@Overridepublic String getRequestedSessionId() {return SessionRepositoryFilter.this.httpSessionStrategy.getRequestedSessionId(this);}/*** Allows creating an HttpSession from a Session instance.** @author Rob Winch* @since 1.0*/private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {HttpSessionWrapper(S session, ServletContext servletContext) {super(session, servletContext);}@Overridepublic void invalidate() {super.invalidate();SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;setCurrentSession(null);SessionRepositoryFilter.this.sessionRepository.delete(getId());}}}

response有對應SessionRepositoryResponseWrapper。

    /**這個就是Servlet response的重寫類了*/private final class SessionRepositoryResponseWrapperextends OnCommittedResponseWrapper {private final SessionRepositoryRequestWrapper request;SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,HttpServletResponse response) {super(response);if (request == null) {throw new IllegalArgumentException("request cannot be null");}this.request = request;}/** 這步是持久化session到存儲容器,我們可能會在一個控制層里多次調用session的操作方法如果我們每次對session的操作都持久化到存儲容器,必定會帶來性能的影響。比如redis所以我們可以在整個控制層執行完畢了,response返回信息到瀏覽器時,才持久化session**/@Overrideprotected void onResponseCommitted() {this.request.commitSession();}}

4.2.2、Spring Session中SessionRepositoryFilter的處理

Spring Session定義了SessionRepositoryFilter,它實現了Servlet Filter接口。我抽取了這個filter的關鍵部分,將其列在下面的代碼片段中,我還添加了一些注釋,用來在本文中闡述這些代碼,所以,同樣的,請閱讀下面代碼的注釋部分。

package org.springframework.session.web.http;    
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends ExpiringSession>extends OncePerRequestFilter {/**  session存儲容器接口,redis、mongoDB、genfire等數據庫都是實現該接口  **/private final SessionRepository<S> sessionRepository;private ServletContext servletContext;/** sessionID的傳遞方式接口。目前spring-session自帶兩個實現類1.cookie方式 :CookieHttpSessionStrategy2.http header 方式:HeaderHttpSessionStrategy當然,我們也可以自定義其他方式。**/private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {if (httpSessionStrategy == null) {throw new IllegalArgumentException("httpSessionStrategy cannot be null");}/** 通過前面的spring-session功能介紹,我們知道spring-session可以支持單瀏覽器多session, 就是通過MultiHttpSessionStrategyAdapter來實現的。每個瀏覽器擁有一個sessionID,但是這個sessionID擁有多個別名(根據瀏覽器的tab)。如:別名1 sessionID別名2 sessionID...而這個別名通過url來傳遞,這就是單瀏覽器多session原理了**/this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(httpSessionStrategy);}public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {if (httpSessionStrategy == null) {throw new IllegalArgumentException("httpSessionStrategy cannot be null");}this.httpSessionStrategy = httpSessionStrategy;}/** 這個方法是魔力真正發揮作用的地方。這個方法創建了* 我們上文所述的封裝請求對象SessionRepositoryRequestWrapper和一個封裝的響應對象SessionRepositoryResponseWrapper,然后調用其余的filter鏈。* 這里,關鍵在于當這個filter后面的應用代碼執行時,* 如果要獲得session的話,得到的將會是Spring Session的HttpServletSession實例,它是由后端的外部數據存儲作為支撐的。*/@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);HttpServletRequest strategyRequest = this.httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);HttpServletResponse strategyResponse = this.httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);try {filterChain.doFilter(strategyRequest, strategyResponse);}finally {wrappedRequest.commitSession();  //filter鏈處理完成后,寫session信息到response及外圍持久化設備,源碼見上面的SessionRepositoryRequestWrapper
        }}

4.2.3、Spring Session中sessionRepository是session存儲容器接口操作session信息的讀取及存儲

session存儲容器接口,redis、mongoDB、genfire等數據庫都是實現該接口

1、sessionRepository

先看SessionRepository接口的4個方法:

package org.springframework.session;public interface SessionRepository<S extends Session> {/*** 創建*/S createSession();/*** 保存*/void save(S session);/*** 讀取*/S getSession(String id);/*** 刪除*/void delete(String id);

?實現類FindByIndexNameSessionRepository.java:

package org.springframework.session;
public interface FindByIndexNameSessionRepository<S extends Session>extends SessionRepository<S> {String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName().concat(".PRINCIPAL_NAME_INDEX_NAME");Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
}

springsession項目啟動后,redis會有:

?

Redis的實現類

package org.springframework.session.data.redis;public class RedisOperationsSessionRepository implementsFindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,MessageListener {/*** RedisSession的構造函數新建一個session,往里看源碼是通過UUID生成MapSession.this(UUID.randomUUID().toString());    */public RedisSession createSession() {RedisSession redisSession = new RedisSession();if (this.defaultMaxInactiveInterval != null) {redisSession.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);}return redisSession;}/***  調用RedisTemplate.convertAndSend()保存到redis中*/public void save(RedisSession session) {session.saveDelta();if (session.isNew()) {String sessionCreatedKey = getSessionCreatedChannel(session.getId());this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);session.setNew(false);}}/***  先構造MapSession,再查找對應的session*/private RedisSession getSession(String id, boolean allowExpired) {Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();if (entries.isEmpty()) {return null;}MapSession loaded = loadSession(id, entries);if (!allowExpired && loaded.isExpired()) {return null;}RedisSession result = new RedisSession(loaded);result.originalLastAccessTime = loaded.getLastAccessedTime();return result;}/***  如果沒有找到對應的session直接返回,如果找到就刪除*/public void delete(String sessionId) {RedisSession session = getSession(sessionId, true);if (session == null) {return;}cleanupPrincipalIndex(session);this.expirationPolicy.onDelete(session);String expireKey = getExpiredKey(session.getId());this.sessionRedisOperations.delete(expireKey);session.setMaxInactiveIntervalInSeconds(0);save(session);}
}

2、Session接口:(包路徑package org.springframework.session;)

Redis的session實現類:其中MapSession中保存關聯屬性,創建完session會設置lastAccessTime。

package org.springframework.session.data.redis;final class RedisSession implements ExpiringSession {/*** Creates a new instance ensuring to mark all of the new attributes to be* persisted in the next save operation.*/RedisSession() {this(new MapSession());this.delta.put(CREATION_TIME_ATTR, getCreationTime());this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());this.isNew = true;this.flushImmediateIfNecessary();}}

4.2.4、Spring Session的ServletFilter配置?

  從4.2.1~4.2.3得到的關鍵信息是,Spring Session對HTTP的支持所依靠的是一個簡單老式的ServletFilter,借助servlet規范中標準的特性來實現Spring Session的功能。最后一個問題是如何配置這個ServletFilter了,配置Spring Session Filter很容易,在Spring Boot中,只需要在Spring Boot的配置類上使用?@EnableRedisHttpSession注解就可以了,如下面的代碼片段所示:

@EnableRedisHttpSession注解的源碼:
package org.springframework.session.data.redis.config.annotation.web.http;
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {int maxInactiveIntervalInSeconds() default 1800;String redisNamespace() default "";String redisNamespace() default "";
}

RedisHttpSessionConfiguration是SpringHttpSessionConfiguration的redis的實現類。先看SpringHttpSessionConfiguration.java的源碼,在這里定義了bean名稱為springSessionRepositoryFilter的Filter,對所有請求[/*]都處理。這一點在啟動日志也可以說明。

package org.springframework.session.config.annotation.web.http;
@Configuration public class SpringHttpSessionConfiguration implements ApplicationContextAware { 

//...
@Bean public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {

return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
@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;
}

//...
}

?

RedisHttpSessionConfiguration.java的源碼:

package org.springframework.session.data.redis.config.annotation.web.http;@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfigurationimplements EmbeddedValueResolverAware, ImportAware {private Integer maxInactiveIntervalInSeconds = 1800;private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();private String redisNamespace = "";private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;private RedisSerializer<Object> defaultRedisSerializer;private Executor redisTaskExecutor;private Executor redisSubscriptionExecutor;private StringValueResolver embeddedValueResolver;@Beanpublic RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,RedisOperationsSessionRepository messageListener) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);if (this.redisTaskExecutor != null) {container.setTaskExecutor(this.redisTaskExecutor);}if (this.redisSubscriptionExecutor != null) {container.setSubscriptionExecutor(this.redisSubscriptionExecutor);}container.addMessageListener(messageListener,Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));container.addMessageListener(messageListener, Arrays.asList(new PatternTopic(messageListener.getSessionCreatedChannelPrefix() + "*")));return container;}@Beanpublic RedisTemplate<Object, Object> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());if (this.defaultRedisSerializer != null) {template.setDefaultSerializer(this.defaultRedisSerializer);}template.setConnectionFactory(connectionFactory);return template;}@Beanpublic RedisOperationsSessionRepository sessionRepository(@Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,ApplicationEventPublisher applicationEventPublisher) {RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);sessionRepository.setApplicationEventPublisher(applicationEventPublisher);sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);if (this.defaultRedisSerializer != null) {sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);}String redisNamespace = getRedisNamespace();if (StringUtils.hasText(redisNamespace)) {sessionRepository.setRedisKeyNamespace(redisNamespace);}sessionRepository.setRedisFlushMode(this.redisFlushMode);return sessionRepository;}//...
}

?

4.2.5、sessionListener support

4.2.5.1、在RedisOperationSessionRepository實現了MessageListener接口,redis有消息通知的時候,onMessage方法被調用,然后接下來把消息封裝成各種事件
然后通過ApplicationEventPublisher和listener協作(觀察者模式),前者發送事件,后者監聽處理。
4.2.5.2、SessionEventHttpSessionListenerAdapter實現了ApplicationListener,有新事件時,onApplicationEvent方法被調用。所以只需要在這個類里面添加我們所需要的處理邏輯的listener即可。
注:假如我們啟動2個tomcat,那么當一個session過期時,那么我們的listener在這2個tomcat都會收到事件通知。會導致重復。如果需要基于session listener做一些事情,則需要注意這點。
配置自己的事件處理器,只需要實現HttpSessionListener接口,同時實現兩個方法即可。如下:
@Component
public class MyListener implements HttpSessionListener {@Overridepublic void sessionCreated(HttpSessionEvent se) {System.out.println("sessionCreated()" + se);System.out.println("online + 1");}@Overridepublic void sessionDestroyed(HttpSessionEvent se) {System.out.println("sessionDestroyed()" + se);System.out.println("online - 1");}}

?

?

---如果想配置session的過期時間,那么需要在RedisHttpSessionConfiguration里面配置,有個參數maxInactiveIntervalInSeconds,默認1800秒。因為session的管理被放到了外部的存儲,所以web.xml里面的關于session過期的配置不在生效。

?

4.2.6、MultiHttpSessionStrategyAdapter單瀏覽器多session支持

    /*** A delegating implementation of {@link MultiHttpSessionStrategy}.*/static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {private HttpSessionStrategy delegate;/*** Create a new {@link MultiHttpSessionStrategyAdapter} instance.* @param delegate the delegate HTTP session strategy*/MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {this.delegate = delegate;}public String getRequestedSessionId(HttpServletRequest request) {return this.delegate.getRequestedSessionId(request);}public void onNewSession(Session session, HttpServletRequest request,HttpServletResponse response) {this.delegate.onNewSession(session, request, response);}public void onInvalidateSession(HttpServletRequest request,HttpServletResponse response) {this.delegate.onInvalidateSession(request, response);}//...}

Spring Session會為每個用戶保留多個session,這是通過使用名為“_s”的session別名參數實現的。例如,如果到達的請求為http://example.com/doSomething?_s=0 ,那么Spring Session將會讀取“_s”參數的值,并通過它確定這個請求所使用的是默認session。

如果到達的請求是http://example.com/doSomething?_s=1的話,那么Spring Session就能知道這個請求所要使用的session別名為1.如果請求沒有指定“_s”參數的話,例如http://example.com/doSomething,那么Spring Session將其視為使用默認的session,也就是說_s=0

要為某個瀏覽器創建新的session,只需要調用javax.servlet.http.HttpServletRequest.getSession()就可以了,就像我們通常所做的那樣,Spring Session將會返回正確的session或者按照標準Servlet規范的語義創建一個新的session。下面的表格描述了針對同一個瀏覽器窗口,getSession()面對不同url時的行為。

HTTP請求URL

Session別名

getSession()的行為

example.com/resource

0

如果存在session與別名0關聯的話,就返回該session,否則的話創建一個新的session并將其與別名0關聯。

example.com/resource?_s=1

1

如果存在session與別名1關聯的話,就返回該session,否則的話創建一個新的session并將其與別名1關聯。

example.com/resource?_s=0

0

如果存在session與別名0關聯的話,就返回該session,否則的話創建一個新的session并將其與別名0關聯。

example.com/resource?_s=abc

abc

如果存在session與別名abc關聯的話,就返回該session,否則的話創建一個新的session并將其與別名abc關聯。

如上面的表格所示,session別名不一定必須是整型,它只需要區別于其他分配給用戶的session別名就可以了。但是,整型的session別名可能是最易于使用的,Spring Session提供了HttpSessionManager接口,這個接口包含了一些使用session別名的工具方法。

五、回顧

5.1、spring-session的包結構介紹

?

  • org.springframework.session包:

    定義一些接口:如:Session接口、SessionRepository接口(存儲接口)、

  • org.springframework.session.web包:

    SessionRepositoryFilter重寫Filter;
    集成Servlet,把上面的filter加入到filter chain、cookie和Http header方式存放到jsession,單瀏覽器多session支持等

  • org.springframework.session.data、org.springframework.session.jdbc、org.springframework.session.hazelcast:

主要是各類存儲容器的實現,如:redis、jvm的map、mongo、gemfire、hazelcast、jdbc等

  • org.springframework.session.event包:

    定義session生命周期相關的事件

  • org.springframework.session.http包:

    配置spring-session

?

5.2、spring-session重寫servlet request 及 redis實現存儲相關問題

spring-session無縫替換應用服務器的request大概原理是:?
1.自定義個Filter,實現doFilter方法?
2.繼承 HttpServletRequestWrapper 、HttpServletResponseWrapper 類,重寫getSession等相關方法(在這些方法里調用相關的 session存儲容器操作類)。?
3.在 第一步的doFilter中,new 第二步 自定義的request和response的類。并把它們分別傳遞 到 過濾器鏈?
4.把該filter配置到 過濾器鏈的第一個位置上

Redis存儲容器實現。?
主要實現存儲公共基礎類->FindByIndexNameSessionRepository ,里面主要有根據indexName從redis中查找session、根據sessionID對redis中的session增刪改查的方法。?
關于redis的session存儲容器,實際上spring-session是有些缺陷的。比如無法做到session的過期以及銷毀的實時發布事件,以及getCurrentSession中可能存在的一些并發問題(小問題)。但整體來說還是可用性很高的,畢竟我們自己寫一套這類框架成本很高。?
以上只是針對redis session的存儲容器,其他存儲容器可能會比redis更好,比如gemfire,至少在事件發布上是完整了(根據它實現了事件猜的)

轉載于:https://www.cnblogs.com/duanxz/p/3471448.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/376186.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/376186.shtml
英文地址,請注明出處:http://en.pswp.cn/news/376186.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

How to check bad fix

最近做了一個backport的票&#xff0c;backport就是別人以前修復了這個bug&#xff0c;我只需要將fix移植到客戶的系統中。這是一 個沒有技術含量的票&#xff0c;遇到簡單的票&#xff0c;三下五除二就解決了。但是遇到目標版本與master差別大時&#xff0c;也許backport后不好…

cad2017怎么改變選擇方式_家用胎心儀怎么使用?建議孕媽媽選擇數胎動的方式...

一般胎心儀都有說明書&#xff0c;孕媽媽可以根據說明書上的方法去做。 下面介紹比較通用的方法。時間&#xff1a;早中晚餐后的30-60分鐘內 環境&#xff1a;周圍沒有電磁或輻射等干擾 輔助&#xff1a;耦合劑 步驟&#xff1a; 1、平躺&#xff0c;尋找適合胎心位置 在聽胎心…

c#endread怎么打印出來_打印機打印出來是白板是怎么回事

引起針式打印紙空白的原因大多是由于色帶油墨干涸、色帶拉斷、打印頭損壞等&#xff0c;應及時更換色帶或維修打印頭。故障現象:針式打印機有打印聲但打印空白。維修方法:具體解決方法如下:1) 檢查打印機色帶盒是否正確安裝&#xff0c;如果安裝不正確&#xff0c;重新安裝色帶…

使用dnspod遭遇的奇特問題以及背后的原因與臨時解決方法

由于園子里有不少用戶在使用dnspod&#xff0c;我們覺得有必要將這兩天blogjava.net域名在dsnpod遇到的奇特問題分享一下&#xff0c;以免再有人踩著這個坑。 12月11日&#xff0c;我們登錄到dnspod的后臺時&#xff0c;大吃一驚&#xff0c;blogjava.net這個域名竟然消失了。 …

lgg6可以root的版本_Kali Linux 2020.1版本變更內容

kali2020.1于2020年1月28日發布&#xff0c;為2020年的第一個版本&#xff0c;由于此版本相較以前有較大變化&#xff0c;故專篇記錄一下。根據官方說明&#xff0c;主要改變如下&#xff1a;默認用戶改為非root用戶針對不同需求出了單獨的鏡像文件nethunter改為非root用戶改進…

隨機生成六位不重復數值

在《Core JAVA》中有個隨機生成六位不重復數值的算法&#xff0c;大二用過一次&#xff0c;今天在寫《Algorithms》的練習題遇到類似的問題&#xff0c;特貼出&#xff01; 1 // 隨機生成六位不重復的數字2 private static int generate6BitInt() {3 int[] arr {0, 1, 2, …

.net 代理類(WebService代理類的詳解 )

http://hi.baidu.com/654085966/item/53ee8c0f108ad78202ce1b1d -----------轉自 客戶端調用Web Service的方式我現在知道的有三種,分別為Http_Get,Http_Post和通過代理類來調用 直接通過HTTP-GET和直接通過HTTP-POST來請求訪問Web服務是非常底層的且麻煩,(詳細用法請查看C#分…

icem密度盒怎么設置_怎么做好火災自動報警系統施工安裝?

關于火災自動報警系統施工安裝GB50166-2019 《火災自動報警系統施工及驗收標準》 中有明確規定&#xff1a;3.1 一般規定3.1.1 系統部件的設置應符合設計文件和現行國家標準《火災自動報警系統設計規范》GB50116的規定。3.1.2 有爆炸危險性的場所&#xff0c;系統的布線和部件的…

Android 廣播機制以及用法詳解 (轉)

轉&#xff1a;http://blog.sina.com.cn/s/blog_5da93c8f010178zl.html 參考&#xff1a;http://blog.sina.com.cn/s/blog_80723de801014e2g.htmlhttp://blog.csdn.net/jjaze3344/article/details/7259272一、什么是廣播&#xff1f;在android里面有各種各樣的廣播&#xff0c;…

erlzmq

ERROR REPORT 24-Dec-2013::17:01:43 The on_load function for module erlzmq_nif returned {error, {load_failed, "Failed to load NIF library: ./ebin/../priv/erlzmq_drv.so: ELF file OS ABI invalid"}} 發布到不同環境的服務器時報上面的錯誤&#xff0c;解決…

python崗位 上海_上海黑馬Python24期,平均薪資10150元,16個工作日就業率70.73%

黑馬程序員上海中心月薪一萬只是起點關注網紅遍地起&#xff0c;顏值即正義&#xff0c;要說哪個網紅靠實力&#xff0c;Python當屬第一&#xff01;Python作為時下最流行的一門網紅語言&#xff0c;用一句話來證明它的實力就是&#xff1a;Python在手&#xff0c;天下我有&…

在IIS中部署Asp.net Mvc

概述&#xff1a; 最近在做一個MVC 3的項目&#xff0c;在部署服務器時破費了一番功夫&#xff0c;特將過程整理下來&#xff0c;希望可以幫到大家&#xff01; 本文主要介紹在IIS5.1、IIS6.0、IIS7.5中安裝配置MVC 3的具體辦法&#xff01; 正文&#xff1a; IIS5.1 1. 安裝Mi…

idea在分屏拖不回來_朋友圈賞花曬照新玩法,宮格分屏視頻!

? 點擊上方【有科嘮】一起漲姿勢~近期的天氣好的不要不要的&#xff0c;出去賞花是件很愜意的事情&#xff0c;繼《城墻下》推出的近期賞花攻略&#xff0c;嘮科粉們可以跟著攻略賞花一番&#xff0c;賞花的同時&#xff0c;大家肯定會發個朋友圈紀念一下&#xff0c;見過九宮…

MFC窗口實現最小化到托盤 右鍵菜單和還原

//.h文件 void toTray();//最小化到托盤 void DeleteTray();//刪除托盤圖標afx_msg LRESULT OnShowTask(WPARAM wParam,LPARAM lParam) ;//圖標恢復//.cpp文件#define WM_SHOWTASK (WM_USER 1) #define IDR_SHOW 11 #define IDR_OTHER 12 #define IDR_EXIT 13 BEGIN_MESSAGE_MA…

Owner Useful links

1、AS3天地會論壇 http://bbs.9ria.com/forum.php 2、Cocoa China中文網 http://www.cocoachina.com/ 3、IT江湖 http://www.itjhwd.com/ 4、The Swift Programming Language in github https://github.com/numbbbbb/the-swift-programming-language-in-chinese 5、Xcode 工具 …

導出配置_Lua配置表導出優化

隨著游戲的開發&#xff0c;項目的配置表數據越來越多&#xff0c;占用的內存越來越&#xff1b;配置表占用太大就會影響游戲加載速度&#xff0c;游戲流暢度的每一毫秒都是我們的必爭之路。[1] {DungeonID10000, Dungeon "王進打高俅", NextDungeonID10100, Battle…

iOS數據存取---iOS-Apple蘋果官方文檔翻譯

本系列所有文章,鏈接地址:iOS7開發-Apple蘋果iPhone開發Xcode官方文檔翻譯PDF下載地址(2013年12月29日更新版) 本文對應pdf文檔下載鏈接,猛戳—>:數據存取文檔.key.zip3.1 MB數據存取文檔.pdf1.1 MB 數據存取/*技術博客http://www.cnblogs.com/ChenYilong/ 新浪微博http://w…

企業QQ 增加在線交談鏈接

企業QQ的在線交流鏈接跟普通QQ的在線交流不一樣&#xff0c;普通QQ的在線交流&#xff0c;可以在http://shang.qq.com/v3/widget.html生成&#xff1b;企業qq的鏈接可以按以下步驟添加&#xff1a;第一步&#xff1a;引入企業QQjs腳本&#xff1a;<script charset"utf-…

為什么打不開_發票查重百科導出的發票臺賬為什么附件打不開?

1在掃描完發票之后&#xff0c;很多人都習慣性的定期將掃描的電子發票臺賬導出來&#xff0c;目前電子發票查重工具小程序版提供了兩種導出方式&#xff1a;通過郵件的方式將指定日期內的發票臺賬直接發送到指定的郵箱&#xff1b;直接將指定日期內的發票臺賬下載到手機上進行查…

無法找到腳本文件 C:/Windows/explorer.exe:574323188.vbs

今天打開電腦后電腦有點反常&#xff0c;在啟動時沒有運行 “局域網” 保護的程序&#xff0c;而且還他是 “無法找到腳本文件”如下圖&#xff1a; 發現這個東西后&#xff0c;第一反應&#xff0c;拔掉網線、因為很有可能是中病毒了&#xff0c;當時就出了一身冷汗&#xff0…