天天看點

Tomcat Session管理機制(Tomcat源碼解析七)

前面幾篇我們分析了Tomcat的啟動,關閉,請求處理的流程,tomcat的classloader機制,本篇将接着分析Tomcat的session管理方面的内容。

在開始之前,我們先來看一下總體上的結構,熟悉了總體結構以後,我們在一步步的去分析源代碼。Tomcat session相光的類圖如下:

Tomcat Session管理機制(Tomcat源碼解析七)

通過上圖,我們可以看出每一個StandardContext會關聯一個Manager,預設情況下Manager的實作類是StandardManager,而StandardManager内部會聚合多個Session,其中StandardSession是Session的預設實作類,當我們調用Request.getSession的時候,Tomcat通過StandardSessionFacade這個外觀類将StandardSession包裝以後傳回。

上面清楚了總體的結構以後,我們來進一步的通過源代碼來分析一下。咋們首先從Request的getSession方法看起。

org.apache.catalina.connector.Request#getSession
           
public HttpSession getSession() {
    Session session = doGetSession(true);
    if (session == null) {
        return null;
    }

    return session.getSession();
}
           

從上面的代碼,我們可以看出首先首先調用doGetSession方法擷取Session,然後再調用Session的getSession方法傳回HttpSession,那接下來我們再來看看doGetSession方法:

org.apache.catalina.connector.Request#doGetSession
protected Session doGetSession(boolean create) {

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

    // Return the current session if it exists and is valid
    if ((session != null) && !session.isValid()) {
        session = null;
    }
    if (session != null) {
        return (session);
    }

    // Return the requested session if it exists and is valid
    // 1 
    Manager manager = null;
    if (context != null) {
        manager = context.getManager();
    }
    if (manager == null)
     {
        return (null);      // Sessions are not supported
    }
    // 2
    if (requestedSessionId != null) {
        try {
            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
    // 3
    if (!create) {
        return (null);
    }
    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.
    // 4
    if (("/".equals(context.getSessionCookiePath())
            && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) {
        session = manager.createSession(getRequestedSessionId());
    } 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)) {
        // 5 
        Cookie cookie =
            ApplicationSessionCookieConfig.createSessionCookie(
                    context, session.getIdInternal(), isSecure());

        response.addSessionCookieInternal(cookie);
    }

    if (session == null) {
        return null;
    }

    session.access();
    return session;
}
           

下面我們就來重點分析一下,上面代碼中标注了數字的地方:

  1. 标注1(第17行)首先從StandardContext中擷取對應的Manager對象,預設情況下,這個地方擷取的其實就是StandardManager的執行個體。
  2. 标注2(第26行)從Manager中根據requestedSessionId擷取session,如果session已經失效了,則将session置為null以便下面建立新的session,如果session不為空則通過調用session的access方法标注session的通路時間,然後傳回。
  3. 标注3(第43行)判斷傳遞的參數,如果為false,則直接傳回空,這其實就是對應的Request.getSession(true/false)的情況,當傳遞false的時候,如果不存在session,則直接傳回空,不會建立。
  4. 标注4 (第59行)調用Manager來建立一個新的session,這裡預設會調用到StandardManager的方法,而StandardManager繼承了ManagerBase,那麼預設其實是調用了了ManagerBase的方法。
  5. 标注5 (第72行)建立了一個Cookie,而Cookie的名稱就是大家熟悉的JSESSIONID,另外JSESSIONID其實也是可以配置的,這個可以通過context節點的sessionCookieName來修改。比如….

通過doGetSession擷取到Session了以後,我們發現調用了session.getSession方法,而Session的實作類是StandardSession,那麼我們再來看下StandardSession的getSession方法。

org.apache.catalina.session.StandardSession#getSession
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 {
            facade = new StandardSessionFacade(this);
        }
    }
    return (facade);

}
           

通過上面的代碼,我們可以看到通過StandardSessionFacade的包裝類将StandardSession包裝以後傳回。到這裡我想大家應該熟悉了Session建立的整個流程。

接着我們再來看看,Sesssion是如何被銷毀的。我們在Tomcat啟動過程(Tomcat源代碼閱讀系列之三)中之處,在容器啟動以後會啟動一個ContainerBackgroundProcessor線程,這個線程是在Container啟動的時候啟動的,這條線程就通過背景周期性的調用org.apache.catalina.core.ContainerBase#backgroundProcess,而backgroundProcess方法最終又會調用org.apache.catalina.session.ManagerBase#backgroundProcess,接下來我們就來看看Manger的backgroundProcess方法。

org.apache.catalina.session.ManagerBase#backgroundProcess
public void backgroundProcess() {
    count = (count + 1) % processExpiresFrequency;
    if (count == 0)
        processExpires();
}
           

上面的代碼裡,需要注意一下,預設情況下backgroundProcess是每10秒運作一次(StandardEngine構造的時候,将backgroundProcessorDelay設定為了10),而這裡我們通過processExpiresFrequency來控制頻率,例如processExpiresFrequency的值預設為6,那麼相當于沒一分鐘運作一次processExpires方法。接下來我們再來看看processExpires。

org.apache.catalina.session.ManagerBase#processExpires
public void processExpires() {

    long timeNow = System.currentTimeMillis();
    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++) {
        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 );

}
           

上面的代碼比較簡單,首先查找出目前context的所有的session,然後調用session的isValid方法,接下來我們在看看Session的isValid方法。

org.apache.catalina.session.StandardSession#isValid
public boolean isValid() {

    if (this.expiring) {
        return true;
    }

    if (!this.isValid) {
        return false;
    }

    if (ACTIVITY_CHECK && accessCount.get() > 0) {
        return true;
    }

    if (maxInactiveInterval > 0) {
        long timeNow = System.currentTimeMillis();
        int timeIdle;
        if (LAST_ACCESS_AT_START) {
            timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L);
        } else {
            timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
        }
        if (timeIdle >= maxInactiveInterval) {
            expire(true);
        }
    }

    return (this.isValid);
}
           

檢視上面的代碼,主要就是通過對比目前時間和上次通路的時間差是否大于了最大的非活動時間間隔,如果大于就會調用expire(true)方法對session進行超期處理。這裡需要注意一點,預設情況下LAST_ACCESS_AT_START為false,讀者也可以通過設定系統屬性的方式進行修改,而如果采用LAST_ACCESS_AT_START的時候,那麼請求本身的處理時間将不算在内。比如一個請求處理開始的時候是10:00,請求處理花了1分鐘,那麼如果LAST_ACCESS_AT_START為true,則算是否超期的時候,是從10:00算起,而不是10:01。

接下來我們再來看看expire方法,代碼如下:

org.apache.catalina.session.StandardSession#expire
public void expire(boolean notify) {

    // Check to see if expire is in progress or has previously been called
    if (expiring || !isValid)
        return;

    synchronized (this) {
        // Check again, now we are inside the sync so this code only runs once
        // Double check locking - expiring and isValid need to be volatile
        if (expiring || !isValid)
            return;

        if (manager == null)
            return;

        // Mark this session as "being expired"
        // 1         
        expiring = true;

        // Notify interested application event listeners
        // FIXME - Assumes we call listeners in reverse order
        Context context = (Context) manager.getContainer();

        // 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
        ClassLoader oldTccl = null;
        if (context.getLoader() != null &&
                context.getLoader().getClassLoader() != null) {
            oldTccl = Thread.currentThread().getContextClassLoader();
            if (Globals.IS_SECURITY_ENABLED) {
                PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                        context.getLoader().getClassLoader());
                AccessController.doPrivileged(pa);
            } else {
                Thread.currentThread().setContextClassLoader(
                        context.getLoader().getClassLoader());
            }
        }
        try {
            // 2
            Object listeners[] = context.getApplicationLifecycleListeners();
            if (notify && (listeners != null)) {
                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.getContainer().getLogger().error
                            (sm.getString("standardSession.sessionEvent"), t);
                    }
                }
            }
        } finally {
            if (oldTccl != null) {
                if (Globals.IS_SECURITY_ENABLED) {
                    PrivilegedAction<Void> pa =
                        new PrivilegedSetTccl(oldTccl);
                    AccessController.doPrivileged(pa);
                } else {
                    Thread.currentThread().setContextClassLoader(oldTccl);
                }
            }
        }

        if (ACTIVITY_CHECK) {
            accessCount.set(0);
        }
        setValid(false);

        // Remove this session from our manager's active sessions
        // 3 
        manager.remove(this, true);

        // Notify interested session event listeners
        if (notify) {
            fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
        }

        // Call the logout method
        if (principal instanceof GenericPrincipal) {
            GenericPrincipal gp = (GenericPrincipal) principal;
            try {
                gp.logout();
            } catch (Exception e) {
                manager.getContainer().getLogger().error(
                        sm.getString("standardSession.logoutfail"),
                        e);
            }
        }

        // We have completed expire of this session
        expiring = false;

        // Unbind any objects associated with this session
        // 4
        String keys[] = keys();
        for (int i = 0; i < keys.length; i++)
            removeAttributeInternal(keys[i], notify);

    }

}
           

上面代碼的主流程我已經标注了數字,我們來逐一分析一下:

  1. 标注1(第18行)标記目前的session為超期
  2. 标注2(第41行)出發HttpSessionListener監聽器的方法。
  3. 标注3(第89行)從Manager裡面移除目前的session
  4. 标注4(第113行)将session中儲存的屬性移除。

到這裡我們已經清楚了Tomcat中對與StandardSession的建立以及銷毀的過程,其實StandardSession僅僅是實作了記憶體中Session的存儲,而Tomcat還支援将Session持久化,以及Session叢集節點間的同步。這些内容我們以後再來分析。