我有一个项目需要模拟HttpSession,在参考Tomcat的HttpSession管理时有一点心得,在这里记录一下。

先说说这几个关键类:

  1. org.apache.catalina.session.StandardManager: 管理Session的类

  2. org.apache.catalina.session.StandardSession: HttpSession的实现

  3. 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的调用链是这样的:

  1. ContainerBase里有一个ContainerBackgroundProcessor线程实例,
    这个线程会每隔ContainerBase.backgroundProcessorDelay的时间调用-->

  2. ContainerBase.processChildren,这个方法调用-->

  3. ContainerBase.backgroundProcess,这个方法调用-->

  4. StandardManager.backgroundProcess,这个方法调用-->

  5. 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()方法时,发生了这些事情:

  1. Request.doGetSession()

  2. StandardSession.access()

  3. 返回给用户Session

  4. 用户在Servlet里处理完请求

  5. Request.recycle()

  6. StandardSession.endAccess()

陷阱

从上面的流程可以看出Tomcat假设在Request的生命周期结束之后便不会有人再去访问Session了。

但是如果我们在处理Request的Thread A里另起一个Thread B,并且在Thread B里访问Session时会怎样呢?

你可能已经猜到,可能会访问到一个已经过期的Session。下面是一个小小的测试代码:

https://gist.github.com/chanjarster/e1793251477cbabfbe92


chanjarster
4.2k 声望244 粉丝