我有一个项目需要模拟HttpSession
,在参考Tomcat的HttpSession
管理时有一点心得,在这里记录一下。
先说说这几个关键类:
org.apache.catalina.session.StandardManager
: 管理Session的类org.apache.catalina.session.StandardSession
:HttpSession
的实现org.apache.catalina.connector.Request
:HttpServletRequest
的实现
StandardManager
下面介绍一下和Session相关的几个关键属性,以及方法
processExpiresFrequency
每隔多少次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。
maxInactiveInterval
一个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()
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();
}
}
endAccess()
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();
}
}
isValid()
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()
之间的时间是算进去的。
Request
doGetSession()
这个方法是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);
}
}
// ...
}
recycle()
这个当一个请求处理完毕后,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。下面是一个小小的测试代码: