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();
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM