1. 程式人生 > >Tomcat session的實現:執行緒安全與管理

Tomcat session的實現:執行緒安全與管理

  本文所說的session是單機版本的session, 事實上在當前的網際網路實踐中已經不太存在這種定義了。我們主要討論的是其安全共享的實現,只從理論上來討論,不必太過在意實用性問題。

  

1. session 的意義簡說

  大概就是一個會話的的定義,客戶端有cookie記錄,服務端session定義。用於確定你就是你的一個東西。

  每個使用者在一定範圍內共享某個session資訊,以實現登入狀態,操作的鑑權保持等。

  我們將會藉助tomcat的實現,剖析session管理的一些實現原理。

 

2. tomcat 中 session 什麼時候建立?

  session 資訊會在兩個地方呼叫,一是每次請求進來時,框架會嘗試去載入原有對應的session資訊(不會新建)。二是應用自己呼叫getSession()時,此時如果不存在session資訊,則建立一個新的session物件,代表應用後續會使用此功能。即框架不會自動支援session相關功能,只是在你需要的時候進行輔助操作。

  

    // 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的流程大概是這樣的:

    1. 先查詢是否有session資訊存在,如果有則判斷是否失敗;
    2. 如果不存在session或已失效,則使用一個新的sessionId(非必須)建立一個session例項;
    3. session建立成功,則將sessionId寫入到cookie資訊中,以便客戶端後續使用;
    4. 每次請求完session,必定重新整理下訪問時間;

  session的管理主要有兩種實現方式,類圖如下:

  我們先主要以基於記憶體的實現來理解下session的管理過程。實際上StandardManager基本就依託於 ManagerBase 就實現了Session管理功能,下面我們來看一下其建立session如何?

    // 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);
    }
View Code

 

  建立好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資訊中。  

 

3. 過期session清理?

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

  會話過期的觸發時機主要有三個: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, 但作為框架必然會設定很多的擴充套件點,為各監聽器接入的機會。這些點的設計,直接關係到整個功能的好壞了。

 

4. session如何保證執行緒安全?

  實際是廢話,前面已經明顯看出,其使用一個 ConcurrentHashMap 作為session的管理容器,而ConcurrentHashMap本身就是執行緒安全的,自然也就保證了執行緒安全了。

  不過需要注意的是,上面的執行緒安全是指的不同客戶端間的資料是互不影響的。然而對於同一個客戶端的重複請求,以上實現並未處理,即可能會生成一次session,也可能生成n次session,不過實際影響不大,因為客戶端的狀態與服務端的狀態都是一致的。

 

5. 使用持久化方案的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管理實現中,一般的思路還是收到的,建立 -> 查詢 -> 維持 -> 刪除 。 可以基於資料庫,快取,或者其他,而且相信也不件難事。

&n