1. 程式人生 > >Tomcat原始碼分析 (十)----- 徹底理解 Session機制

Tomcat原始碼分析 (十)----- 徹底理解 Session機制

Tomcat Session 概述

首先 HTTP 是一個無狀態的協議, 這意味著每次發起的HTTP請求, 都是一個全新的請求(與上個請求沒有任何聯絡, 服務端不會保留上個請求的任何資訊), 而 Session 的出現就是為了解決這個問題, 將 Client 端的每次請求都關聯起來, 要實現 Session 機制 通常通過 Cookie(cookie 裡面儲存統一識別符號號), URI 附加引數, 或者就是SSL (就是SSL 中的各種屬性作為一個Client請求的唯一標識), 而在初始化 ApplicationContext 指定預設的Session追蹤機制(URL + COOKIE), 若 Connector 配置了 SSLEnabled, 則將通過 SSL 追蹤Session的模式也加入追蹤機制裡面 (將 ApplicationContext.populateSessionTrackingModes()方法)

Cookie 概述

Cookie 是在Http傳輸中存在於Header中的一小撮文字資訊(KV), 每次瀏覽器都會將服務端傳送給自己的Cookie資訊返回傳送給服務端(PS: Cookie的內容儲存在瀏覽器端); 有了這種技術服務端就知道這次請求是誰傳送過來的(比如我們這裡的Session, 就是基於在Http傳輸中, 在Cookie裡面加入一個全域性唯一的識別符號號JsessionId來區分是哪個使用者的請求)

Tomcat 中 Cookie 的解析

在 Tomcat 8.0.5 中 Cookie 的解析是通過內部的函式 processCookies() 來進行操作的(其實就是將Http header 的內容直接賦值給 Cookie 物件, Cookie在Header中找name是"Cookie"的資料, 拿出來進行解析), 我們這裡主要從 jsessionid 的角度來看一下整個過程是如何觸發的, 我們直接看函式 CoyoteAdapter.postParseRequest() 中解析 jsessionId 那部分

// 嘗試從 URL, Cookie, SSL 回話中獲取請求的 ID, 並將 mapRequired 設定為 false
String sessionID = null;
// 1. 是否支援通過 URI 尾綴 JSessionId 的方式來追蹤 Session 的變化 (預設是支援的)
if (request.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) {
    // 2. 從 URI 尾綴的引數中拿取 jsessionId 的資料 (SessionConfig.getSessionUriParamName 是獲取對應cookie的名字, 預設 jsessionId, 可以在 web.xml 裡面進行定義)
    sessionID = request.getPathParameter( SessionConfig.getSessionUriParamName(request.getContext()));
    if (sessionID != null) { 
        // 3. 若從 URI 裡面拿取了 jsessionId, 則直接進行賦值給 request
        request.setRequestedSessionId(sessionID);
        request.setRequestedSessionURL(true);
    }
}

// Look for session ID in cookies and SSL session
// 4. 通過 cookie 裡面獲取 JSessionId 的值
parseSessionCookiesId(req, request);   
// 5. 在 SSL 模式下獲取 JSessionId 的值                             
parseSessionSslId(request);                                         

/**
 * Parse session id in URL.
 */
protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {

    // If session tracking via cookies has been disabled for the current
    // context, don't go looking for a session ID in a cookie as a cookie
    // from a parent context with a session ID may be present which would
    // overwrite the valid session ID encoded in the URL
    Context context = request.getMappingData().context;
    // 1. Tomcat 是否支援 通過 cookie 機制 跟蹤 session
    if (context != null && !context.getServletContext()
            .getEffectiveSessionTrackingModes().contains(
                    SessionTrackingMode.COOKIE)) {                      
        return;
    }

    // Parse session id from cookies
     // 2. 獲取 Cookie的實際引用物件 (PS: 這裡還沒有觸發 Cookie 解析, 也就是 serverCookies 裡面是空資料, 資料還只是儲存在 http header 裡面)
    Cookies serverCookies = req.getCookies(); 
    // 3. 就在這裡出發了 Cookie 解析Header裡面的資料 (PS: 其實就是 輪訓查詢 Header 裡面那個 name 是 Cookie 的資料, 拿出來進行解析)    
    int count = serverCookies.getCookieCount();                         
    if (count <= 0) {
        return;
    }

    // 4. 獲取 sessionId 的名稱 JSessionId
    String sessionCookieName = SessionConfig.getSessionCookieName(context); 

    for (int i = 0; i < count; i++) {
        // 5. 輪詢所有解析出來的 Cookie
        ServerCookie scookie = serverCookies.getCookie(i);      
        // 6. 比較 Cookie 的名稱是否是 jsessionId        
        if (scookie.getName().equals(sessionCookieName)) {              
            logger.info("scookie.getName().equals(sessionCookieName)");
            logger.info("Arrays.asList(Thread.currentThread().getStackTrace()):" + Arrays.asList(Thread.currentThread().getStackTrace()));
            // Override anything requested in the URL
            // 7. 是否 jsessionId 還沒有解析 (並且只將第一個解析成功的值 set 進去)
            if (!request.isRequestedSessionIdFromCookie()) {            
                // Accept only the first session id cookie
                // 8. 將MessageBytes轉成 char
                convertMB(scookie.getValue());        
                // 9. 設定 jsessionId 的值                
                request.setRequestedSessionId(scookie.getValue().toString());
                request.setRequestedSessionCookie(true);
                request.setRequestedSessionURL(false);
                if (log.isDebugEnabled()) {
                    log.debug(" Requested cookie session id is " +
                        request.getRequestedSessionId());
                }
            } else {
                // 10. 若 Cookie 裡面存在好幾個 jsessionid, 則進行覆蓋 set 值
                if (!request.isRequestedSessionIdValid()) {             
                    // Replace the session id until one is valid
                    convertMB(scookie.getValue());
                    request.setRequestedSessionId
                        (scookie.getValue().toString());
                }
            }
        }
    }

}
上面的步驟其實就是依次從 URI, Cookie, SSL 裡面進行 jsessionId 的解析, 其中從Cookie裡面進行解析是最常用的, 而且 就這個Tomcat版本里面, 從cookie裡面解析 jsessionid 藏得比較深, 是由 Cookie.getCookieCount() 來進行觸發的, 整個解析的過程其實就是將執行緒 header 裡面的資料依次遍歷, 找到 name="Cookie"的資料,拿出來解析字串(這裡就不再敘述了); 程式到這裡其實若客戶端傳 jsessionId 的話, 則服務端已經將其解析出來, 並且set到Request物件裡面了, 但是 Session 物件還沒有觸發建立, 最多也就是查詢一下 jsessionId 對應的 Session 在 Manager 裡面是否存在。

tomcat session 設計分析

tomcat session 元件圖如下所示,其中 Context 對應一個 webapp 應用,每個 webapp 有多個 HttpSessionListener, 並且每個應用的 session 是獨立管理的,而 session 的建立、銷燬由 Manager 元件完成,它內部維護了 N 個 Session 例項物件。在前面的文章中,我們分析了 Context 元件,它的預設實現是 StandardContext,它與 Manager 是一對一的關係,Manager 建立、銷燬會話時,需要藉助 StandardContext 獲取 HttpSessionListener 列表並進行事件通知,而 StandardContext 的後臺執行緒會對 Manager 進行過期 Session 的清理工作

org.apache.catalina.Manager 介面的主要方法如下所示,它提供了 Contextorg.apache.catalina.SessionIdGenerator的 getter/setter 介面,以及建立、新增、移除、查詢、遍歷 Session 的 API 介面,此外還提供了 Session 持久化的介面(load/unload) 用於載入/解除安裝會話資訊,當然持久化要看不同的實現類

public interface Manager {
    public Context getContext();
    public void setContext(Context context);
    public SessionIdGenerator getSessionIdGenerator();
    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
    public void add(Session session);
    public void addPropertyChangeListener(PropertyChangeListener listener);
    public void changeSessionId(Session session);
    public void changeSessionId(Session session, String newId);
    public Session createEmptySession();
    public Session createSession(String sessionId);
    public Session findSession(String id) throws IOException;
    public Session[] findSessions();
    public void remove(Session session);
    public void remove(Session session, boolean update);
    public void removePropertyChangeListener(PropertyChangeListener listener);
    public void unload() throws IOException;
    public void backgroundProcess();
    public boolean willAttributeDistribute(String name, Object value);
}

tomcat8.5 提供了 4 種實現,預設使用 StandardManager,tomcat 還提供了叢集會話的解決方案,但是在實際專案中很少運用

  • StandardManager:Manager 預設實現,在記憶體中管理 session,宕機將導致 session 丟失;但是當呼叫 Lifecycle 的 start/stop 介面時,將採用 jdk 序列化儲存 Session 資訊,因此當 tomcat 發現某個應用的檔案有變更進行 reload 操作時,這種情況下不會丟失 Session 資訊
  • DeltaManager:增量 Session 管理器,用於Tomcat叢集的會話管理器,某個節點變更 Session 資訊都會同步到叢集中的所有節點,這樣可以保證 Session 資訊的實時性,但是這樣會帶來較大的網路開銷
  • BackupManager:用於 Tomcat 叢集的會話管理器,與DeltaManager不同的是,某個節點變更 Session 資訊的改變只會同步給叢集中的另一個 backup 節點
  • PersistentManager:當會話長時間空閒時,將會把 Session 資訊寫入磁碟,從而限制記憶體中的活動會話數量;此外,它還支援容錯,會定期將記憶體中的 Session 資訊備份到磁碟

 我們來看下 StandardManager 的類圖,它也是個 Lifecycle 元件,並且 ManagerBase 實現了主要的邏輯。

Tomcat 中 Session 的建立

經過上面的Cookie解析, 則若存在jsessionId的話, 則已經set到Request裡面了, 那Session又是何時觸發建立的呢? 主要還是程式碼 request.getSession(), 看程式碼:

public class SessionExample extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException  {
        HttpSession session = request.getSession();
        // other code......
    }
}

我們來看看getSession():

// 獲取 request 對應的 session
public HttpSession getSession() {
    // 這裡就是 通過 managerBase.sessions 獲取 Session
    Session session = doGetSession(true); 
    if (session == null) {
        return null;
    }
    return session.getSession();
}

// create 代表是否建立 StandardSession
protected Session doGetSession(boolean create) {              

    // There cannot be a session if no context has been assigned yet
    // 1. 檢驗 StandardContext
    if (context == null) {
        return (null);                                           
    }

    // Return the current session if it exists and is valid
     // 2. 校驗 Session 的有效性
    if ((session != null) && !session.isValid()) {              
        session = null;
    }
    if (session != null) {
        return (session);
    }

    // Return the requested session if it exists and is valid
    Manager manager = null;
    if (context != null) {
        //拿到StandardContext 中對應的StandardManager,Context與 Manager 是一對一的關係
        manager = context.getManager();
    }
    if (manager == null)
     {
        return (null);      // Sessions are not supported
    }
    if (requestedSessionId != null) {
        try {        
            // 3. 通過 managerBase.sessions 獲取 Session
            // 4. 通過客戶端的 sessionId 從 managerBase.sessions 來獲取 Session 物件
            session = manager.findSession(requestedSessionId);   
        } catch (IOException e) {
            session = null;
        }
         // 5. 判斷 session 是否有效
        if ((session != null) && !session.isValid()) {          
            session = null;
        }
        if (session != null) {
            // 6. session access +1
            session.access();                                    
            return (session);
        }
    }

    // Create a new session if requested and the response is not committed
    // 7. 根據標識是否建立 StandardSession ( false 直接返回)
    if (!create) {
        return (null);                                           
    }
    // 當前的 Context 是否支援通過 cookie 的方式來追蹤 Session
    if ((context != null) && (response != null) && context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE) && response.getResponse().isCommitted()) {
        throw new IllegalStateException
          (sm.getString("coyoteRequest.sessionCreateCommitted"));
    }

    // Attempt to reuse session id if one was submitted in a cookie
    // Do not reuse the session id if it is from a URL, to prevent possible
    // phishing attacks
    // Use the SSL session ID if one is present.
    // 8. 到這裡其實是沒有找到 session, 直接建立 Session 出來
    if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) {
        session = manager.createSession(getRequestedSessionId()); // 9. 從客戶端讀取 sessionID, 並且根據這個 sessionId 建立 Session
    } else {
        session = manager.createSession(null);
    }

    // Creating a new session cookie based on that session
    if ((session != null) && (getContext() != null)&& getContext().getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)) {
        // 10. 根據 sessionId 來建立一個 Cookie
        Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure());
        // 11. 最後在響應體中寫入 cookie
        response.addSessionCookieInternal(cookie);              
    }

    if (session == null) {
        return null;
    }
    // 12. session access 計數器 + 1
    session.access();                                          
    return session;
}

我們看看 manager.createSession(null);

public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
    //Manager管理著當前Context的所有session
    protected Map<String, Session> sessions = new ConcurrentHashMap<>();
    @Override
    public Session findSession(String id) throws IOException {
        if (id == null) {
            return null;
        }
        //通過JssionId獲取session
        return sessions.get(id);
    }
    
    public Session createSession(String sessionId) {
        // 1. 判斷 單節點的 Session 個數是否超過限制
        if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) {      
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }

        // Recycle or create a Session instance
        // 建立一個 空的 session
        // 2. 建立 Session
        Session session = createEmptySession();                     

        // Initialize the properties of the new session and return it
        // 初始化空 session 的屬性
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        // 3. StandardSession 最大的預設 Session 啟用時間
        session.setMaxInactiveInterval(this.maxInactiveInterval); 
        String id = sessionId;
        // 若沒有從 client 端讀取到 jsessionId
        if (id == null) {      
            // 4. 生成 sessionId (這裡通過隨機數來生成)    
            id = generateSessionId();                              
        }
        //這裡會將session存入Map<String, Session> sessions = new ConcurrentHashMap<>();
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            // 5. 每次建立 Session 都會建立一個 SessionTiming, 並且 push 到 連結串列 sessionCreationTiming 的最後
            sessionCreationTiming.add(timing); 
            // 6. 並且將 連結串列 最前面的節點刪除        
            sessionCreationTiming.poll();                         
        }      
        // 那這個 sessionCreationTiming 是什麼作用呢, 其實 sessionCreationTiming 是用來統計 Session的新建及失效的頻率 (好像Zookeeper 裡面也有這個的統計方式)    
        return (session);
    }
    
    @Override
    public void add(Session session) {
        //將建立的Seesion存入Map<String, Session> sessions = new ConcurrentHashMap<>();
        sessions.put(session.getIdInternal(), session);
        int size = getActiveSessions();
        if( size > maxActive ) {
            synchronized(maxActiveUpdateLock) {
                if( size > maxActive ) {
                    maxActive = size;
                }
            }
        }
    }
}

@Override
public void setId(String id) {
    setId(id, true);
}

@Override
public void setId(String id, boolean notify) {

    if ((this.id != null) && (manager != null))
        manager.remove(this);

    this.id = id;

    if (manager != null)
        manager.add(this);

    if (notify) {
        tellNew();
    }
}

其主要的步驟就是:

1. 若 request.Session != null, 則直接返回 (說明同一時刻之前有其他執行緒建立了Session, 並且賦值給了 request)

2. 若 requestedSessionId != null, 則直接通過 manager 來進行查詢一下, 並且判斷是否有效

3. 呼叫 manager.createSession 來建立對應的Session,並將Session存入Manager的Map中

4. 根據 SessionId 來建立 Cookie, 並且將 Cookie 放到 Response 裡面

5. 直接返回 Session

Session清理

Background 執行緒

前面我們分析了 Session 的建立過程,而 Session 會話是有時效性的,下面我們來看下 tomcat 是如何進行失效檢查的。在分析之前,我們先回顧下 Container 容器的 Background 執行緒。

tomcat 所有容器元件,都是繼承至 ContainerBase 的,包括 StandardEngineStandardHostStandardContextStandardWrapper,而 ContainerBase 在啟動的時候,如果 backgroundProcessorDelay 引數大於 0 則會開啟 ContainerBackgroundProcessor 後臺執行緒,呼叫自己以及子容器的 backgroundProcess 進行一些後臺邏輯的處理,和 Lifecycle 一樣,這個動作是具有傳遞性的,也就

關鍵程式碼如下所示:

ContainerBase.java

protected synchronized void startInternal() throws LifecycleException {
    // other code......
    // 開啟ContainerBackgroundProcessor執行緒用於處理子容器,預設情況下backgroundProcessorDelay=-1,不會啟用該執行緒
    threadStart();
}

protected class ContainerBackgroundProcessor implements Runnable {
    public void run() {
        // threadDone 是 volatile 變數,由外面的容器控制
        while (!threadDone) {
            try {
                Thread.sleep(backgroundProcessorDelay * 1000L);
            } catch (InterruptedException e) {
                // Ignore
            }
            if (!threadDone) {
                processChildren(ContainerBase.this);
            }
        }
    }

    protected void processChildren(Container container) {
        container.backgroundProcess();
        Container[] children = container.findChildren();
        for (int i = 0; i < children.length; i++) {
            // 如果子容器的 backgroundProcessorDelay 引數小於0,則遞迴處理子容器
            // 因為如果該值大於0,說明子容器自己開啟了執行緒處理,因此父容器不需要再做處理
            if (children[i].getBackgroundProcessorDelay() <= 0) {
                processChildren(children[i]);
            }
        }
    }
}

Session 檢查

backgroundProcessorDelay 引數預設值為 -1,單位為秒,即預設不啟用後臺執行緒,而 tomcat 的 Container 容器需要開啟執行緒處理一些後臺任務,比如監聽 jsp 變更、tomcat 配置變動、Session 過期等等,因此 StandardEngine 在構造方法中便將 backgroundProcessorDelay 引數設為 10(當然可以在 server.xml 中指定該引數),即每隔 10s 執行一次。那麼這個執行緒怎麼控制生命週期呢?我們注意到 ContainerBase 有個 threadDone 變數,用 volatile 修飾,如果呼叫 Container 容器的 stop 方法該值便會賦值為 false,那麼該後臺執行緒也會退出迴圈,從而結束生命週期。另外,有個地方需要注意下,父容器在處理子容器的後臺任務時,需要判斷子容器的 backgroundProcessorDelay 值,只有當其小於等於 0 才進行處理,因為如果該值大於0,子容器自己會開啟執行緒自行處理,這時候父容器就不需要再做處理了

前面分析了容器的後臺執行緒是如何排程的,下面我們重點來看看 webapp 這一層,以及 StandardManager 是如何清理過期會話的。StandardContext 重寫了 backgroundProcess 方法,除了對子容器進行處理之外,還會對一些快取資訊進行清理,關鍵程式碼如下所示:

 

StandardContext.java

@Override
public void backgroundProcess() {
    if (!getState().isAvailable())
        return;
    // 熱載入 class,或者 jsp
    Loader loader = getLoader();
    if (loader != null) {
        loader.backgroundProcess();
    }
    // 清理過期Session
    Manager manager = getManager();
    if (manager != null) {
        manager.backgroundProcess();
    }
    // 清理資原始檔的快取
    WebResourceRoot resources = getResources();
    if (resources != null) {
        resources.backgroundProcess();
    }
    // 清理物件或class資訊快取
    InstanceManager instanceManager = getInstanceManager();
    if (instanceManager instanceof DefaultInstanceManager) {
        ((DefaultInstanceManager)instanceManager).backgroundProcess();
    }
    // 呼叫子容器的 backgroundProcess 任務
    super.backgroundProcess();
}

StandardContext 重寫了 backgroundProcess 方法,在呼叫子容器的後臺任務之前,還會呼叫 LoaderManagerWebResourceRootInstanceManager 的後臺任務,這裡我們只關心 Manager 的後臺任務。弄清楚了 StandardManager 的來龍去脈之後,我們接下來分析下具體的邏輯。

StandardManager 繼承至 ManagerBase,它實現了主要的邏輯,關於 Session 清理的程式碼如下所示。backgroundProcess 預設是每隔10s呼叫一次,但是在 ManagerBase 做了取模處理,預設情況下是 60s 進行一次 Session 清理。tomcat 對 Session 的清理並沒有引入時間輪,因為對 Session 的時效性要求沒有那麼精確,而且除了通知 SessionListener

ManagerBase.java

public void backgroundProcess() {
    // processExpiresFrequency 預設值為 6,而backgroundProcess預設每隔10s呼叫一次,也就是說除了任務執行的耗時,每隔 60s 執行一次
    count = (count + 1) % processExpiresFrequency;
    if (count == 0) // 預設每隔 60s 執行一次 Session 清理
        processExpires();
}

/**
 * 單執行緒處理,不存線上程安全問題
 */
public void processExpires() {
    long timeNow = System.currentTimeMillis();
    Session sessions[] = findSessions();    // 獲取所有的 Session
    int expireHere = 0 ;
    for (int i = 0; i < sessions.length; i++) {
        // Session 的過期是在 isValid() 裡面處理的
        if (sessions[i]!=null && !sessions[i].isValid()) {
            expireHere++;
        }
    }
    long timeEnd = System.currentTimeMillis();
    // 記錄下處理時間
    processingTime += ( timeEnd - timeNow );
}

清理過期 Session

在上面的程式碼,我們並沒有看到太多的過期處理,只是呼叫了 sessions[i].isValid(),原來清理動作都在這個方法裡面處理的,相當的隱晦。在 StandardSession#isValid() 方法中,如果 now - thisAccessedTime >= maxInactiveInterval則判定當前 Session 過期了,而這個 thisAccessedTime 引數在每次訪問都會進行更新

public boolean isValid() {
    // other code......
    // 如果指定了最大不活躍時間,才會進行清理,這個時間是 Context.getSessionTimeout(),預設是30分鐘
    if (maxInactiveInterval > 0) {
        int timeIdle = (int) (getIdleTimeInternal() / 1000L);
        if (timeIdle >= maxInactiveInterval) {
            expire(true);
        }
    }
    return this.isValid;
}

而 expire 方法處理的邏輯較繁鎖,下面我用虛擬碼簡單地描述下核心的邏輯,由於這個步驟可能會有多執行緒進行操作,因此使用 synchronized 對當前 Session 物件加鎖,還做了雙重校驗,避免重複處理過期 Session。它還會向 Container 容器發出事件通知,還會呼叫 HttpSessionListener 進行事件通知,這個也就是我們 web 應用開發的 HttpSessionListener 了。由於 Manager 中維護了 Session 物件,因此還要將其從 Manager 移除。Session 最重要的功能就是儲存資料了,可能存在強引用,而導致 Session 無法被 gc 回收,因此還要移除內部的 key/value 資料。由此可見,tomcat 編碼的嚴謹性了,稍有不慎將可能出現併發問題,以及出現記憶體洩露

public void expire(boolean notify) {
    //1、校驗 isValid 值,如果為 false 直接返回,說明已經被銷燬了
    synchronized (this) {   // 加鎖
        //2、雙重校驗 isValid 值,避免併發問題
        Context context = manager.getContext();
        if (notify) {   
            Object listeners[] = context.getApplicationLifecycleListeners();
            HttpSessionEvent event = new HttpSessionEvent(getSession());
            for (int i = 0; i < listeners.length; i++) {
            //3、判斷是否為 HttpSessionListener,不是則繼續迴圈
            //4、向容器發出Destory事件,並呼叫 HttpSessionListener.sessionDestroyed() 進行通知
            context.fireContainerEvent("beforeSessionDestroyed", listener);
            listener.sessionDestroyed(event);
            context.fireContainerEvent("afterSessionDestroyed", listener);
        }
        //5、從 manager 中移除該  session
        //6、向 tomcat 的 SessionListener 發出事件通知,非 HttpSessionListener
        //7、清除內部的 key/value,避免因為強引用而導致無法回收 Session 物件
    }
}

由前面的分析可知,tomcat 會根據時間戳清理過期 Session,那麼 tomcat 又是如何更新這個時間戳呢? tomcat 在處理完請求之後,會對 Request 物件進行回收,並且會對 Session 資訊進行清理,而這個時候會更新 thisAccessedTimelastAccessedTime 時間戳。此外,我們通過呼叫 request.getSession() 這個 API 時,在返回 Session 時會呼叫 Session#access() 方法,也會更新 thisAccessedTime 時間戳。這樣一來,每次請求都會更新時間戳,可以保證 Session 的鮮活時間。

org.apache.catalina.connector.Request.java

protected void recycleSessionInfo() {
    if (session != null) {  
        session.endAccess();    // 更新時間戳
    }
    // 回收 Request 物件的內部資訊
    session = null;
    requestedSessionCookie = false;
    requestedSessionId = null;
    requestedSessionURL = false;
    requestedSessionSSL = false;
}

org.apache.catalina.session.StandardSession.java

public void endAccess() {
    isNew = false;
    if (LAST_ACCESS_AT_START) {     // 可以通過系統引數改變該值,預設為false
        this.lastAccessedTime = this.thisAccessedTime;
        this.thisAccessedTime = System.currentTimeMillis();
    } else {
        this.thisAccessedTime = System.currentTimeMillis();
        this.lastAccessedTime = this.thisAccessedTime;
    }
}

public void access() {
    this.thisAccessedTime = System.currentTimeMillis();
}

&n