spring-session(一)揭秘
前言
在開始spring-session揭秘之前,先做下熱腦(活動(dòng)活動(dòng)腦子)運(yùn)動(dòng)。主要從以下三個(gè)方面進(jìn)行熱腦:
一.為什么要spring-session
在傳統(tǒng)單機(jī)web應(yīng)用中,一般使用tomcat/jetty等web容器時(shí),用戶的session都是由容器管理。瀏覽器使用cookie中記錄sessionId,容器根據(jù)sessionId判斷用戶是否存在會(huì)話session。這里的限制是,session存儲(chǔ)在web容器中,被單臺(tái)服務(wù)器容器管理。
但是網(wǎng)站主鍵演變,分布式應(yīng)用和集群是趨勢(shì)(提高性能)。此時(shí)用戶的請(qǐng)求可能被負(fù)載分發(fā)至不同的服務(wù)器,此時(shí)傳統(tǒng)的web容器管理用戶會(huì)話session的方式即行不通。除非集群或者分布式web應(yīng)用能夠共享session,盡管tomcat等支持這樣做。但是這樣存在以下兩點(diǎn)問(wèn)題:
- 需要侵入web容器,提高問(wèn)題的復(fù)雜
- web容器之間共享session,集群機(jī)器之間勢(shì)必要交互耦合
基于這些,必須提供新的可靠的集群分布式/集群session的解決方案,突破traditional-session單機(jī)限制(即web容器session方式,下面簡(jiǎn)稱traditional-session),spring-session應(yīng)用而生。
二.比較traditional-session方案和spring-session方案
下圖展示了traditional-session和spring-session的區(qū)別
傳統(tǒng)模式中,當(dāng)request進(jìn)入web容器,根據(jù)reqest獲取session時(shí),如果web容器中存在session則返回,如果不存在,web容器則創(chuàng)建一個(gè)session。然后返回response時(shí),將sessonId作為response的head一并返回給客戶端或者瀏覽器。
但是上節(jié)中說(shuō)明了traditional-session的局限性在于:單機(jī)session。在此限制的相反面,即將session從web容器中抽出來(lái),形成獨(dú)立的模塊,以便分布式應(yīng)用或者集群都能共享,即能解決。
spring-session的核心思想在于此:將session從web容器中剝離,存儲(chǔ)在獨(dú)立的存儲(chǔ)服務(wù)器中。目前支持多種形式的session存儲(chǔ)器:Redis、Database、MogonDB等。session的管理責(zé)任委托給spring-session承擔(dān)。當(dāng)request進(jìn)入web容器,根據(jù)request獲取session時(shí),由spring-session負(fù)責(zé)存存儲(chǔ)器中獲取session,如果存在則返回,如果不存在則創(chuàng)建并持久化至存儲(chǔ)器中。
三.JSR340規(guī)范與spring-session的透明繼承
JSR340是Java Servlet 3.1的規(guī)范提案,其中定義了大量的api,包括:servlet、servletRequest/HttpServletRequest/HttpServletRequestWrapper、servletResponse/HttpServletResponse/HttpServletResponseWrapper、Filter、Session等,是標(biāo)準(zhǔn)的web容器需要遵循的規(guī)約,如tomcat/jetty/weblogic等等。
在日常的應(yīng)用開發(fā)中,develpers也在頻繁的使用servlet-api,比如:
以下的方式獲取請(qǐng)求的session:
HttpServletRequest request = ... HttpSession session = request.getSession(false);其中HttpServletRequest和HttpSession都是servlet規(guī)范中定義的接口,web容器實(shí)現(xiàn)的標(biāo)準(zhǔn)。那如果引入spring-session,要如何獲取session?
- 遵循servlet規(guī)范,同樣方式獲取session,對(duì)應(yīng)用代碼無(wú)侵入且對(duì)于developers透明化
- 全新實(shí)現(xiàn)一套session規(guī)范,定義一套新的api和session管理機(jī)制
兩種方案都可以實(shí)現(xiàn),但是顯然第一種更友好,且具有兼容性。spring-session正是第一種方案的實(shí)現(xiàn)。
實(shí)現(xiàn)第一種方案的關(guān)鍵點(diǎn)在于做到透明和兼容
- 接口適配:仍然使用HttpServletRequest獲取session,獲取到的session仍然是HttpSession類型——適配器模式
- 類型包裝增強(qiáng):Session不能存儲(chǔ)在web容器內(nèi),要外化存儲(chǔ)——裝飾模式
讓人興奮的是,以上的需求在Servlet規(guī)范中的擴(kuò)展性都是予以支持!Servlet規(guī)范中定義一系列的接口都是支持?jǐn)U展,同時(shí)提供Filter支撐擴(kuò)展點(diǎn)。建議閱讀《JavaTM Servlet Specification》。
熱腦活動(dòng)結(jié)束,下面章節(jié)正式進(jìn)入今天的主題:spring-session揭秘
Spring Session探索
主要從以下兩個(gè)方面來(lái)說(shuō)spring-session:
- 特點(diǎn)
- 工作原理
一.特點(diǎn)
spring-session在無(wú)需綁定web容器的情況下提供對(duì)集群session的支持。并提供對(duì)以下情況的透明集成:
- HttpSession:容許替換web容器的HttpSession
- WebSocket:使用WebSocket通信時(shí),提供Session的活躍
- WebSession:容許以應(yīng)用中立的方式替換webflux的webSession
二.工作原理
再詳細(xì)閱讀源碼之前先來(lái)看張圖,介紹下spring-session中的核心模塊以及之間的交互。
spring-session分為以下核心模塊:
- SessionRepositoryFilter:Servlet規(guī)范中Filter的實(shí)現(xiàn),用來(lái)切換HttpSession至Spring Session,包裝HttpServletRequest和HttpServletResponse
- HttpServerletRequest/HttpServletResponse/HttpSessionWrapper包裝器:包裝原有的HttpServletRequest、HttpServletResponse和Spring Session,實(shí)現(xiàn)切換Session和透明繼承HttpSession的關(guān)鍵之所在
- Session:Spring Session模塊
- SessionRepository:管理Spring Session的模塊
- HttpSessionStrategy:映射HttpRequst和HttpResponse到Session的策略
1. SessionRepositoryFilter
SessionRepositoryFilter是一個(gè)Filter過(guò)濾器,符合Servlet的規(guī)范定義,用來(lái)修改包裝請(qǐng)求和響應(yīng)。這里負(fù)責(zé)包裝切換HttpSession至Spring Session的請(qǐng)求和響應(yīng)。
@Override protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// 設(shè)置SessionRepository至Request的屬性中request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);// 包裝原始HttpServletRequest至SessionRepositoryRequestWrapperSessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);// 包裝原始HttpServletResponse響應(yīng)至SessionRepositoryResponseWrapperSessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);// 設(shè)置當(dāng)前請(qǐng)求的HttpSessionStrategy策略HttpServletRequest strategyRequest = this.httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);// 設(shè)置當(dāng)前響應(yīng)的HttpSessionStrategy策略HttpServletResponse strategyResponse = this.httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);try {filterChain.doFilter(strategyRequest, strategyResponse);}finally {// 提交sessionwrappedRequest.commitSession();} }以上是SessionRepositoryFilter的核心操作,每個(gè)HttpRequest進(jìn)入,都會(huì)被該Filter包裝成切換Session的請(qǐng)求很響應(yīng)對(duì)象。
Tips:責(zé)任鏈模式
Filter是Servlet規(guī)范中的非常重要的組件,在tomcat的實(shí)現(xiàn)中使用了責(zé)任鏈模式,將多個(gè)Filter組織成鏈?zhǔn)秸{(diào)用。Filter的作用就是在業(yè)務(wù)邏輯執(zhí)行前后對(duì)請(qǐng)求和響應(yīng)做修改配置。配合HttpServletRequestWrapper和HttpServletResponseWrapper使用,可謂威力驚人!
2. SessionRepositoryRequestWrapper
對(duì)于developers獲取HttpSession的api
HttpServletRequest request = ...; HttpSession session = request.getSession(true);在spring session中request的實(shí)際類型SessionRepositoryRequestWrapper。調(diào)用SessionRepositoryRequestWrapper的getSession方法會(huì)觸發(fā)創(chuàng)建spring session,而非web容器的HttpSession。
SessionRepositoryRequestWrapper用來(lái)包裝原始的HttpServletRequest實(shí)現(xiàn)HttpSession切換至Spring Session。是透明Spring Session透明集成HttpSession的關(guān)鍵。
private final class SessionRepositoryRequestWrapperextends HttpServletRequestWrapper {private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class.getName();// 當(dāng)前請(qǐng)求sessionId有效private Boolean requestedSessionIdValid;// 當(dāng)前請(qǐng)求sessionId無(wú)效private boolean requestedSessionInvalidated;private final HttpServletResponse response;private final ServletContext servletContext;private SessionRepositoryRequestWrapper(HttpServletRequest request,HttpServletResponse response, ServletContext servletContext) {// 調(diào)用HttpServletRequestWrapper構(gòu)造方法,實(shí)現(xiàn)包裝super(request);this.response = response;this.servletContext = servletContext;} }SessionRepositoryRequestWrapper繼承Servlet規(guī)范中定義的包裝器HttpServletRequestWrapper。HttpServletRequestWrapper是Servlet規(guī)范api提供的用于擴(kuò)展HttpServletRequest的擴(kuò)張點(diǎn)——即裝飾器模式,可以通過(guò)重寫一些api達(dá)到功能點(diǎn)的增強(qiáng)和自定義。
Tips:裝飾器模式
裝飾器模式(包裝模式)是對(duì)功能增強(qiáng)的一種絕佳模式。實(shí)際利用的是面向?qū)ο蟮亩鄳B(tài)性實(shí)現(xiàn)擴(kuò)展。Servlet規(guī)范中開放此HttpServletRequestWrapper接口,是讓developers自行擴(kuò)展實(shí)現(xiàn)。這種使用方式和jdk中的FilterInputStream/FilterInputStream如出一轍。
HttpServletRequestWrapper中持有一個(gè)HttpServletRequest對(duì)象,然后實(shí)現(xiàn)HttpServletRequest接口的所有方法,所有方法實(shí)現(xiàn)中都是調(diào)用持有的HttpServletRequest對(duì)象的相應(yīng)的方法。繼承HttpServletRequestWrapper 可以對(duì)其重寫。SessionRepositoryRequestWrapper繼承HttpServletRequestWrapper,在構(gòu)造方法中將原有的HttpServletRequest通過(guò)調(diào)用super完成對(duì)HttpServletRequestWrapper中持有的HttpServletRequest初始化賦值,然后重寫和session相關(guān)的方法。這樣就保證SessionRepositoryRequestWrapper的其他方法調(diào)用都是使用原有的HttpServletRequest的數(shù)據(jù),只有session相關(guān)的是重寫的邏輯。
Tips:
這里的設(shè)計(jì)是否很精妙!一切都多虧與Servlet規(guī)范設(shè)計(jì)的的巧妙啊!
重寫HttpServletRequest的getSession()方法,調(diào)用有參數(shù)getSession(arg)方法,默認(rèn)為true,表示當(dāng)前reques沒有session時(shí)創(chuàng)建session。繼續(xù)看下有參數(shù)getSession(arg)的重寫邏輯.
@Override public HttpSessionWrapper getSession(boolean create) {// 從當(dāng)前請(qǐng)求的attribute中獲取session,如果有直接返回HttpSessionWrapper currentSession = getCurrentSession();if (currentSession != null) {return currentSession;}// 獲取當(dāng)前request的sessionId,這里使用了HttpSessionStrategy// 決定怎樣將Request映射至Session,默認(rèn)使用Cookie策略,即從cookies中解析sessionIdString requestedSessionId = getRequestedSessionId();// 請(qǐng)求的如果sessionId存在且當(dāng)前request的attribute中的沒有session失效屬性// 則根據(jù)sessionId獲取spring sessionif (requestedSessionId != null&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {S session = getSession(requestedSessionId);// 如果spring session不為空,則將spring session包裝成HttpSession并// 設(shè)置到當(dāng)前Request的attribute中,防止同一個(gè)request getsession時(shí)頻繁的到存儲(chǔ)器//中獲取session,提高性能if (session != null) {this.requestedSessionIdValid = true;currentSession = new HttpSessionWrapper(session, getServletContext());currentSession.setNew(false);setCurrentSession(currentSession);return currentSession;}// 如果根據(jù)sessionId,沒有獲取到session,則設(shè)置當(dāng)前request屬性,此sessionId無(wú)效// 同一個(gè)請(qǐng)求中獲取session,直接返回?zé)o效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");}}// 判斷是否創(chuàng)建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)"));}// 根據(jù)sessionRepository創(chuàng)建spring sessionS session = SessionRepositoryFilter.this.sessionRepository.createSession();// 設(shè)置session的最新訪問(wèn)時(shí)間session.setLastAccessedTime(System.currentTimeMillis());// 包裝成HttpSession透明化集成currentSession = new HttpSessionWrapper(session, getServletContext());// 設(shè)置session至Requset的attribute中,提高同一個(gè)request訪問(wèn)session的性能setCurrentSession(currentSession);return currentSession; }再來(lái)看下spring session的持久化。上述SessionRepositoryFilter在包裝HttpServletRequest后,執(zhí)行FilterChain中使用finally保證請(qǐng)求的Session始終session會(huì)被提交,此提交操作中將sesionId設(shè)置到response的head中并將session持久化至存儲(chǔ)器中。
持久化只持久spring session,并不是將spring session包裝后的HttpSession持久化,因?yàn)镠ttpSession不過(guò)是包裝器,持久化沒有意義。
/*** Uses the HttpSessionStrategy to write the session id to the response and* persist the Session.*/ private void commitSession() {// 獲取當(dāng)前sessionHttpSessionWrapper wrappedSession = getCurrentSession();// 如果當(dāng)前session為空,則刪除cookie中的相應(yīng)的sessionIdif (wrappedSession == null) {if (isInvalidateClientSession()) {SessionRepositoryFilter.this.httpSessionStrategy.onInvalidateSession(this, this.response);}}else {// 從HttpSession中獲取當(dāng)前spring sessionS session = wrappedSession.getSession();// 持久化spring session至存儲(chǔ)器SessionRepositoryFilter.this.sessionRepository.save(session);// 如果是新創(chuàng)建spring session,sessionId到response的cookieif (!isRequestedSessionIdValid()|| !session.getId().equals(getRequestedSessionId())) {SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,this, this.response);}} }再來(lái)看下包裝的響應(yīng)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();} }上面的注釋已經(jīng)非常詳細(xì),這里不再贅述。這里只講述為什么需要包裝原始的響應(yīng)。從注釋上可以看出包裝響應(yīng)時(shí)為了:確保如果響應(yīng)被提交session能夠被保存。
這里我有點(diǎn)疑惑:在上述的SessionRepositoryFilter.doFilterInternal方法中不是已經(jīng)request.commitSession()了嗎,FilterChain執(zhí)行完或者異常后都會(huì)執(zhí)行Finally中的request.commitSession。為什么這里仍然需要包裝響應(yīng),為了確保session能夠保存,包裝器中的onResponseCommitted方法可以看出也是做了一次request.commitSession()。難道這不是多此一舉?
Tips
如果有和我相同疑問(wèn)的同學(xué),那就說(shuō)明我們的基礎(chǔ)都不扎實(shí),對(duì)Servlet仍然沒有一個(gè)清楚全面的認(rèn)識(shí)。對(duì)于此問(wèn)題,我特意在github上提了issuse:Why is the request.commitSession() method called repeatedly?。
但是在提完issue后的回家路上,我思考了下response可以有流方式的寫,會(huì)不會(huì)在response.getOutStream寫的時(shí)候已經(jīng)將響應(yīng)全部返回到客戶端,這時(shí)響應(yīng)結(jié)束。
在家中是,spring sesion作者大大已經(jīng)回復(fù)了我的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被創(chuàng)建。如果response已經(jīng)被提交,將沒有辦法追蹤session(例如:無(wú)法將cookie寫入response以跟蹤哪個(gè)session id)。
在此之前我又閱讀了JavaTM Servlet Specification,規(guī)范中這樣解釋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;結(jié)合以上兩點(diǎn),一旦response執(zhí)行flushBuffer方法,迫使Response中在Buffer中任何數(shù)據(jù)都會(huì)被返回至client端。這個(gè)方法自動(dòng)提交響應(yīng)中的status code和head。那么如果不包裝請(qǐng)求,監(jiān)聽flushBuffer事件在提交response前,將session寫入response和持久化session,將導(dǎo)致作者大大說(shuō)的無(wú)法追蹤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方法,監(jiān)聽response commit,當(dāng)發(fā)生response commit時(shí),可以在commit之前寫session至response中并持久化session。
Tips:
spring mvc中HttpMessageConverters使用到的jackson即調(diào)用了outstream.flushBuffer(),當(dāng)使用@ResponseBody時(shí)。
以上做法固然合理,但是如此重復(fù)操作兩次commit,存在兩次persist session?
這個(gè)問(wèn)題后面涉及SessionRepository時(shí)再詳述!
再看SessionRepository之前,先來(lái)看下spring session中的session接口。
3.Session接口
spring-session和tomcat中的Session的實(shí)現(xiàn)模式上有很大不同,tomcat中直接對(duì)HttpSession接口進(jìn)行實(shí)現(xiàn),而spring-session中則抽象出單獨(dú)的Session層接口,讓后再使用適配器模式將Session適配層Servlet規(guī)范中的HttpSession。spring-sesion中關(guān)于session的實(shí)現(xiàn)和適配整個(gè)UML類圖如下:
Tips:適配器模式
spring-session單獨(dú)抽象出Session層接口,可以應(yīng)對(duì)多種場(chǎng)景下不同的session的實(shí)現(xiàn),然后通過(guò)適配器模式將Session適配成HttpSession的接口,精妙至極!
Session是spring-session對(duì)session的抽象,主要是為了鑒定用戶,為Http請(qǐng)求和響應(yīng)提供上下文過(guò)程,該Session可以被HttpSession、WebSocket Session,非WebSession等使用。定義了Session的基本行為:
- getId:獲取sessionId
- setAttribute:設(shè)置session屬性
- getAttribte:獲取session屬性
ExipringSession:提供Session額外的過(guò)期特性。定義了以下關(guān)于過(guò)期的行為:
- setLastAccessedTime:設(shè)置最近Session會(huì)話過(guò)程中最近的訪問(wèn)時(shí)間
- getLastAccessedTime:獲取最近的訪問(wèn)時(shí)間
- setMaxInactiveIntervalInSeconds:設(shè)置Session的最大閑置時(shí)間
- getMaxInactiveIntervalInSeconds:獲取最大閑置時(shí)間
- isExpired:判斷Session是否過(guò)期
MapSession:基于java.util.Map的ExpiringSession的實(shí)現(xiàn)
RedisSession:基于MapSession和Redis的ExpiringSession實(shí)現(xiàn),提供Session的持久化能力
先來(lái)看下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用于存儲(chǔ)Session設(shè)置屬性,比如調(diào)用的setAttribute方法的k-v就存儲(chǔ)在該HashMap中。這個(gè)和tomcat內(nèi)部實(shí)現(xiàn)HttpSession的方式類似,tomcat中使用了ConcurrentHashMap存儲(chǔ)。
其中l(wèi)astAccessedTime用于記錄最近的一次訪問(wèn)時(shí)間,maxInactiveInterval用于記錄Session的最大閑置時(shí)間(過(guò)期時(shí)間-針對(duì)沒有Request活躍的情況下的最大時(shí)間,即相對(duì)于最近一次訪問(wèn)后的最大閑置時(shí)間)。
public void setAttribute(String attributeName, Object attributeValue) {if (attributeValue == null) {removeAttribute(attributeName);}else {this.sessionAttrs.put(attributeName, attributeValue);} }setAttribute方法極其簡(jiǎn)單,null時(shí)就移除attributeName,否則put存儲(chǔ)。
重點(diǎn)熟悉RedisSession如何實(shí)現(xiàn)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,對(duì)于閱讀源碼,學(xué)會(huì)看javadocs非常重要!
基于MapSession的基本映射實(shí)現(xiàn)的Session,能夠追蹤發(fā)生變化的所有屬性,當(dāng)調(diào)用saveDelta方法后,變化的屬性將被持久化!
在RedisSession中有兩個(gè)非常重要的成員屬性:
- cached:實(shí)際上是一個(gè)MapSession實(shí)例,用于做本地緩存,每次在getAttribute時(shí)無(wú)需從Redis中獲取,主要為了improve性能
- delta:用于跟蹤變化數(shù)據(jù),做持久化
再來(lái)看下RedisSession中最為重要的行為saveDelta——持久化Session至Redis中:
/*** Saves any attributes that have been changed and updates the expiration of this* session.*/ private void saveDelta() {// 如果delta為空,則Session中沒有任何數(shù)據(jù)需要存儲(chǔ)if (this.delta.isEmpty()) {return;}String sessionId = getId();// 使用spring data redis將delta中的數(shù)據(jù)保存至Redis中g(shù)etSessionBoundHashOperations(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,代表沒有任何需要持久化的數(shù)據(jù)。同時(shí)保證//SessionRepositoryFilter和SessionRepositoryResponseWrapper的onResponseCommitted//只會(huì)持久化一次Session至Redis中,解決前面提到的疑問(wèn)this.delta = new HashMap<String, Object>(this.delta.size()); // 更新過(guò)期時(shí)間,滾動(dòng)至下一個(gè)過(guò)期時(shí)間間隔的時(shí)刻Long originalExpiration = this.originalLastAccessTime == null ? null: this.originalLastAccessTime + TimeUnit.SECONDS.toMillis(getMaxInactiveIntervalInSeconds());RedisOperationsSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this); }從javadoc中可以看出,saveDelta用于存儲(chǔ)Session的屬性:
再看下RedisSession中的其他行為
// 設(shè)置session的存活時(shí)間,即最大過(guò)期時(shí)間。先保存至本地緩存,然后再保存至delta public void setMaxInactiveIntervalInSeconds(int interval) {this.cached.setMaxInactiveIntervalInSeconds(interval);this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());flushImmediateIfNecessary(); }// 直接從本地緩存獲取過(guò)期時(shí)間 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的接口實(shí)例。抽象出:
S createSession(); void save(S session); S getSession(String id); void delete(String id);創(chuàng)建、保存、獲取、刪除Session的接口行為。根據(jù)Session的不同,分為很多種Session操作倉(cāng)庫(kù)。
這里重點(diǎn)介紹下RedisOperationsSessionRepository。在詳細(xì)介紹其之前,了解下RedisOperationsSessionRepository的數(shù)據(jù)存儲(chǔ)細(xì)節(jié)。
當(dāng)創(chuàng)建一個(gè)RedisSession,然后存儲(chǔ)在Redis中時(shí),RedisSession的存儲(chǔ)細(xì)節(jié)如下:
spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000
Redis會(huì)為每個(gè)RedisSession存儲(chǔ)三個(gè)k-v。
這里不由好奇,為什么一個(gè)RedisSession卻如此復(fù)雜的存儲(chǔ)。關(guān)于這個(gè)可以參考spring-session作者本人在github上的兩篇回答:
Why does Spring Session use spring:session:expirations?
Clarify Redis expirations and cleanup task
簡(jiǎn)單描述下,為什么RedisSession的存儲(chǔ)用到了三個(gè)Key,而非一個(gè)Redis過(guò)期Key。
對(duì)于Session的實(shí)現(xiàn),需要支持HttpSessionEvent,即Session創(chuàng)建、過(guò)期、銷毀等事件。當(dāng)應(yīng)用用監(jiān)聽器設(shè)置監(jiān)聽相應(yīng)事件,Session發(fā)生上述行為時(shí),監(jiān)聽器能夠做出相應(yīng)的處理。
Redis的強(qiáng)大之處在于支持KeySpace Notifiction——鍵空間通知。即可以監(jiān)視某個(gè)key的變化,如刪除、更新、過(guò)期。當(dāng)key發(fā)生上述行為是,以便可以接受到變化的通知做出相應(yīng)的處理。具體詳情可以參考:
Redis Keyspace Notifications
但是Redis中帶有過(guò)期的key有兩種方式:
- 當(dāng)訪問(wèn)時(shí)發(fā)現(xiàn)其過(guò)期
- Redis后臺(tái)逐步查找過(guò)期鍵
當(dāng)訪問(wèn)時(shí)發(fā)現(xiàn)其過(guò)期,會(huì)產(chǎn)生過(guò)期事件,但是無(wú)法保證key的過(guò)期時(shí)間抵達(dá)后立即生成過(guò)期事件。具體可以參考:Timing of expired events
spring-session為了能夠及時(shí)的產(chǎn)生Session的過(guò)期時(shí)的過(guò)期事件,所以增加了:
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000
spring-session中有個(gè)定時(shí)任務(wù),每個(gè)整分鐘都會(huì)查詢相應(yīng)的spring:session:expirations:整分鐘的時(shí)間戳中的過(guò)期SessionId,然后再訪問(wèn)一次這個(gè)SessionId,即spring:session:sessions:expires:SessionId,以便能夠讓Redis及時(shí)的產(chǎn)生key過(guò)期事件——即Session過(guò)期事件。
接下來(lái)再看下RedisOperationsSessionRepository中的具體實(shí)現(xiàn)原理
createSession方法:
public RedisSession createSession() {// new一個(gè)RedisSession實(shí)例RedisSession redisSession = new RedisSession();// 如果設(shè)置的最大過(guò)期時(shí)間不為空,則設(shè)置RedisSession的過(guò)期時(shí)間if (this.defaultMaxInactiveInterval != null) {redisSession.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);}return redisSession; }再來(lái)看下RedisSession的構(gòu)造方法:
/*** Creates a new instance ensuring to mark all of the new attributes to be* persisted in the next save operation.*/ RedisSession() {// 設(shè)置本地緩存為MapSessionthis(new MapSession());// 設(shè)置Session的基本屬性this.delta.put(CREATION_TIME_ATTR, getCreationTime());this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());// 標(biāo)記Session的是否為新創(chuàng)建this.isNew = true;// 持久化flushImmediateIfNecessary(); }save方法:
public void save(RedisSession session) {// 調(diào)用RedisSession的saveDelta持久化Sessionsession.saveDelta();// 如果Session為新創(chuàng)建,則發(fā)布一個(gè)Session創(chuàng)建的事件if (session.isNew()) {String sessionCreatedKey = getSessionCreatedChannel(session.getId());this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);session.setNew(false);} }getSession方法:
// 根據(jù)SessionId獲取Session,這里的false代表的參數(shù) // 指:如果Session已經(jīng)過(guò)期,是否仍然獲取返回 public RedisSession getSession(String id) {return getSession(id, false); }在有些情況下,Session過(guò)期,仍然需要能夠獲取到Session。這里先來(lái)看下getSession(String id, boolean allowExpired):
private RedisSession getSession(String id, boolean allowExpired) {// 根據(jù)SessionId,從Redis獲取到持久化的Session信息Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();// 如果Redis中沒有,則返回nullif (entries.isEmpty()) {return null;}// 根據(jù)Session信息,加載創(chuàng)建一個(gè)MapSession對(duì)象MapSession loaded = loadSession(id, entries);// 判斷是否允許過(guò)期獲取和Session是否過(guò)期if (!allowExpired && loaded.isExpired()) {return null;}// 根據(jù)MapSession new一個(gè)信息的RedisSession,此時(shí)isNew為falseRedisSession result = new RedisSession(loaded);// 設(shè)置最新的訪問(wèn)時(shí)間result.originalLastAccessTime = loaded.getLastAccessedTime();return result; }這里需要注意的是loaded.isExpired()和loadSession。loaded.isExpired判斷Session是否過(guò)期,如果過(guò)期返回null:
public boolean isExpired() {// 根據(jù)當(dāng)前時(shí)間判斷是否過(guò)期return isExpired(System.currentTimeMillis()); } boolean isExpired(long now) {// 如果maxInactiveInterval小于0,表示Session永不過(guò)期if (this.maxInactiveInterval < 0) {return false;}// 最大過(guò)期時(shí)間單位轉(zhuǎn)換為毫秒// 當(dāng)前時(shí)間減去Session的最大有效期間隔以獲取理論上有效的上一次訪問(wèn)時(shí)間// 然后在與實(shí)際的上一次訪問(wèn)時(shí)間進(jìn)行比較// 如果大于,表示理論上的時(shí)間已經(jīng)在實(shí)際的訪問(wèn)時(shí)間之后,那么表示Session已經(jīng)過(guò)期return now - TimeUnit.SECONDS.toMillis(this.maxInactiveInterval) >= this.lastAccessedTime; }loadSession中,將Redis中存儲(chǔ)的Session信息轉(zhuǎn)換為MapSession對(duì)象,以便從Session中獲取屬性時(shí)能夠從內(nèi)存直接獲取提高性能:
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)的過(guò)期實(shí)現(xiàn)原理。
delete方法:
public void delete(String sessionId) {// 獲取SessionRedisSession session = getSession(sessionId, true);if (session == null) {return;}cleanupPrincipalIndex(session);// 從過(guò)期集合中移除sessionIdthis.expirationPolicy.onDelete(session);String expireKey = getExpiredKey(session.getId());// 刪除session的過(guò)期鍵this.sessionRedisOperations.delete(expireKey);// 設(shè)置session過(guò)期session.setMaxInactiveIntervalInSeconds(0);save(session); }至此RedisOperationsSessionRepository的核心原理就介紹完畢。但是RedisOperationsSessionRepository中還包括關(guān)于Session事件的處理和清理Session的定時(shí)任務(wù)。這部分內(nèi)容在后述的SessionEvent部分介紹。
5. HttpSessionStrategy
A strategy for mapping HTTP request and responses to a {@link Session}.
從javadoc中可以看出,HttpSessionStrategy是建立Request/Response和Session之間的映射關(guān)系的策略。
Tips:策略模式
策略模式是一個(gè)傳神的神奇模式,是java的多態(tài)非常典型應(yīng)用,是開閉原則、迪米特法則的具體體現(xiàn)。將同類型的一系列的算法封裝在不同的類中,通過(guò)使用接口注入不同類型的實(shí)現(xiàn),以達(dá)到的高擴(kuò)展的目的。一般是定義一個(gè)策略接口,按照不同的場(chǎng)景實(shí)現(xiàn)各自的策略。
該策略接口中定義一套策略行為:
// 根據(jù)請(qǐng)求獲取SessionId,即建立請(qǐng)求至Session的映射關(guān)系 String getRequestedSessionId(HttpServletRequest request); // 對(duì)于新創(chuàng)建的Session,通知客戶端 void onNewSession(Session session, HttpServletRequest request,HttpServletResponse response); // 對(duì)于session無(wú)效,通知客戶端 void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);如下UML類圖:
?
這里主要介紹CookieHttpSessionStrategy,這個(gè)也是默認(rèn)的策略,可以查看spring-session中類SpringHttpSessionConfiguration,在注冊(cè)SessionRepositoryFilter Bean時(shí)默認(rèn)采用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; }下面來(lái)分析CookieHttpSessionStrategy的原理。該策略使用Cookie來(lái)映射Request/Response至Session。即request/requset的head中cookie存儲(chǔ)SessionId,當(dāng)請(qǐng)求至web服務(wù)器,可以解析請(qǐng)求head中的cookie,然后獲取sessionId,根據(jù)sessionId獲取spring-session。當(dāng)創(chuàng)建新的session或者session過(guò)期,將相應(yīng)的sessionId寫入response的set-cookie或者從respose中移除sessionId。
getRequestedSessionId方法
public String getRequestedSessionId(HttpServletRequest request) {// 獲取當(dāng)前請(qǐng)求的sessionId:session別名和sessionId映射Map<String, String> sessionIds = getSessionIds(request);// 獲取當(dāng)前請(qǐng)求的Session別名String sessionAlias = getCurrentSessionAlias(request);// 獲取相應(yīng)別名的sessionIdreturn sessionIds.get(sessionAlias); }接下來(lái)看下具體獲取SessionIds的具體過(guò)程:
public String getRequestedSessionId(HttpServletRequest request) {// 獲取當(dāng)前請(qǐng)求的sessionId:session別名和sessionId映射Map<String, String> sessionIds = getSessionIds(request);// 獲取當(dāng)前請(qǐng)求的Session別名String sessionAlias = getCurrentSessionAlias(request);// 獲取相應(yīng)別名的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>();// 根據(jù)分詞器對(duì)sessionId進(jìn)行分割,因?yàn)閟pring-session支持多session。默認(rèn)情況只有一個(gè)sessionStringTokenizer tokens = new StringTokenizer(sessionCookieValue, this.deserializationDelimiter);// 如果只有一個(gè)session,則設(shè)置默認(rèn)別名為0if (tokens.countTokens() == 1) {result.put(DEFAULT_ALIAS, tokens.nextToken());return result;}// 如果有多個(gè)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,還有可能存儲(chǔ)其他內(nèi)容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; }再來(lái)看下獲取當(dāng)前request對(duì)應(yīng)的Session的別名方法getCurrentSessionAlias
public String getCurrentSessionAlias(HttpServletRequest request) {// 如果session參數(shù)為空,則返回默認(rèn)session別名if (this.sessionParam == null) {return DEFAULT_ALIAS;}// 從request中獲取session別名,如果為空則返回默認(rèn)別名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,才弄出多個(gè)session別名。當(dāng)時(shí)一般應(yīng)用場(chǎng)景都是一個(gè)session,都是默認(rèn)的session別名0。
上述獲取sessionId和別名映射關(guān)系中,也是默認(rèn)別名0。這里返回別名0,所以返回當(dāng)前請(qǐng)求對(duì)應(yīng)的sessionId。
onNewSession方法
public void onNewSession(Session session, HttpServletRequest request,HttpServletResponse response) {// 從當(dāng)前request中獲取已經(jīng)寫入Cookie的sessionId集合Set<String> sessionIdsWritten = getSessionIdsWritten(request);// 判斷是否包含,如果包含,表示該sessionId已經(jīng)寫入過(guò)cookie中,則直接返回if (sessionIdsWritten.contains(session.getId())) {return;}// 如果沒有寫入,則加入集合,后續(xù)再寫入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主要是用來(lái)記錄已經(jīng)寫入Cookie的SessionId,防止SessionId重復(fù)寫入Cookie中。
onInvalidateSession方法
public void onInvalidateSession(HttpServletRequest request,HttpServletResponse response) {// 從當(dāng)前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)); }繼續(xù)看下具體的寫入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;// 構(gòu)造servlet規(guī)范中的Cookie對(duì)象,注意這里cookieName為:SESSION,表示為Session,// 上述的從Cookie中讀取SessionId,也是使用該cookieNameCookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding? base64Encode(actualCookieValue) : actualCookieValue);// 設(shè)置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介紹結(jié)束。
由于篇幅過(guò)長(zhǎng),關(guān)于spring-session event和RedisOperationSessionRepository清理session并且產(chǎn)生過(guò)期事件的部分后續(xù)文章介紹。
總結(jié)
spring-session提供集群環(huán)境下HttpSession的透明集成。spring-session的優(yōu)勢(shì)在于開箱即用,具有較強(qiáng)的設(shè)計(jì)模式。且支持多種持久化方式,其中RedisSession較為成熟,與spring-data-redis整合,可謂威力無(wú)窮。
?
轉(zhuǎn)自懷瑾握瑜
轉(zhuǎn)載于:https://my.oschina.net/u/4133389/blog/3089584
總結(jié)
以上是生活随笔為你收集整理的spring-session(一)揭秘的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 撩人的套路
- 下一篇: 58同城溢价私有化,互联网“前浪”躲不过