国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Java > 正文

深入淺析TomCat Session管理分析

2019-11-26 14:53:27
字體:
來源:轉載
供稿:網友

前言

  對于廣大java開發者而已,對于J2EE規范中的Session應該并不陌生,我們可以使用Session管理用戶的會話信息,最常見的就是拿Session用來存放用戶登錄、身份、權限及狀態等信息。對于使用Tomcat作為Web容器的大部分開發人員而言,Tomcat是如何實現Session標記用戶和管理Session信息的呢?

概要

SESSION

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

圖1  Session類繼承體系

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

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管理的執行步驟如下:

創建StandardManager;

如果Tomcat結合Apache做了分布式部署,會將當前StandardManager注冊到集群中;
啟動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的步驟如下:

調用init方法初始化StandardManager;

調用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方法的執行步驟:

將容器自身即StandardManager注冊到JMX(LifecycleMBeanBase的initInternal方法的實現請參考《Tomcat源碼分析――生命周期管理》一文);

從父容器StandardContext中獲取當前Tomcat是否是集群部署,并設置為ManagerBase的布爾屬性distributable;
調用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中的setRandomFile方法(見代碼清單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)生成新的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方法的執行步驟如下:

清空sessions緩存維護的Session信息;

調用file方法返回當前Context下的Session持久化文件,比如:D:/workspace/Tomcat7.0/work/Catalina/localhost/host-manager/SESSIONS.ser;

打開Session持久化文件的輸入流,并封裝為CustomObjectInputStream;

從Session持久化文件讀入持久化的Session的數量,然后逐個讀取Session信息并放入sessions緩存中。

至此,有關StandardManager的啟動就介紹到這里,我將會在下篇內容講解Session的分配、追蹤、銷毀等內容。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 汉源县| 峨边| 富民县| 临漳县| 漳平市| 宜良县| 东方市| 个旧市| 孝感市| 高唐县| 神木县| 仪陇县| 青浦区| 黔西县| 大城县| 云梦县| 普格县| 民丰县| 泰州市| 三河市| 达拉特旗| 洪湖市| 阳江市| 稻城县| 永定县| 宁强县| 额尔古纳市| 临澧县| 会昌县| 西贡区| 阿坝| 定兴县| 濉溪县| 涟水县| 博野县| 建瓯市| 柳河县| 卢龙县| 青神县| 赤城县| 青川县|