Tomcat源碼分析——Session管理分析(上)

前言

  對於廣大java開發者而已,對於J2EE規範中的Session應該並不陌生,咱們可使用Session管理用戶的會話信息,最多見的就是拿Session用來存放用戶登陸、身份、權限及狀態等信息。對於使用Tomcat做爲Web容器的大部分開發人員而言,Tomcat是如何實現Session標記用戶和管理Session信息的呢?html

概述

Session

  Tomcat內部定義了Session和HttpSession這兩個會話相關的接口,其類繼承體系如圖1所示。java

圖1  Session類繼承體系git

圖1中額外列出了Session的類繼承體系,這裏對他們逐個進行介紹。web

Session:Tomcat中有關會話的基本接口規範,圖1列出了它定義的主要方法,表1對這些方法進行介紹。數據庫

表1  Session接口說明數組

方法 描述
getCreationTime()/setCreationTime(time : long)  獲取與設置Session的建立時間
getId()/setId(id : String)   獲取與設置Session的ID
getThisAccessedTime() 獲取最近一次請求的開始時間
getLastAccessedTime() 獲取最近一次請求的完成時間
getManager()/setManager(manager : Manager)  獲取與設置Session管理器
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int) 獲取與設置Session的最大訪問間隔
getSession() 獲取HttpSession
isValid()/setValid(isValid : boolean)  獲取與設置Session的有效狀態
access()/endAccess()  開始與結束Session的訪問
expire() 設置Session過時

HttpSession:在HTTP客戶端與HTTP服務端提供的一種會話的接口規範,圖1列出了它定義的主要方法,表2對這些方法進行介紹。緩存

表2  HttpSession接口說明安全

方法 描述
getCreationTime() 獲取Session的建立時間
getId() 獲取Session的ID
getLastAccessedTime() 獲取最近一次請求的完成時間
getServletContext()  獲取當前Session所屬的ServletContext
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int) 獲取與設置Session的最大訪問間隔
getAttribute(name : String) /setAttribute(name : String, value : Object) 獲取與設置Session做用域的屬性
removeAttribute(name : String) 清除Session做用域的屬性
invalidate() 使Session失效並解除任何與此Session綁定的對象

ClusterSession:集羣部署下的會話接口規範,圖1列出了它的主要方法,表3對這些方法進行介紹。服務器

表3  ClusterSession接口說明網絡

方法 描述
isPrimarySession() 是不是集羣的主Session
setPrimarySession(boolean primarySession) 設置集羣主Session

StandardSession:標準的HTTP Session實現,本文將以此實現爲例展開。

在部署Tomcat集羣時,須要使集羣中各個節點的會話狀態保持同步,目前Tomcat提供了兩種同步策略:

  • ReplicatedSession:每次都把整個會話對象同步給集羣中的其餘節點,其餘節點而後更新整個會話對象。這種實現比較簡單方便,但會形成大量無效信息的傳輸。
  • DeltaSession:對會話中增量修改的屬性進行同步。這種方式因爲是增量的,因此會大大下降網絡I/O的開銷,可是實現上會比較複雜由於涉及到對會話屬性操做過程的管理。

Session管理器

  Tomcat內部定義了Manager接口用於制定Session管理器的接口規範,目前已經有不少Session管理器的實現,如圖2所示。

圖2  Session管理器的類繼承體系

對應圖2中的內容咱們下面逐個描述:

Manager:Tomcat對於Session管理器定義的接口規範,圖2已經列出了Manager接口中定義的主要方法,表4詳細描述了這些方法的做用。

表4  Manager接口說明

方法 描述
getContainer()/setContainer(container : Container)  獲取或設置Session管理器關聯的容器,通常爲Context容器
getDistributable()/setDistributable(distributable : boolean)   獲取或設置Session管理器是否支持分佈式
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int)   獲取或設置Session管理器建立的Session的最大非活動時間間隔
getSessionIdLength()/setSessionIdLength(idLength : int)  獲取或設置Session管理器建立的Session ID的長度
getSessionCounter()/setSessionCounter(sessionCounter : long)   獲取或設置Session管理器建立的Session總數
getMaxActive()/setMaxActive(maxActive : int)  獲取或設置當前已激活Session的最大數量
getActiveSessions()   獲取當前激活的全部Session
getExpiredSessions()/setExpiredSessions(expiredSessions : long)  獲取或設置當前已過時Session的數量
getRejectedSessions()/setRejectedSessions(rejectedSessions : int)  獲取或設置已拒絕建立Session的數量
getSessionMaxAliveTime()/setSessionMaxAliveTime(sessionMaxAliveTime : int)   獲取或設置已過時Session中的最大活動時長
getSessionAverageAliveTime()/setSessionAverageAliveTime(sessionAverageAliveTime : int)  獲取或設置已過時Session的平均活動時長
add(session : Session)/remove(session : Session)  給Session管理器增長或刪除活動Session
changeSessionId(session : Session)  給Session設置新生成的隨機Session ID
createSession(sessionId : String)  基於Session管理器的默認屬性配置建立新的Session
findSession(id : String)  返回sessionId參數惟一標記的Session
findSessions()  返回Session管理器管理的全部活動Session
load()/unload()  從持久化機制中加載Session或向持久化機制寫入Session
backgroundProcess()  容器接口中定義的爲具體容器在後臺處理相關工做的實現,Session管理器基於此機制實現了過時Session的銷燬

ManagerBase:封裝了Manager接口通用實現的抽象類,未提供對load()/unload()等方法的實現,須要具體子類去實現。全部的Session管理器都繼承自ManagerBase。

ClusterManager:在Manager接口的基礎上增長了集羣部署下的一些接口,全部實現集羣下Session管理的管理器都須要實現此接口。

PersistentManagerBase:提供了對於Session持久化的基本實現。

PersistentManager:繼承自PersistentManagerBase,能夠在Server.xml的<Context>元素下經過配置<Store>元素來使用。PersistentManager能夠將內存中的Session信息備份到文件或數據庫中。當備份一個Session對象時,該Session對象會被複制到存儲器(文件或者數據庫)中,而原對象仍然留在內存中。所以即使服務器宕機,仍然能夠從存儲器中獲取活動的Session對象。若是活動的Session對象超過了上限值或者Session對象閒置了的時間過長,那麼Session會被換出到存儲器中以節省內存空間。

 

StandardManager:不用配置<Store>元素,當Tomcat正常關閉,重啓或Web應用從新加載時,它會將內存中的Session序列化到Tomcat目錄下的/work/Catalina/host_name/webapp_name/SESSIONS.ser文件中。當Tomcat重啓或應用加載完成後,Tomcat會將文件中的Session從新還原到內存中。若是忽然終止該服務器,則全部Session都將丟失,由於StandardManager沒有機會實現存盤處理。

ClusterManagerBase:提供了對於Session的集羣管理實現。

DeltaManager:繼承自ClusterManagerBase。此Session管理器是Tomcat在集羣部署下的默認管理器,當集羣中的某一節點生成或修改Session後,DeltaManager將會把這些修改增量複製到其餘節點。

BackupManager:沒有繼承ClusterManagerBase,而是直接實現了ClusterManager接口。是Tomcat在集羣部署下的可選的Session管理器,集羣中的全部Session都被全量複製到一個備份節點。集羣中的全部節點均可以訪問此備份節點,達到Session在集羣下的備份效果。

  爲簡單起見,本文以StandardManager爲例講解Session的管理。StandardManager是StandardContext的子組件,用來管理當前Context的全部Session的建立和維護。若是你已經閱讀或者熟悉了《Tomcat源碼分析——生命週期管理》一文的內容,那麼你就知道當StandardContext正式啓動,也就是StandardContext的startInternal方法(見代碼清單1)被調用時,StandardContext還會啓動StandardManager。

代碼清單1

    @Override
    protected synchronized void startInternal() throws LifecycleException {
               
        // 省略與Session管理無關的代碼
                       
                // Acquire clustered manager
                Manager contextManager = null;
                if (manager == null) {
                    if ( (getCluster() != null) && distributable) {
                        try {
                            contextManager = getCluster().createManager(getName());
                        } catch (Exception ex) {
                            log.error("standardContext.clusterFail", ex);
                            ok = false;
                        }
                    } else {
                        contextManager = new StandardManager();
                    }
                } 
                
                // Configure default manager if none was specified
                if (contextManager != null) {
                    setManager(contextManager);
                }

                if (manager!=null && (getCluster() != null) && distributable) {
                    //let the cluster know that there is a context that is distributable
                    //and that it has its own manager
                    getCluster().registerManager(manager);
                }
     // 省略與Session管理無關的代碼
            
            try {
                // Start manager
                if ((manager != null) && (manager instanceof Lifecycle)) {
                    ((Lifecycle) getManager()).start();
                }
    
                // Start ContainerBackgroundProcessor thread
                super.threadStart();
            } catch(Exception e) {
                log.error("Error manager.start()", e);
                ok = false;
            }
            
     // 省略與Session管理無關的代碼
    }

從代碼清單1能夠看到StandardContext的startInternal方法中涉及Session管理的執行步驟以下:

  1. 建立StandardManager;
  2. 若是Tomcat結合Apache作了分佈式部署,會將當前StandardManager註冊到集羣中;
  3. 啓動StandardManager;

StandardManager的start方法用於啓動StandardManager,實現見代碼清單2。

代碼清單2

    @Override
    public synchronized final void start() throws LifecycleException {
        
        //省略狀態校驗的代碼if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        setState(LifecycleState.STARTING_PREP);

        try {
            startInternal();
        } catch (LifecycleException e) {
            setState(LifecycleState.FAILED);
            throw e;
        }

        if (state.equals(LifecycleState.FAILED) ||
                state.equals(LifecycleState.MUST_STOP)) {
            stop();
        } else {
            // Shouldn't be necessary but acts as a check that sub-classes are
            // doing what they are supposed to.
            if (!state.equals(LifecycleState.STARTING)) {
                invalidTransition(Lifecycle.AFTER_START_EVENT);
            }
            
            setState(LifecycleState.STARTED);
        }
    }

從代碼清單2能夠看出啓動StandardManager的步驟以下:

  1. 調用init方法初始化StandardManager;
  2. 調用startInternal方法啓動StandardManager;

StandardManager的初始化

   通過上面的分析,咱們知道啓動StandardManager的第一步就是調用父類LifecycleBase的init方法,關於此方法已在《Tomcat源碼分析——生命週期管理》一文詳細介紹,因此咱們只須要關心StandardManager的initInternal。StandardManager自己並無實現initInternal方法,可是StandardManager的父類ManagerBase實現了此方法,其實現見代碼清單3。

代碼清單3

    @Override
    protected void initInternal() throws LifecycleException {
        
        super.initInternal();
        
        setDistributable(((Context) getContainer()).getDistributable());

        // Initialize random number generation
        getRandomBytes(new byte[16]);
    }

閱讀代碼清單3,咱們總結下ManagerBase的initInternal方法的執行步驟:

  1. 將容器自身即StandardManager註冊到JMX(LifecycleMBeanBase的initInternal方法的實現請參考《Tomcat源碼分析——生命週期管理》一文);
  2. 從父容器StandardContext中獲取當前Tomcat是不是集羣部署,並設置爲ManagerBase的布爾屬性distributable;
  3. 調用getRandomBytes方法從隨機數文件/dev/urandom中獲取隨機數字節數組,若是不存在此文件則經過反射生成java.security.SecureRandom的實例,用它生成隨機數字節數組。

注意:此處調用getRandomBytes方法生成的隨機數字節數組並不會被使用,之因此在這裏調用實際是爲了完成對隨機數生成器的初始化,以便未來分配Session ID時使用。

咱們詳細閱讀下getRandomBytes方法的代碼實現,見代碼清單4。

代碼清單4

    protected void getRandomBytes(byte bytes[]) {
        // Generate a byte array containing a session identifier
        if (devRandomSource != null && randomIS == null) {
            setRandomFile(devRandomSource);
        }
        if (randomIS != null) {
            try {
                int len = randomIS.read(bytes);
                if (len == bytes.length) {
                    return;
                }
                if(log.isDebugEnabled())
                    log.debug("Got " + len + " " + bytes.length );
            } catch (Exception ex) {
                // Ignore
            }
            devRandomSource = null;
            
            try {
                randomIS.close();
            } catch (Exception e) {
                log.warn("Failed to close randomIS.");
            }
            
            randomIS = null;
        }
        getRandom().nextBytes(bytes);
    }

代碼清單4中的setRandomFile方法(見代碼清單5)用於從隨機數文件/dev/urandom中獲取隨機數字節數組。

代碼清單5

    public void setRandomFile( String s ) {
        // as a hack, you can use a static file - and generate the same
        // session ids ( good for strange debugging )
        if (Globals.IS_SECURITY_ENABLED){
            randomIS = AccessController.doPrivileged(new PrivilegedSetRandomFile(s));
        } else {
            try{
                devRandomSource=s;
                File f=new File( devRandomSource );
                if( ! f.exists() ) return;
                randomIS= new DataInputStream( new FileInputStream(f));
                randomIS.readLong();
                if( log.isDebugEnabled() )
                    log.debug( "Opening " + devRandomSource );
            } catch( IOException ex ) {
                log.warn("Error reading " + devRandomSource, ex);
                if (randomIS != null) {
                    try {
                        randomIS.close();
                    } catch (Exception e) {
                        log.warn("Failed to close randomIS.");
                    }
                }
                devRandomSource = null;
                randomIS=null;
            }
        }
    }

代碼清單4中的getRandom方法(見代碼清單6)經過反射生成java.security.SecureRandom的實例,並用此實例生成隨機數字節數組。

代碼清單6

    public Random getRandom() {
        if (this.random == null) {
            // Calculate the new random number generator seed
            long seed = System.currentTimeMillis();
            long t1 = seed;
            char entropy[] = getEntropy().toCharArray();
            for (int i = 0; i < entropy.length; i++) {
                long update = ((byte) entropy[i]) << ((i % 8) * 8);
                seed ^= update;
            }
            try {
                // Construct and seed a new random number generator
                Class<?> clazz = Class.forName(randomClass);
                this.random = (Random) clazz.newInstance();
                this.random.setSeed(seed);
            } catch (Exception e) {
                // Fall back to the simple case
                log.error(sm.getString("managerBase.random", randomClass),
                        e);
                this.random = new java.util.Random();
                this.random.setSeed(seed);
            }
            if(log.isDebugEnabled()) {
                long t2=System.currentTimeMillis();
                if( (t2-t1) > 100 )
                    log.debug(sm.getString("managerBase.seeding", randomClass) + " " + (t2-t1));
            }
        }
        
        return (this.random);

    }

根據以上的分析,StandardManager的初始化主要就是執行了ManagerBase的initInternal方法。

StandardManager的啓動

  調用StandardManager的startInternal方法用於啓動StandardManager,見代碼清單7。

 代碼清單7

    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Force initialization of the random number generator
        if (log.isDebugEnabled())
            log.debug("Force random number initialization starting");
        generateSessionId();
        if (log.isDebugEnabled())
            log.debug("Force random number initialization completed");

        // Load unloaded sessions, if any
        try {
            load();
        } catch (Throwable t) {
            log.error(sm.getString("standardManager.managerLoad"), t);
        }

        setState(LifecycleState.STARTING);
    }

 從代碼清單7能夠看出啓動StandardManager的步驟以下:

步驟一 調用generateSessionId方法(見代碼清單8)強制初始化隨機數生成器;

注意:此處調用generateSessionId方法的目的不是爲了生成Session ID,而是爲了強制初始化隨機數生成器。

代碼清單8

    protected synchronized String generateSessionId() {

        byte random[] = new byte[16];
        String jvmRoute = getJvmRoute();
        String result = null;

        // Render the result as a String of hexadecimal digits
        StringBuilder buffer = new StringBuilder();
        do {
            int resultLenBytes = 0;
            if (result != null) {
                buffer = new StringBuilder();
                duplicates++;
            }

            while (resultLenBytes < this.sessionIdLength) {
                getRandomBytes(random);
                random = getDigest().digest(random);
                for (int j = 0;
                j < random.length && resultLenBytes < this.sessionIdLength;
                j++) {
                    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 (jvmRoute != null) {
                buffer.append('.').append(jvmRoute);
            }
            result = buffer.toString();
        } while (sessions.containsKey(result));
        return (result);

    }

步驟二  加載持久化的Session信息。爲何Session須要持久化?因爲在StandardManager中,全部的Session都維護在一個ConcurrentHashMap中,所以服務器重啓或者宕機會形成這些Session信息丟失或失效,爲了解決這個問題,Tomcat將這些Session經過持久化的方式來保證不會丟失。下面咱們來看看StandardManager的load方法的實現,見代碼清單9所示。

代碼清單9

    public void load() throws ClassNotFoundException, IOException {
        if (SecurityUtil.isPackageProtectionEnabled()){
            try{
                AccessController.doPrivileged( new PrivilegedDoLoad() );
            } catch (PrivilegedActionException ex){
                Exception exception = ex.getException();
                if (exception instanceof ClassNotFoundException){
                    throw (ClassNotFoundException)exception;
                } else if (exception instanceof IOException){
                    throw (IOException)exception;
                }
                if (log.isDebugEnabled())
                    log.debug("Unreported exception in load() "
                        + exception);
            }
        } else {
            doLoad();
        }
    }

若是須要安全機制是打開的而且包保護模式打開,會經過建立PrivilegedDoLoad來加載持久化的Session,其實現如代碼清單10所示。

代碼清單10

    private class PrivilegedDoLoad
        implements PrivilegedExceptionAction<Void> {

        PrivilegedDoLoad() {
            // NOOP
        }

        public Void run() throws Exception{
           doLoad();
           return null;
        }
    }

從代碼清單10看到實際負責加載的方法是doLoad,根據代碼清單9知道默認狀況下,加載Session信息的方法也是doLoad。因此咱們只須要看看doLoad的實現了,見代碼清單11。

代碼清單11

    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));
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        ObjectInputStream ois = null;
        Loader loader = null;
        ClassLoader classLoader = null;
        try {
            fis = new FileInputStream(file.getAbsolutePath());
            bis = new BufferedInputStream(fis);
            if (container != null)
                loader = container.getLoader();
            if (loader != null)
                classLoader = loader.getClassLoader();
            if (classLoader != null) {
                if (log.isDebugEnabled())
                    log.debug("Creating custom object input stream for class loader ");
                ois = new CustomObjectInputStream(bis, classLoader);
            } else {
                if (log.isDebugEnabled())
                    log.debug("Creating standard object input stream");
                ois = new ObjectInputStream(bis);
            }
        } catch (FileNotFoundException e) {
            if (log.isDebugEnabled())
                log.debug("No persisted data file found");
            return;
        } catch (IOException e) {
            log.error(sm.getString("standardManager.loading.ioe", e), e);
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException f) {
                    // Ignore
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException f) {
                    // Ignore
                }
            }
            throw e;
        }

        // Load the previously unloaded active sessions
        synchronized (sessions) {
            try {
                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.
                        session.setValid(true);
                        session.expire();
                    }
                    sessionCounter++;
                }
            } catch (ClassNotFoundException e) {
                log.error(sm.getString("standardManager.loading.cnfe", e), e);
                try {
                    ois.close();
                } catch (IOException f) {
                    // Ignore
                }
                throw e;
            } catch (IOException e) {
                log.error(sm.getString("standardManager.loading.ioe", e), e);
                try {
                    ois.close();
                } catch (IOException f) {
                    // Ignore
                }
                throw e;
            } finally {
                // Close the input stream
                try {
                    ois.close();
                } catch (IOException f) {
                    // ignored
                }

                // Delete the persistent storage file
                if (file.exists() )
                    file.delete();
            }
        }

        if (log.isDebugEnabled())
            log.debug("Finish: Loading persisted sessions");
    }

 從代碼清單11看到StandardManager的doLoad方法的執行步驟以下:

  1. 清空sessions緩存維護的Session信息;
  2. 調用file方法返回當前Context下的Session持久化文件,好比:D:\workspace\Tomcat7.0\work\Catalina\localhost\host-manager\SESSIONS.ser;
  3. 打開Session持久化文件的輸入流,並封裝爲CustomObjectInputStream;
  4. 從Session持久化文件讀入持久化的Session的數量,而後逐個讀取Session信息並放入sessions緩存中。

至此,有關StandardManager的啓動就介紹到這裏,我將會在《TOMCAT源碼分析——SESSION管理分析(下)》一文講解Session的分配、追蹤、銷燬等內容。

如需轉載,請標明本文做者及出處——做者:jiaan.gja,本文原創首發:博客園,原文連接:http://www.cnblogs.com/jiaan-geng/p/4913616.html 
相關文章
相關標籤/搜索