一、背景
二、spring-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
接口以及兩個默認實現,即CookieHttpSessionStrategy
和HeaderHttpSessionStrategy
,其中前者使用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.HttpServletRequest
的getSession()
方法時,都會返回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
注解就可以了,如下面的代碼片段所示:
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

@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");}}
?
?
?
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,至少在事件發布上是完整了(根據它實現了事件猜的)