我有一个项目需要模拟 HttpSession
,在参考Tomcat的 HttpSession
管理时有一点心得,在这里记录一下。
先说说这几个关键类:
org.apache.catalina.session.StandardManager
: 管理Session的类
org.apache.catalina.session.StandardSession
: HttpSession
的实现
org.apache.catalina.connector.Request
: HttpServletRequest
的实现
下面介绍一下和Session相关的几个关键属性,以及方法
每隔多少次 StandardManager.backgroundProcess
做一次session清理,数字越小越频繁,默认6次。
下面是源代码片段:
/** * Frequency of the session expiration, and related manager operations. * Manager operations will be done once for the specified amount of * backgrondProcess calls (ie, the lower the amount, the most often the * checks will occur). */ protected int processExpiresFrequency = 6;
StandardManager.backgroundProcess
的调用链是这样的:
ContainerBase
里有一个 ContainerBackgroundProcessor
线程实例,
这个线程会每隔 ContainerBase.backgroundProcessorDelay
的时间调用-->
ContainerBase.processChildren
,这个方法调用-->
ContainerBase.backgroundProcess
,这个方法调用-->
StandardManager.backgroundProcess
,这个方法调用-->
StandardManager.processExpires
,在这里清理掉已经过期的Session。
一个session不被访问的时间间隔,默认30分钟(1800秒)。
下面是源代码片段:
/** * The default maximum inactive interval for Sessions created by * this Manager. */ protected int maxInactiveInterval = 30 * 60;
StandardManager.maxInactiveInterval
的值会作为新Session的默认 maxInactiveInterval
的值
(实际上用户在get到session后修改这个值)。
下面是代码片段:
public Session createSession(String sessionId) { // ... // Recycle or create a Session instance Session session = createEmptySession(); // Initialize the properties of the new session and return it session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(this.maxInactiveInterval); // ... }
StandardSession.access
方法是用来设置这个Session被访问的时间的,何时被调用会在 Request
里讲。
下面是代码片段:
/** * Update the accessed time information for this session. This method * should be called by the context when a request comes in for a particular * session, even if the application does not reference it. */ @Override public void access() { this.thisAccessedTime = System.currentTimeMillis(); if (ACTIVITY_CHECK) { accessCount.incrementAndGet(); } }
StandardSession.endAccess
方法是用来设置这个Session访问结束的时间的,何时被调用会在 Request
里讲。
/** * End the access. */ @Override public void endAccess() { isNew = false; /** * The servlet spec mandates to ignore request handling time * in lastAccessedTime. */ if (LAST_ACCESS_AT_START) { this.lastAccessedTime = this.thisAccessedTime; this.thisAccessedTime = System.currentTimeMillis(); } else { this.thisAccessedTime = System.currentTimeMillis(); this.lastAccessedTime = this.thisAccessedTime; } if (ACTIVITY_CHECK) { accessCount.decrementAndGet(); } }
StandardSession.isValid
方法是很关键的,这个方法会用来判断这个Session是否还处于有效状态。
代码片段:
/** * Return the <code>isValid</code> flag for this session. */ @Override public boolean isValid() { if (!this.isValid) { return false; } if (this.expiring) { return true; } if (ACTIVITY_CHECK && accessCount.get() > 0) { return true; } if (maxInactiveInterval > 0) { long timeNow = System.currentTimeMillis(); int timeIdle; if (LAST_ACCESS_AT_START) { timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L); } else { timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); } if (timeIdle >= maxInactiveInterval) { expire(true); } } return this.isValid; }
ACTIVITY_CHECK
,的意思是判断session是否过期前,是否要先判断一下这个session是否还在使用中(用 accessCount
判断)
如果是,那么这个session是不会过期的。
如果不是,那么这个session就会被“粗暴”地过期。
LAST_ACCESS_AT_START
,是两种判断session过期方式的开关
如果为true,会根据 getSession
的时间判断是否过期了。 access()
和 endAccess()
之间的时间是不算进去的。
如果为false,则根据session结束访问的时间判断是否过期了。 access()
和 endAccess()
之间的时间是算进去的。
这个方法是tomcat获得session的地方,从下面的代码判断里可以看到,它会调用 StandardSession.access()
方法:
protected Session doGetSession(boolean create) { // There cannot be a session if no context has been assigned yet if (context == null) { return (null); } // Return the current session if it exists and is valid if ((session != null) && !session.isValid()) { session = null; } if (session != null) { return (session); } // Return the requested session if it exists and is valid Manager manager = null; if (context != null) { manager = context.getManager(); } if (manager == null) { return (null); // Sessions are not supported } if (requestedSessionId != null) { try { session = manager.findSession(requestedSessionId); } catch (IOException e) { session = null; } if ((session != null) && !session.isValid()) { session = null; } if (session != null) { // 在这里调用了access session.access(); return (session); } } // ... }
这个当一个请求处理完毕后, CoyoteAdapter
会调用 Request.recycle()
方法,
而这个方法会调用 StandardSession.endAccess()
方法(也就是告诉Session,你的这次访问结束了)
/** * Release all object references, and initialize instance variables, in * preparation for reuse of this object. */ public void recycle() { // ... if (session != null) { try { session.endAccess(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.warn(sm.getString("coyoteRequest.sessionEndAccessFail"), t); } } // ... }
所以,当用户调用 HttpSession.getSession()
方法时,发生了这些事情:
Request.doGetSession()
StandardSession.access()
返回给用户Session
用户在Servlet里处理完请求
Request.recycle()
StandardSession.endAccess()
从上面的流程可以看出Tomcat假设在Request的生命周期结束之后便不会有人再去访问Session了。
但是如果我们在处理Request的Thread A里另起一个Thread B,并且在Thread B里访问Session时会怎样呢?
你可能已经猜到,可能会访问到一个已经过期的Session。下面是一个小小的测试代码:
https://gist.github.com/chanjarster/e1793251477cbabfbe92