對於廣大java開發者而已,對於J2EE規範中的Session應該並不陌生,咱們可使用Session管理用戶的會話信息,最多見的就是拿Session用來存放用戶登陸、身份、權限及狀態等信息。對於使用Tomcat做爲Web容器的大部分開發人員而言,Tomcat是如何實現Session標記用戶和管理Session信息的呢?html
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提供了兩種同步策略:
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管理的執行步驟以下:
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的步驟以下:
通過上面的分析,咱們知道啓動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方法的執行步驟:
注意:此處調用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的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方法的執行步驟以下:
至此,有關StandardManager的啓動就介紹到這裏,我將會在《TOMCAT源碼分析——SESSION管理分析(下)》一文講解Session的分配、追蹤、銷燬等內容。
如需轉載,請標明本文做者及出處——做者:jiaan.gja,本文原創首發:博客園,原文連接:http://www.cnblogs.com/jiaan-geng/p/4913616.html