本文所說的session是單機版本的session, 事實上在當前的互聯網實踐中已經不太存在這種定義了。咱們主要討論的是其安全共享的實現,只從理論上來討論,沒必要太過在乎實用性問題。react
大概就是一個會話的的定義,客戶端有cookie記錄,服務端session定義。用於肯定你就是你的一個東西。git
每一個用戶在必定範圍內共享某個session信息,以實現登陸狀態,操做的鑑權保持等。web
咱們將會藉助tomcat的實現,剖析session管理的一些實現原理。算法
session 信息會在兩個地方調用,一是每次請求進來時,框架會嘗試去加載原有對應的session信息(不會新建)。二是應用本身調用getSession()時,此時若是不存在session信息,則建立一個新的session對象,表明應用後續會使用此功能。即框架不會自動支持session相關功能,只是在你須要的時候進行輔助操做。spring
// case1. 框架自行調用session信息,不會主動建立session // org.springframework.web.servlet.support.SessionFlashMapManager#retrieveFlashMaps /** * Retrieves saved FlashMap instances from the HTTP session, if any. */ @Override @SuppressWarnings("unchecked") protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) { HttpSession session = request.getSession(false); return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null); } // case2. 應用主動調用session信息,不存在時會建立新的session, 以知足業務連續性須要 @GetMapping("sessionTest") public Object sessionTest(HttpServletRequest request, HttpServletResponse response) { // 主動獲取session信息 HttpSession session = request.getSession(); String sid = session.getId(); System.out.println("sessionId:" + sid); return ResponseInfoBuilderUtil.success(sid); }
在tomcat中,HttpServletRequest的實際類都是 RequestFacade, 因此獲取session信息也是以其爲入口進行。數據庫
// org.apache.catalina.connector.RequestFacade#getSession() @Override public HttpSession getSession() { if (request == null) { throw new IllegalStateException( sm.getString("requestFacade.nullRequest")); } // 若是不存在session則建立一個 // session 的實現有兩種:一是基於內存的實現,二是基於文件的實現。 return getSession(true); } @Override public HttpSession getSession(boolean create) { if (request == null) { throw new IllegalStateException( sm.getString("requestFacade.nullRequest")); } if (SecurityUtil.isPackageProtectionEnabled()){ return AccessController. doPrivileged(new GetSessionPrivilegedAction(create)); } else { // RequestFacade 是個外觀模式實現,核心請求仍是會傳遞給 Request處理的 // org.apache.catalina.connector.Request return request.getSession(create); } } // org.apache.catalina.connector.Request#getSession(boolean) /** * @return the session associated with this Request, creating one * if necessary and requested. * * @param create Create a new session if one does not exist */ @Override public HttpSession getSession(boolean create) { // 由 create 字段決定是否須要建立新的session, 若是不存在的話。 // Session 是tomcat的一個會話實現類,並不是對接規範接口類,其會包裝一個HttpSession,以便統一交互 // 由於只有 HttpSession 纔是 Servlet 的接口規範,在tomcat中會以 StandardSessionFacade 實現接口,其也是一個外觀模式的實現,具體工做由 StandardSession 處理。 Session session = doGetSession(create); if (session == null) { return null; } // 包裝 Session 爲 HttpSession 規範返回 return session.getSession(); } // org.apache.catalina.connector.Request#doGetSession protected Session doGetSession(boolean create) { // There cannot be a session if no context has been assigned yet // mappingData.context; Context context = getContext(); if (context == null) { return (null); } // Return the current session if it exists and is valid // 此處檢查session有效性時,也會作部分清理工做 if ((session != null) && !session.isValid()) { session = null; } if (session != null) { return (session); } // Return the requested session if it exists and is valid // 獲取manager 實例,即真正進行 Session 管理的類,其實主要分兩種:1. 基於內存;2. 基於文件的持久化; Manager manager = context.getManager(); if (manager == null) { return (null); // Sessions are not supported } if (requestedSessionId != null) { try { // 若是不是第一次請求,則會帶上服務返回的 sessionId, 就會主動查找原來的session // 從 sessions 中查找便可 session = manager.findSession(requestedSessionId); } catch (IOException e) { session = null; } if ((session != null) && !session.isValid()) { session = null; } // 後續請求,每次請求都會更新有效時間 if (session != null) { session.access(); return (session); } } // Create a new session if requested and the response is not committed // 主動請求session時,纔會繼續後續邏輯 if (!create) { return (null); } if (response != null && context.getServletContext() .getEffectiveSessionTrackingModes() .contains(SessionTrackingMode.COOKIE) && response.getResponse().isCommitted()) { throw new IllegalStateException( sm.getString("coyoteRequest.sessionCreateCommitted")); } // Re-use session IDs provided by the client in very limited // circumstances. String sessionId = getRequestedSessionId(); if (requestedSessionSSL) { // If the session ID has been obtained from the SSL handshake then // use it. } else if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie())) { /* This is the common(ish) use case: using the same session ID with * multiple web applications on the same host. Typically this is * used by Portlet implementations. It only works if sessions are * tracked via cookies. The cookie must have a path of "/" else it * won't be provided for requests to all web applications. * * Any session ID provided by the client should be for a session * that already exists somewhere on the host. Check if the context * is configured for this to be confirmed. */ if (context.getValidateClientProvidedNewSessionId()) { boolean found = false; for (Container container : getHost().findChildren()) { Manager m = ((Context) container).getManager(); if (m != null) { try { if (m.findSession(sessionId) != null) { found = true; break; } } catch (IOException e) { // Ignore. Problems with this manager will be // handled elsewhere. } } } if (!found) { sessionId = null; } } } else { // 當session無效時,須要將原來的seesionId置空,刪除並新建立一個使用 sessionId = null; } // 建立session, StandardManager -> ManagerBase session = manager.createSession(sessionId); // Creating a new session cookie based on that session if (session != null && context.getServletContext() .getEffectiveSessionTrackingModes() .contains(SessionTrackingMode.COOKIE)) { // 建立cookie信息,與session對應 Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie( context, session.getIdInternal(), isSecure()); // 添加到response中,在響應結果一塊兒返回給客戶端 response.addSessionCookieInternal(cookie); } if (session == null) { return null; } // 每次請求session時,必然刷新激活時間,以便斷定會話是否超時 session.access(); return session; }
從上面咱們能夠看到,session的流程大概是這樣的:apache
1. 先查找是否有session信息存在,若是有則判斷是否失敗;
2. 若是不存在session或已失效,則使用一個新的sessionId(非必須)建立一個session實例;
3. session建立成功,則將sessionId寫入到cookie信息中,以便客戶端後續使用;
4. 每次請求完session,一定刷新下訪問時間;數組
session的管理主要有兩種實現方式,類圖以下:緩存
咱們先主要以基於內存的實現來理解下session的管理過程。實際上StandardManager基本就依託於 ManagerBase 就實現了Session管理功能,下面咱們來看一下其建立session如何?tomcat
// org.apache.catalina.session.ManagerBase#createSession @Override public Session createSession(String sessionId) { // 首先來個安全限制,容許同時存在多少會話 // 這個會話實際上表明的是一段時間的有效性,並不是真正的用戶有效使用在線,因此該值通常要求比預計的數量大些纔好 if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) { rejectedSessions++; throw new TooManyActiveSessionsException( sm.getString("managerBase.createSession.ise"), maxActiveSessions); } // Recycle or create a Session instance // 建立空的session 容器 return new StandardSession(this); Session session = createEmptySession(); // Initialize the properties of the new session and return it // 默認30分鐘有效期 session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60); String id = sessionId; if (id == null) { // sessionId 爲空時,生成一個,隨機id id = generateSessionId(); } // 設置sessionId, 注意此處不只僅是set這麼簡單,其同時會將自身session註冊到全局session管理器中.以下文 session.setId(id); sessionCounter++; SessionTiming timing = new SessionTiming(session.getCreationTime(), 0); synchronized (sessionCreationTiming) { // LinkedList, 添加一個,刪除一個? sessionCreationTiming.add(timing); sessionCreationTiming.poll(); } return (session); } // org.apache.catalina.session.StandardSession#setId /** * Set the session identifier for this session. * * @param id The new session identifier */ @Override public void setId(String id) { setId(id, true); } @Override public void setId(String id, boolean notify) { // 若是原來的id不爲空,則先刪除原有的 if ((this.id != null) && (manager != null)) manager.remove(this); this.id = id; // 再將自身會話註冊到 manager 中,即 sessions 中 if (manager != null) manager.add(this); // 通知監聽者,這是框架該作好的事(擴展點),不過不是本文的方向,忽略 if (notify) { tellNew(); } } // org.apache.catalina.session.ManagerBase#add @Override public void add(Session session) { // 取出 sessionId, 添加到 sessions 容器,統一管理 sessions.put(session.getIdInternal(), session); int size = getActiveSessions(); // 刷新最大活躍數,使用雙重鎖優化更新該值 if( size > maxActive ) { synchronized(maxActiveUpdateLock) { if( size > maxActive ) { maxActive = size; } } } } // 查找session也是異常簡單,只管從 ConcurrentHashMap 中查找便可 // org.apache.catalina.session.ManagerBase#findSession @Override public Session findSession(String id) throws IOException { if (id == null) { return null; } return sessions.get(id); }
有興趣的同窗能夠看一下sessionId的生成算法:主要保證兩點:1. 隨機性;2.不可重複性;
// org.apache.catalina.session.ManagerBase#generateSessionId /** * Generate and return a new session identifier. * @return a new session id */ protected String generateSessionId() { String result = null; do { if (result != null) { // Not thread-safe but if one of multiple increments is lost // that is not a big deal since the fact that there was any // duplicate is a much bigger issue. duplicates++; } // 使用 sessionIdGenerator 生成sessionId result = sessionIdGenerator.generateSessionId(); // 若是已經存在該sessionId, 則從新生成一個 // session 是一個 ConcurrentHashMap 結構數據 } while (sessions.containsKey(result)); return result; } // org.apache.catalina.util.SessionIdGeneratorBase#generateSessionId /** * Generate and return a new session identifier. */ @Override public String generateSessionId() { return generateSessionId(jvmRoute); } // org.apache.catalina.util.StandardSessionIdGenerator#generateSessionId @Override public String generateSessionId(String route) { byte random[] = new byte[16]; // 默認16 int sessionIdLength = getSessionIdLength(); // Render the result as a String of hexadecimal digits // Start with enough space for sessionIdLength and medium route size // 建立雙倍大小的stringBuilder, 容納sessionId StringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20); int resultLenBytes = 0; // while (resultLenBytes < sessionIdLength) { getRandomBytes(random); for (int j = 0; j < random.length && resultLenBytes < sessionIdLength; j++) { // 轉換爲16進制 byte b1 = (byte) ((random[j] & 0xf0) >> 4); byte b2 = (byte) (random[j] & 0x0f); if (b1 < 10) buffer.append((char) ('0' + b1)); else buffer.append((char) ('A' + (b1 - 10))); if (b2 < 10) buffer.append((char) ('0' + b2)); else buffer.append((char) ('A' + (b2 - 10))); resultLenBytes++; } } if (route != null && route.length() > 0) { buffer.append('.').append(route); } else { String jvmRoute = getJvmRoute(); if (jvmRoute != null && jvmRoute.length() > 0) { buffer.append('.').append(jvmRoute); } } return buffer.toString(); } // org.apache.catalina.util.SessionIdGeneratorBase#getRandomBytes protected void getRandomBytes(byte bytes[]) { // 使用 random.nextBytes(), 預生成 random SecureRandom random = randoms.poll(); if (random == null) { random = createSecureRandom(); } random.nextBytes(bytes); // 添加到 ConcurrentLinkedQueue 隊列中,事實上該 random 將會被反覆循環使用, poll->add randoms.add(random); }
建立好session後,須要進行隨時的維護:咱們看下tomcat是如何刷新訪問時間的?可能比預想的簡單,其僅是更新一個訪問時間字段,再無其餘。
// org.apache.catalina.session.StandardSession#access /** * Update the accessed time information for this session. This method * should be called by the context when a request comes in for a particular * session, even if the application does not reference it. */ @Override public void access() { // 更新訪問時間 this.thisAccessedTime = System.currentTimeMillis(); // 訪問次數統計,默認不啓用 if (ACTIVITY_CHECK) { accessCount.incrementAndGet(); } }
最後,還須要看下 HttpSession 是如何被包裝返回的?
// org.apache.catalina.session.StandardSession#getSession /** * Return the <code>HttpSession</code> for which this object * is the facade. */ @Override public HttpSession getSession() { if (facade == null){ if (SecurityUtil.isPackageProtectionEnabled()){ final StandardSession fsession = this; facade = AccessController.doPrivileged( new PrivilegedAction<StandardSessionFacade>(){ @Override public StandardSessionFacade run(){ return new StandardSessionFacade(fsession); } }); } else { // 直接使用 StandardSessionFacade 包裝便可 facade = new StandardSessionFacade(this); } } return (facade); }
再最後,要說明的是,整個sessions的管理使用一個 ConcurrentHashMap 來存放全局會話信息,sessionId->session實例。
對於同一次http請求中,該session會被存儲在當前的Request棧org.apache.catalina.connector.Request#session字段中,從而無需每次深刻獲取。每一個請求進來後,會將session保存在當前的request信息中。
會話不可能不過時,不過時的也不叫會話了。
會話過時的觸發時機主要有三個:1. 每次進行會話調用時,會主動有效性isValid()驗證,此時若是發現過時能夠主動清理: 2. 後臺定時任務觸發清理; 3. 啓動或中止應用的時候清理;(這對於非內存式的存儲會更有用些)
// case1. 請求時驗證,如前面所述 // org.apache.catalina.connector.Request#doGetSession protected Session doGetSession(boolean create) { ... // Return the current session if it exists and is valid if ((session != null) && !session.isValid()) { session = null; } if (session != null) { return (session); } ... } // case2. 後臺定時任務清理 // org.apache.catalina.session.ManagerBase#backgroundProcess @Override public void backgroundProcess() { // 並不是每次定時任務到達時都會進行清理,而是要根據其清理頻率設置來運行 // 默認是 6 count = (count + 1) % processExpiresFrequency; if (count == 0) processExpires(); } /** * Invalidate all sessions that have expired. */ public void processExpires() { long timeNow = System.currentTimeMillis(); // 找出全部的sessions, 轉化爲數組遍歷 Session sessions[] = findSessions(); int expireHere = 0 ; if(log.isDebugEnabled()) log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); for (int i = 0; i < sessions.length; i++) { // 事實上後臺任務也是調用 isValid() 方法 進行過時任務清理的 if (sessions[i]!=null && !sessions[i].isValid()) { expireHere++; } } long timeEnd = System.currentTimeMillis(); if(log.isDebugEnabled()) log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere); processingTime += ( timeEnd - timeNow ); } //case3. start/stop 時觸發過時清理(生命週期事件) // org.apache.catalina.session.StandardManager#startInternal /** * Start this component and implement the requirements * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. * * @exception LifecycleException if this component detects a fatal error * that prevents this component from being used */ @Override protected synchronized void startInternal() throws LifecycleException { super.startInternal(); // Load unloaded sessions, if any try { // doLoad() 調用 load(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("standardManager.managerLoad"), t); } setState(LifecycleState.STARTING); } /** * Load any currently active sessions that were previously unloaded * to the appropriate persistence mechanism, if any. If persistence is not * supported, this method returns without doing anything. * * @exception ClassNotFoundException if a serialized class cannot be * found during the reload * @exception IOException if an input/output error occurs */ protected void doLoad() throws ClassNotFoundException, IOException { if (log.isDebugEnabled()) { log.debug("Start: Loading persisted sessions"); } // Initialize our internal data structures sessions.clear(); // Open an input stream to the specified pathname, if any File file = file(); if (file == null) { return; } if (log.isDebugEnabled()) { log.debug(sm.getString("standardManager.loading", pathname)); } Loader loader = null; ClassLoader classLoader = null; Log logger = null; try (FileInputStream fis = new FileInputStream(file.getAbsolutePath()); BufferedInputStream bis = new BufferedInputStream(fis)) { Context c = getContext(); loader = c.getLoader(); logger = c.getLogger(); if (loader != null) { classLoader = loader.getClassLoader(); } if (classLoader == null) { classLoader = getClass().getClassLoader(); } // Load the previously unloaded active sessions synchronized (sessions) { try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger, getSessionAttributeValueClassNamePattern(), getWarnOnSessionAttributeFilterFailure())) { Integer count = (Integer) ois.readObject(); int n = count.intValue(); if (log.isDebugEnabled()) log.debug("Loading " + n + " persisted sessions"); for (int i = 0; i < n; i++) { StandardSession session = getNewSession(); session.readObjectData(ois); session.setManager(this); sessions.put(session.getIdInternal(), session); session.activate(); if (!session.isValidInternal()) { // If session is already invalid, // expire session to prevent memory leak. // 主動調用 expire session.setValid(true); session.expire(); } sessionCounter++; } } finally { // Delete the persistent storage file if (file.exists()) { file.delete(); } } } } catch (FileNotFoundException e) { if (log.isDebugEnabled()) { log.debug("No persisted data file found"); } return; } if (log.isDebugEnabled()) { log.debug("Finish: Loading persisted sessions"); } } // stopInternal() 事件到達時清理 sessions /** * Save any currently active sessions in the appropriate persistence * mechanism, if any. If persistence is not supported, this method * returns without doing anything. * * @exception IOException if an input/output error occurs */ protected void doUnload() throws IOException { if (log.isDebugEnabled()) log.debug(sm.getString("standardManager.unloading.debug")); if (sessions.isEmpty()) { log.debug(sm.getString("standardManager.unloading.nosessions")); return; // nothing to do } // Open an output stream to the specified pathname, if any File file = file(); if (file == null) { return; } if (log.isDebugEnabled()) { log.debug(sm.getString("standardManager.unloading", pathname)); } // Keep a note of sessions that are expired ArrayList<StandardSession> list = new ArrayList<>(); try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath()); BufferedOutputStream bos = new BufferedOutputStream(fos); ObjectOutputStream oos = new ObjectOutputStream(bos)) { synchronized (sessions) { if (log.isDebugEnabled()) { log.debug("Unloading " + sessions.size() + " sessions"); } // Write the number of active sessions, followed by the details oos.writeObject(Integer.valueOf(sessions.size())); for (Session s : sessions.values()) { StandardSession session = (StandardSession) s; list.add(session); session.passivate(); session.writeObjectData(oos); } } } // Expire all the sessions we just wrote // 將全部session失效,實際上應用即將關閉,失不失效的應該也無所謂了 if (log.isDebugEnabled()) { log.debug("Expiring " + list.size() + " persisted sessions"); } for (StandardSession session : list) { try { session.expire(false); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } finally { session.recycle(); } } if (log.isDebugEnabled()) { log.debug("Unloading complete"); } }
接下來咱們看下具體如何清理過時的會話?實際應該就是一個remove的事。
// org.apache.catalina.session.StandardSession#isValid /** * Return the <code>isValid</code> flag for this session. */ @Override public boolean isValid() { if (!this.isValid) { return false; } if (this.expiring) { return true; } if (ACTIVITY_CHECK && accessCount.get() > 0) { return true; } // 超過有效期,主動觸發清理 if (maxInactiveInterval > 0) { int timeIdle = (int) (getIdleTimeInternal() / 1000L); if (timeIdle >= maxInactiveInterval) { expire(true); } } return this.isValid; } // org.apache.catalina.session.StandardSession#expire(boolean) /** * Perform the internal processing required to invalidate this session, * without triggering an exception if the session has already expired. * * @param notify Should we notify listeners about the demise of * this session? */ public void expire(boolean notify) { // Check to see if session has already been invalidated. // Do not check expiring at this point as expire should not return until // isValid is false if (!isValid) return; // 上鎖保證線程安全 synchronized (this) { // Check again, now we are inside the sync so this code only runs once // Double check locking - isValid needs to be volatile // The check of expiring is to ensure that an infinite loop is not // entered as per bug 56339 if (expiring || !isValid) return; if (manager == null) return; // Mark this session as "being expired" expiring = true; // Notify interested application event listeners // FIXME - Assumes we call listeners in reverse order Context context = manager.getContext(); // The call to expire() may not have been triggered by the webapp. // Make sure the webapp's class loader is set when calling the // listeners if (notify) { ClassLoader oldContextClassLoader = null; try { oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null); Object listeners[] = context.getApplicationLifecycleListeners(); if (listeners != null && listeners.length > 0) { HttpSessionEvent event = new HttpSessionEvent(getSession()); for (int i = 0; i < listeners.length; i++) { int j = (listeners.length - 1) - i; if (!(listeners[j] instanceof HttpSessionListener)) continue; HttpSessionListener listener = (HttpSessionListener) listeners[j]; try { context.fireContainerEvent("beforeSessionDestroyed", listener); listener.sessionDestroyed(event); context.fireContainerEvent("afterSessionDestroyed", listener); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); try { context.fireContainerEvent( "afterSessionDestroyed", listener); } catch (Exception e) { // Ignore } manager.getContext().getLogger().error (sm.getString("standardSession.sessionEvent"), t); } } } } finally { context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader); } } if (ACTIVITY_CHECK) { accessCount.set(0); } // Remove this session from our manager's active sessions // 從ManagerBase 中刪除 manager.remove(this, true); // Notify interested session event listeners if (notify) { fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null); } // Call the logout method if (principal instanceof TomcatPrincipal) { TomcatPrincipal gp = (TomcatPrincipal) principal; try { gp.logout(); } catch (Exception e) { manager.getContext().getLogger().error( sm.getString("standardSession.logoutfail"), e); } } // We have completed expire of this session setValid(false); expiring = false; // Unbind any objects associated with this session String keys[] = keys(); ClassLoader oldContextClassLoader = null; try { oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null); for (int i = 0; i < keys.length; i++) { removeAttributeInternal(keys[i], notify); } } finally { context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader); } } } // org.apache.catalina.session.ManagerBase#remove(org.apache.catalina.Session, boolean) @Override public void remove(Session session, boolean update) { // If the session has expired - as opposed to just being removed from // the manager because it is being persisted - update the expired stats if (update) { long timeNow = System.currentTimeMillis(); int timeAlive = (int) (timeNow - session.getCreationTimeInternal())/1000; updateSessionMaxAliveTime(timeAlive); expiredSessions.incrementAndGet(); SessionTiming timing = new SessionTiming(timeNow, timeAlive); synchronized (sessionExpirationTiming) { sessionExpirationTiming.add(timing); sessionExpirationTiming.poll(); } } // 從sessions中移除session if (session.getIdInternal() != null) { sessions.remove(session.getIdInternal()); } }
清理工做的核心任務沒猜錯,仍是進行remove對應的session, 但做爲框架必然會設置不少的擴展點,爲各監聽器接入的機會。這些點的設計,直接關係到整個功能的好壞了。
實際是廢話,前面已經明顯看出,其使用一個 ConcurrentHashMap 做爲session的管理容器,而ConcurrentHashMap自己就是線程安全的,天然也就保證了線程安全了。
不過須要注意的是,上面的線程安全是指的不一樣客戶端間的數據是互不影響的。然而對於同一個客戶端的重複請求,以上實現並未處理,便可能會生成一次session,也可能生成n次session,不過實際影響不大,由於客戶端的狀態與服務端的狀態都是一致的。
默認狀況使用內存做爲session管理工具,一是方便,二是速度至關快。可是最大的缺點是,其沒法實現持久化,便可能停機後信息就丟失了(雖然上面有在停機時作了持久化操做,但仍然是不可靠的)。
因此就有了與之相對的存儲方案了:Persistent,它有一個基類 PersistentManagerBase 繼承了 ManagerBase,作了些特別的實現:
// 1. session的添加 // 複用 ManagerBase // 2. session的查找 // org.apache.catalina.session.PersistentManagerBase#findSession /** * {@inheritDoc} * <p> * This method checks the persistence store if persistence is enabled, * otherwise just uses the functionality from ManagerBase. */ @Override public Session findSession(String id) throws IOException { // 複用ManagerBase, 獲取Session實例 Session session = super.findSession(id); // OK, at this point, we're not sure if another thread is trying to // remove the session or not so the only way around this is to lock it // (or attempt to) and then try to get it by this session id again. If // the other code ran swapOut, then we should get a null back during // this run, and if not, we lock it out so we can access the session // safely. if(session != null) { synchronized(session){ session = super.findSession(session.getIdInternal()); if(session != null){ // To keep any external calling code from messing up the // concurrency. session.access(); session.endAccess(); } } } if (session != null) return session; // See if the Session is in the Store // 若是內存中找不到會話信息,從存儲中查找,這是主要的區別 session = swapIn(id); return session; } // org.apache.catalina.session.PersistentManagerBase#swapIn /** * Look for a session in the Store and, if found, restore * it in the Manager's list of active sessions if appropriate. * The session will be removed from the Store after swapping * in, but will not be added to the active session list if it * is invalid or past its expiration. * * @param id The id of the session that should be swapped in * @return restored session, or {@code null}, if none is found * @throws IOException an IO error occurred */ protected Session swapIn(String id) throws IOException { if (store == null) return null; Object swapInLock = null; /* * The purpose of this sync and these locks is to make sure that a * session is only loaded once. It doesn't matter if the lock is removed * and then another thread enters this method and tries to load the same * session. That thread will re-create a swapIn lock for that session, * quickly find that the session is already in sessions, use it and * carry on. */ // 額,總之就是有點複雜 synchronized (this) { swapInLock = sessionSwapInLocks.get(id); if (swapInLock == null) { swapInLock = new Object(); sessionSwapInLocks.put(id, swapInLock); } } Session session = null; synchronized (swapInLock) { // First check to see if another thread has loaded the session into // the manager session = sessions.get(id); if (session == null) { Session currentSwapInSession = sessionToSwapIn.get(); try { if (currentSwapInSession == null || !id.equals(currentSwapInSession.getId())) { // 從存儲中查找session session = loadSessionFromStore(id); sessionToSwapIn.set(session); if (session != null && !session.isValid()) { log.error(sm.getString("persistentManager.swapInInvalid", id)); session.expire(); removeSession(id); session = null; } // 從新加入到內存 sessions 中 if (session != null) { reactivateLoadedSession(id, session); } } } finally { sessionToSwapIn.remove(); } } } // Make sure the lock is removed synchronized (this) { sessionSwapInLocks.remove(id); } return session; } private Session loadSessionFromStore(String id) throws IOException { try { if (SecurityUtil.isPackageProtectionEnabled()){ return securedStoreLoad(id); } else { // 依賴於store的實現了,好比 file, jdbc... return store.load(id); } } catch (ClassNotFoundException e) { String msg = sm.getString( "persistentManager.deserializeError", id); log.error(msg, e); throw new IllegalStateException(msg, e); } } // store 實現樣例: fileStore // org.apache.catalina.session.FileStore#load /** * Load and return the Session associated with the specified session * identifier from this Store, without removing it. If there is no * such stored Session, return <code>null</code>. * * @param id Session identifier of the session to load * * @exception ClassNotFoundException if a deserialization error occurs * @exception IOException if an input/output error occurs */ @Override public Session load(String id) throws ClassNotFoundException, IOException { // Open an input stream to the specified pathname, if any File file = file(id); if (file == null) { return null; } if (!file.exists()) { return null; } Context context = getManager().getContext(); Log contextLog = context.getLogger(); if (contextLog.isDebugEnabled()) { contextLog.debug(sm.getString(getStoreName()+".loading", id, file.getAbsolutePath())); } ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null); try (FileInputStream fis = new FileInputStream(file.getAbsolutePath()); ObjectInputStream ois = getObjectInputStream(fis)) { StandardSession session = (StandardSession) manager.createEmptySession(); session.readObjectData(ois); session.setManager(manager); return session; } catch (FileNotFoundException e) { if (contextLog.isDebugEnabled()) { contextLog.debug("No persisted data file found"); } return null; } finally { context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL); } } private void reactivateLoadedSession(String id, Session session) { if(log.isDebugEnabled()) log.debug(sm.getString("persistentManager.swapIn", id)); session.setManager(this); // make sure the listeners know about it. ((StandardSession)session).tellNew(); // 添加回sessions add(session); ((StandardSession)session).activate(); // endAccess() to ensure timeouts happen correctly. // access() to keep access count correct or it will end up // negative session.access(); session.endAccess(); } // 3. session 的移除 @Override public void remove(Session session, boolean update) { super.remove (session, update); // 和內存的實現差異就是,還要多一個對外部存儲的管理維護 if (store != null){ removeSession(session.getIdInternal()); } }
能夠看到, PersistentManager 的實現仍是有點複雜的,主要在安全性和性能之間的平衡,它的 StandardManager 是一種基本是一種包含關係,即除了要維護內存session外,還要維護外部存儲的狀態。
而現實狀況是,既然已經須要自行維護外部狀態了,爲什麼還要去使用tomcat自帶的session管理呢?而如裏站在框架session管理的設計者的角度,這多是也是迫不得已的事。
而在咱們本身的session管理實現中,通常的思路仍是收到的,建立 -> 查找 -> 維持 -> 刪除 。 能夠基於數據庫,緩存,或者其餘,並且相信也不件難事。