深入浅析TomCat Session管理分析
前言
对于广大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的分配、追踪、销毁等内容。
推荐阅读
-
深入浅析TomCat Session管理分析
-
Hibernate管理Session和批量操作分析
-
【转 Tomcat集群session管理解决方案(关于sticky session、session replication与使用memcached缓存sess tomcatmemcachedsticky sessionnon sticky session
-
深入分析iOS应用中对于图片缓存的管理和使用
-
深入分析iOS应用中对于图片缓存的管理和使用
-
Shiro权限管理框架(四):深入分析Shiro中的Session管理
-
SEO 网站管理员工具深入分析
-
深入分析Tomcat无响应问题及解决方法
-
深入浅析Oracle数据库管理之创建和删除数据库
-
Tomcat中的session是如何管理的?