欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

深入浅析TomCat Session管理分析

程序员文章站 2024-03-06 16:04:50
前言   对于广大java开发者而已,对于j2ee规范中的session应该并不陌生,我们可以使用session管理用户的会话信息,最常见的就是拿session用来存放用...

前言

  对于广大java开发者而已,对于j2ee规范中的session应该并不陌生,我们可以使用session管理用户的会话信息,最常见的就是拿session用来存放用户登录、身份、权限及状态等信息。对于使用tomcat作为web容器的大部分开发人员而言,tomcat是如何实现session标记用户和管理session信息的呢?

概要

session

  tomcat内部定义了session和httpsession这两个会话相关的接口,其类继承体系如图1所示。

深入浅析TomCat Session管理分析

图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所示。

深入浅析TomCat Session管理分析

图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的分配、追踪、销毁等内容。