<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Tomcat session的實(shí)現(xiàn):線程安全與管理

          共 39365字,需瀏覽 79分鐘

           ·

          2021-03-16 13:23

          走過路過不要錯過

          點(diǎn)擊藍(lán)字關(guān)注我們


          本文所說的session是單機(jī)版本的session, 事實(shí)上在當(dāng)前的互聯(lián)網(wǎng)實(shí)踐中已經(jīng)不太存在這種定義了。我們主要討論的是其安全共享的實(shí)現(xiàn),只從理論上來討論,不必太過在意實(shí)用性問題。

          1:session 的意義簡說

          大概就是一個(gè)會話的的定義,客戶端有cookie記錄,服務(wù)端session定義。用于確定你就是你的一個(gè)東西。

          每個(gè)用戶在一定范圍內(nèi)共享某個(gè)session信息,以實(shí)現(xiàn)登錄狀態(tài),操作的鑒權(quán)保持等。

          我們將會借助tomcat的實(shí)現(xiàn),剖析session管理的一些實(shí)現(xiàn)原理。

          2. tomcat 中 session 什么時(shí)候創(chuàng)建?

          session 信息會在兩個(gè)地方調(diào)用,一是每次請求進(jìn)來時(shí),框架會嘗試去加載原有對應(yīng)的session信息(不會新建)。二是應(yīng)用自己調(diào)用getSession()時(shí),此時(shí)如果不存在session信息,則創(chuàng)建一個(gè)新的session對象,代表應(yīng)用后續(xù)會使用此功能。即框架不會自動支持session相關(guān)功能,只是在你需要的時(shí)候進(jìn)行輔助操作。

              // case1. 框架自行調(diào)用session信息,不會主動創(chuàng)建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. 應(yīng)用主動調(diào)用session信息,不存在時(shí)會創(chuàng)建新的session, 以滿足業(yè)務(wù)連續(xù)性需要    @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的實(shí)際類都是 RequestFacade, 所以獲取session信息也是以其為入口進(jìn)行。

              // org.apache.catalina.connector.RequestFacade#getSession()    @Override    public HttpSession getSession() {
          if (request == null) { throw new IllegalStateException( sm.getString("requestFacade.nullRequest")); } // 如果不存在session則創(chuàng)建一個(gè) // session 的實(shí)現(xiàn)有兩種:一是基于內(nèi)存的實(shí)現(xiàn),二是基于文件的實(shí)現(xiàn)。 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 是個(gè)外觀模式實(shí)現(xiàn),核心請求還是會傳遞給 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 字段決定是否需要創(chuàng)建新的session, 如果不存在的話。 // Session 是tomcat的一個(gè)會話實(shí)現(xiàn)類,并非對接規(guī)范接口類,其會包裝一個(gè)HttpSession,以便統(tǒng)一交互 // 因?yàn)橹挥?HttpSession 才是 Servlet 的接口規(guī)范,在tomcat中會以 StandardSessionFacade 實(shí)現(xiàn)接口,其也是一個(gè)外觀模式的實(shí)現(xiàn),具體工作由 StandardSession 處理。 Session session = doGetSession(create); if (session == null) { return null; } // 包裝 Session 為 HttpSession 規(guī)范返回 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有效性時(shí),也會做部分清理工作 if ((session != null) && !session.isValid()) { session = null; } if (session != null) { return (session); }
          // Return the requested session if it exists and is valid // 獲取manager 實(shí)例,即真正進(jìn)行 Session 管理的類,其實(shí)主要分兩種:1. 基于內(nèi)存;2. 基于文件的持久化; Manager manager = context.getManager(); if (manager == null) { return (null); // Sessions are not supported } if (requestedSessionId != null) { try { // 如果不是第一次請求,則會帶上服務(wù)返回的 sessionId, 就會主動查找原來的session // 從 sessions 中查找即可 session = manager.findSession(requestedSessionId); } catch (IOException e) { session = null; } if ((session != null) && !session.isValid()) { session = null; } // 后續(xù)請求,每次請求都會更新有效時(shí)間 if (session != null) { session.access(); return (session); } }
          // Create a new session if requested and the response is not committed // 主動請求session時(shí),才會繼續(xù)后續(xù)邏輯 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 { // 當(dāng)session無效時(shí),需要將原來的seesionId置空,刪除并新創(chuàng)建一個(gè)使用 sessionId = null; } // 創(chuàng)建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)) { // 創(chuàng)建cookie信息,與session對應(yīng) Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie( context, session.getIdInternal(), isSecure()); // 添加到response中,在響應(yīng)結(jié)果一起返回給客戶端 response.addSessionCookieInternal(cookie); }
          if (session == null) { return null; } // 每次請求session時(shí),必然刷新激活時(shí)間,以便判定會話是否超時(shí) session.access(); return session; }

          從上面我們可以看到,session的流程大概是這樣的:

              1. 先查找是否有session信息存在,如果有則判斷是否失效;
              2. 如果不存在session或已失效,則使用一個(gè)新的sessionId(非必須)創(chuàng)建一個(gè)session實(shí)例;
              3. session創(chuàng)建成功,則將sessionId寫入到cookie信息中,以便客戶端后續(xù)使用;
              4. 每次請求完session,必定刷新下訪問時(shí)間以續(xù)期;

          session的管理主要有兩種實(shí)現(xiàn)方式,類圖如下:

          我們先主要以基于內(nèi)存的實(shí)現(xiàn)來理解下session的管理過程。實(shí)際上StandardManager基本就依托于 ManagerBase 就實(shí)現(xiàn)了Session管理功能,下面我們來看一下其創(chuàng)建session如何?

              // org.apache.catalina.session.ManagerBase#createSession    @Override    public Session createSession(String sessionId) {        // 首先來個(gè)安全限制,允許同時(shí)存在多少會話        // 這個(gè)會話實(shí)際上代表的是一段時(shí)間的有效性,并非真正的用戶有效使用在線,所以該值一般要求比預(yù)計(jì)的數(shù)量大些才好        if ((maxActiveSessions >= 0) &&                (getActiveSessions() >= maxActiveSessions)) {            rejectedSessions++;            throw new TooManyActiveSessionsException(                    sm.getString("managerBase.createSession.ise"),                    maxActiveSessions);        }
          // Recycle or create a Session instance // 創(chuàng)建空的session 容器 return new StandardSession(this); Session session = createEmptySession();
          // Initialize the properties of the new session and return it // 默認(rèn)30分鐘有效期 session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60); String id = sessionId; if (id == null) { // sessionId 為空時(shí),生成一個(gè),隨機(jī)id id = generateSessionId(); } // 設(shè)置sessionId, 注意此處不僅僅是set這么簡單,其同時(shí)會將自身session注冊到全局session管理器中.如下文 session.setId(id); sessionCounter++;
          SessionTiming timing = new SessionTiming(session.getCreationTime(), 0); synchronized (sessionCreationTiming) { // LinkedList, 添加一個(gè),刪除一個(gè)? 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); // 通知監(jiān)聽者,這是框架該做好的事(擴(kuò)展點(diǎn)),不過不是本文的方向,忽略 if (notify) { tellNew(); } } // org.apache.catalina.session.ManagerBase#add @Override public void add(Session session) { // 取出 sessionId, 添加到 sessions 容器,統(tǒng)一管理 sessions.put(session.getIdInternal(), session); int size = getActiveSessions(); // 刷新最大活躍數(shù),使用雙重鎖優(yōu)化更新該值 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); }

          有興趣的同學(xué)可以看一下sessionId的生成算法:主要保證兩點(diǎn):1. 隨機(jī)性;2.不可重復(fù)性;

              // 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(); // 如果已經(jīng)存在該sessionId, 則重新生成一個(gè) // session 是一個(gè) ConcurrentHashMap 結(jié)構(gòu)數(shù)據(jù) } 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]; // 默認(rèn)16 int sessionIdLength = getSessionIdLength();
          // Render the result as a String of hexadecimal digits // Start with enough space for sessionIdLength and medium route size // 創(chuàng)建雙倍大小的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++) { // 轉(zhuǎn)換為16進(jìn)制 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(), 預(yù)生成 random SecureRandom random = randoms.poll(); if (random == null) { random = createSecureRandom(); } random.nextBytes(bytes); // 添加到 ConcurrentLinkedQueue 隊(duì)列中,事實(shí)上該 random 將會被反復(fù)循環(huán)使用, poll->add randoms.add(random); }

          創(chuàng)建好session后,需要進(jìn)行隨時(shí)的維護(hù):我們看下tomcat是如何刷新訪問時(shí)間的?可能比預(yù)想的簡單,其僅是更新一個(gè)訪問時(shí)間字段,再無其他。

              // 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() {        // 更新訪問時(shí)間        this.thisAccessedTime = System.currentTimeMillis();        // 訪問次數(shù)統(tǒng)計(jì),默認(rèn)不啟用        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);
          }

          再最后,要說明的是,整個(gè)sessions的管理使用一個(gè) ConcurrentHashMap 來存放全局會話信息,sessionId->session實(shí)例。

          對于同一次http請求中,該session會被存儲在當(dāng)前的Request棧org.apache.catalina.connector.Request#session字段中,從而無需每次深入獲取。每個(gè)請求進(jìn)來后,會將session保存在當(dāng)前的request信息中。

          3. 過期session清理?

          會話不可能不過期,不過期的也不叫會話了。

          會話過期的觸發(fā)時(shí)機(jī)主要有三個(gè):1. 每次進(jìn)行會話調(diào)用時(shí),會主動有效性isValid()驗(yàn)證,此時(shí)如果發(fā)現(xiàn)過期可以主動清理:2. 后臺定時(shí)任務(wù)觸發(fā)清理;3. 啟動或停止應(yīng)用的時(shí)候清理;(這對于非內(nèi)存式的存儲會更有用些)

              // case1. 請求時(shí)驗(yàn)證,如前面所述    // 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. 后臺定時(shí)任務(wù)清理 // org.apache.catalina.session.ManagerBase#backgroundProcess @Override public void backgroundProcess() { // 并非每次定時(shí)任務(wù)到達(dá)時(shí)都會進(jìn)行清理,而是要根據(jù)其清理頻率設(shè)置來運(yùn)行 // 默認(rèn)是 6 count = (count + 1) % processExpiresFrequency; if (count == 0) processExpires(); } /** * Invalidate all sessions that have expired. */ public void processExpires() {
          long timeNow = System.currentTimeMillis(); // 找出所有的sessions, 轉(zhuǎn)化為數(shù)組遍歷 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++) { // 事實(shí)上后臺任務(wù)也是調(diào)用 isValid() 方法 進(jìn)行過期任務(wù)清理的 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 時(shí)觸發(fā)過期清理(生命周期事件) // 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() 調(diào)用 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. // 主動調(diào)用 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() 事件到達(dá)時(shí)清理 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失效,實(shí)際上應(yīng)用即將關(guān)閉,失不失效的應(yīng)該也無所謂了 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"); } }

          接下來我們看下具體如何清理過期的會話?實(shí)際應(yīng)該就是一個(gè)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; } // 超過有效期,主動觸發(fā)清理 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()); } }

          清理工作的核心任務(wù)沒猜錯,還是進(jìn)行remove對應(yīng)的session, 但作為框架必然會設(shè)置很多的擴(kuò)展點(diǎn),為各監(jiān)聽器接入的機(jī)會。這些點(diǎn)的設(shè)計(jì),直接關(guān)系到整個(gè)功能的好壞了。

          4. session如何保證線程安全?

          實(shí)際是廢話,前面已經(jīng)明顯看出,其使用一個(gè) ConcurrentHashMap 作為session的管理容器,而ConcurrentHashMap本身就是線程安全的,自然也就保證了線程安全了。

          不過需要注意的是,上面的線程安全是指的不同客戶端間的數(shù)據(jù)是互不影響的。然而對于同一個(gè)客戶端的重復(fù)請求,以上實(shí)現(xiàn)并未處理,即可能會生成一次session,也可能生成n次session,不過實(shí)際影響不大,因?yàn)榭蛻舳说臓顟B(tài)與服務(wù)端的狀態(tài)都是一致的。

          5. 使用持久化方案的session管理實(shí)現(xiàn)

          默認(rèn)情況使用內(nèi)存作為session管理工具,一是方便,二是速度相當(dāng)快。但是最大的缺點(diǎn)是,其無法實(shí)現(xiàn)持久化,即可能停機(jī)后信息就丟失了(雖然上面有在停機(jī)時(shí)做了持久化操作,但仍然是不可靠的)。

          所以就有了與之相對的存儲方案了:Persistent,它有一個(gè)基類 PersistentManagerBase 繼承了 ManagerBase,做了些特別的實(shí)現(xiàn):

              // 1. session的添加    // 復(fù)用 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 { // 復(fù)用ManagerBase, 獲取Session實(shí)例 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 // 如果內(nèi)存中找不到會話信息,從存儲中查找,這是主要的區(qū)別 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. */ // 額,總之就是有點(diǎn)復(fù)雜 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; } // 重新加入到內(nèi)存 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的實(shí)現(xiàn)了,比如 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 實(shí)現(xiàn)樣例: 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); // 和內(nèi)存的實(shí)現(xiàn)差別就是,還要多一個(gè)對外部存儲的管理維護(hù) if (store != null){ removeSession(session.getIdInternal()); } }

          可以看到, PersistentManager 的實(shí)現(xiàn)還是有點(diǎn)復(fù)雜的,主要是在安全性和性能之間的平衡,它和 StandardManager 基本是一種包含關(guān)系,即除了要維護(hù)內(nèi)存session外,還要維護(hù)外部存儲的狀態(tài)。

          而現(xiàn)實(shí)情況是,既然已經(jīng)需要自行維護(hù)外部狀態(tài)了,為何還要去使用tomcat自帶的session管理呢?而如果站在框架session管理的設(shè)計(jì)者的角度,這可能也是無可奈何的事。

          而在我們自己的session管理實(shí)現(xiàn)中,一般的思路還是相通的,創(chuàng)建 -> 查找 -> 維持 -> 刪除 。 可以基于數(shù)據(jù)庫,緩存,或者其他,而且相信也不是件難事。




          往期精彩推薦



          騰訊、阿里、滴滴后臺面試題匯總總結(jié) — (含答案)

          面試:史上最全多線程面試題 !

          最新阿里內(nèi)推Java后端面試題

          JVM難學(xué)?那是因?yàn)槟銢]認(rèn)真看完這篇文章


          END


          關(guān)注作者微信公眾號 —《JAVA爛豬皮》


          了解更多java后端架構(gòu)知識以及最新面試寶典


          你點(diǎn)的每個(gè)好看,我都認(rèn)真當(dāng)成了


          看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動力


          作者:等你歸去來

          出處:https://www.cnblogs.com/yougewe/p/12902495.html

          瀏覽 35
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  九色91 | 日本女人性高潮视频 | 777久久| 人人草人人摸人人爽 | 大香蕉人人 |