欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
1. Tomcat Session 概述
首先 HTTP 是一个无状态的协议, 这意味着每次发起的HTTP请求, 都是一个全新的请求(与上个请求没有任何联系, 服务端不会保留上个请求的任何信息), 而 Session 的出现就是为了解决这个问题, 将 Client 端的每次请求都关联起来, 要实现 Session 机制 通常通过 Cookie(cookie 里面保存统一标识符号), URI 附加参数, 或者就是SSL (就是SSL 中的各种属性作为一个Client请求的唯一标识), 而在初始化 ApplicationContext 指定默认的Session追踪机制(URL + COOKIE), 若 Connector 配置了 SSLEnabled, 则将通过 SSL 追踪Session的模式也加入追踪机制里面 (将 ApplicationContext.populateSessionTrackingModes()方法)
2. Cookie 概述
Cookie 是在Http传输中存在于Header中的一小撮文本信息(KV), 每次浏览器都会将服务端发送给自己的Cookie信息返回发送给服务端(PS: Cookie的内容存储在浏览器端); 有了这种技术服务端就知道这次请求是谁发送过来的(比如我们这里的Session, 就是基于在Http传输中, 在Cookie里面加入一个全局唯一的标识符号JsessionId来区分是哪个用户的请求)。
3. Tomcat 中 Cookie 的解析
在 Tomcat 9.0.5 中 Cookie 的解析是通过内部的函数 processCookies() 来进行操作的(其实就是将Http header 的内容直接赋值给 Cookie 对象, Cookie在Header中找name是"Cookie"的数据, 拿出来进行解析), 我们这里主要从 jsessionid 的角度来看一下整个过程是如何触发的, 我们直接看函数 CoyoteAdapter.postParseRequest() 中解析 jsessionId那部分源码:
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
...
String sessionID;
// (5)尝试从请求的URL、Cookie、SSL会话获取请求会话D,并将mapRequired设置为false(当
//第(3)步执行成功后,默认不再执行循环,是否需要重新执行由后续步骤确定)。
// 是否支持通过 URI 尾缀 JSessionId 的方式来追踪 Session 的变化 (默认是支持的)
if (request.getServletContext().getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.URL)) {
// Get the session ID if there was one
// 从 URI 尾缀的参数中拿取 jsessionId 的数据 (SessionConfig.getSessionUriParamName 是获取对应cookie的名字,
// 默认 jsessionId, 可以在 web.xml 里面进行定义)
sessionID = request.getPathParameter(
SessionConfig.getSessionUriParamName(
request.getContext()));
if (sessionID != null) {
// 若从 URI 里面拿取了 jsessionId, 则直接进行赋值给 request
request.setRequestedSessionId(sessionID);
request.setRequestedSessionURL(true);
}
}
// Look for session ID in cookies and SSL session
try {
// 通过 cookie 里面获取 JSessionId 的值
parseSessionCookiesId(request);
} catch (IllegalArgumentException e) {
// Too many cookies
if (!response.isError()) {
response.setError();
response.sendError(400);
}
return true;
}
// 在 SSL 模式下获取 JSessionId 的值
parseSessionSslId(request);
sessionID = request.getRequestedSessionId();
...
}
protected void parseSessionCookiesId(Request request) {
// If session tracking via cookies has been disabled for the current
// context, don't go looking for a session ID in a cookie as a cookie
// from a parent context with a session ID may be present which would
// overwrite the valid session ID encoded in the URL
Context context = request.getMappingData().context;
// Tomcat 是否支持 通过 cookie 机制 跟踪 session
if (context != null && !context.getServletContext()
.getEffectiveSessionTrackingModes().contains(
SessionTrackingMode.COOKIE)) {
return;
}
// Parse session id from cookies
// 解析cookie, 获取 Cookie的实际引用对象 (PS: 这里还没有触发 Cookie 解析, 也就是 serverCookies 里面是空数据,
// 数据还只是存储在 http header 里面)
ServerCookies serverCookies = request.getServerCookies();
int count = serverCookies.getCookieCount();
if (count <= 0) {
return;
}
// 获取 sessionId 的名称 JSessionId
String sessionCookieName = SessionConfig.getSessionCookieName(context);
for (int i = 0; i < count; i++) {
// 轮询所有解析出来的 Cookie
ServerCookie scookie = serverCookies.getCookie(i);
// 比较 Cookie 的名称是否是 jsessionId
if (scookie.getName().equals(sessionCookieName)) {
// Override anything requested in the URL
// 是否 jsessionId 还没有解析 (并且只将第一个解析成功的值 set 进去)
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
// 将MessageBytes转成 char
convertMB(scookie.getValue());
// 设置 jsessionId 的值
request.setRequestedSessionId
(scookie.getValue().toString());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
if (log.isDebugEnabled()) {
log.debug(" Requested cookie session id is " +
request.getRequestedSessionId());
}
} else {
// 若 Cookie 里面存在好几个 jsessionid, 则进行覆盖 set 值
if (!request.isRequestedSessionIdValid()) {
// Replace the session id until one is valid
convertMB(scookie.getValue());
request.setRequestedSessionId
(scookie.getValue().toString());
}
}
}
}
}
上面的步骤其实就是依次从 URI, Cookie, SSL 里面进行 jsessionId 的解析, 其中从Cookie里面进行解析是最常用的, 而且 就这个Tomcat版本里面, 从cookie里面解析 jsessionid 藏得比较深, 是由request.getServerCookies() 来进行触发的, 整个解析的过程其实就是将线程 header 里面的数据依次遍历, 找到 name="Cookie"的数据,拿出来解析字符串(这里就不再叙述了); 程序到这里其实若客户端传 jsessionId 的话, 则服务端已经将其解析出来, 并且set到Request对象里面了, 但是 Session 对象还没有触发创建, 最多也就是查找一下 jsessionId 对应的 Session 在 Manager 里面是否存在。
4. tomcat session 设计分析
tomcat session 组件图如下所示,其中 Context 对应一个 webapp 应用,每个 webapp 有多个 HttpSessionListener, 并且每个应用的 session 是独立管理的,而 session 的创建、销毁由 Manager 组件完成,它内部维护了 N 个 Session 实例对象。在前面的文章中,我们分析了 Context 组件,它的默认实现是 StandardContext,它与 Manager 是一对一的关系,Manager 创建、销毁会话时,需要借助 StandardContext 获取 HttpSessionListener 列表并进行事件通知,而 StandardContext 的后台线程会对 Manager 进行过期 Session 的清理工作。
org.apache.catalina.Manager 接口的主要方法如下所示,它提供了 Context、org.apache.catalina.SessionIdGenerator的 getter/setter 接口,以及创建、添加、移除、查找、遍历 Session 的 API 接口,此外还提供了 Session 持久化的接口(load/unload) 用于加载/卸载会话信息,当然持久化要看不同的实现类。
public interface Manager {
public Context getContext();
public void setContext(Context context);
public SessionIdGenerator getSessionIdGenerator();
public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
public void add(Session session);
public void addPropertyChangeListener(PropertyChangeListener listener);
public void changeSessionId(Session session);
public void changeSessionId(Session session, String newId);
public Session createEmptySession();
public Session createSession(String sessionId);
public Session findSession(String id) throws IOException;
public Session[] findSessions();
public void remove(Session session);
public void remove(Session session, boolean update);
public void removePropertyChangeListener(PropertyChangeListener listener);
public void unload() throws IOException;
public void backgroundProcess();
public boolean willAttributeDistribute(String name, Object value);
}
tomcat9 提供了 4 种实现,默认使用 StandardManager,tomcat 还提供了集群会话的解决方案,但是在实际项目中很少运用
-
StandardManager:Manager 默认实现,在内存中管理 session,宕机将导致 session 丢失;但是当调用 Lifecycle 的 start/stop 接口时,将采用 jdk 序列化保存 Session 信息,因此当 tomcat 发现某个应用的文件有变更进行 reload 操作时,这种情况下不会丢失 Session -
DeltaManager:增量 Session 管理器,用于Tomcat集群的会话管理器,某个节点变更 Session 信息都会同步到集群中的所有节点,这样可以保证 Session 信息的实时性,但是这样会带来较大的网络开销 -
BackupManager:用于 Tomcat 集群的会话管理器,与DeltaManager不同的是,某个节点变更 Session 信息的改变只会同步给集群中的另一个 backup 节点 -
PersistentManager:当会话长时间空闲时,将会把 Session 信息写入磁盘,从而限制内存中的活动会话数量;此外,它还支持容错,会定期将内存中的 Session 信息备份到磁盘
我们来看下 StandardManager 的类图,它也是个 Lifecycle 组件,并且 ManagerBase 实现了主要的逻辑。
5. Tomcat 中 Session 的创建
经过上面的Cookie解析, 则若存在jsessionId的话, 则已经set到Request里面了, 那Session又是何时触发创建的呢? 主要还是代码 request.getSession() , 看代码:
public class SessionExample extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
...
HttpSession session = request.getSession();
...
}
}
我们来看看Request.getSession():
// 获取 request 对应的session
@Override
public HttpSession getSession(boolean create) {
// 通过managerBase.sessions 获取Session
Session session = doGetSession(create);
if (session == null) {
return null;
}
return session.getSession();
}
/**
* @param create 是否创建StandardSesion
*/
protected Session doGetSession(boolean create) {
// There cannot be a session if no context has been assigned yet
// 1. 检验 StandardContext
Context context = getContext();
if (context == null) {
return null;
}
// Return the current session if it exists and is valid
// 2. 校验 Session 的有效性
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return session;
}
// Return the requested session if it exists and is valid
// 获取StandardContext中对应的standardManager,Context与Manager是一对一的关系
Manager manager = context.getManager();
if (manager == null) {
return null; // Sessions are not supported
}
if (requestedSessionId != null) {
try {
// 3. 通过managerBase.sessions 获取Session
// 4. 通过客户端 的sessionId 从managerBase.sessions 来获取Session对象
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
} else {
log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
}
session = null;
}
// 5. 判断Session 是否有效
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
// 6. accessCount +1
session.access();
return session;
}
}
// Create a new session if requested and the response is not committed
// 7. 根据标识是否创建 StandardSession ( false 直接返回)
if (!create) {
return null;
}
// 当前的 Context 是否支持通过 cookie 的方式来追踪 Session
boolean trackModesIncludesCookie =
context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// Re-use session IDs provided by the client in very limited
// circumstances.
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
// If the session ID has been obtained from the SSL handshake then
// use it.
// 8. 到这里其实是没有找到 session, 直接创建 Session 出来
} else if (("/".equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
/* This is the common(ish) use case: using the same session ID with
* multiple web applications on the same host. Typically this is
* used by Portlet implementations. It only works if sessions are
* tracked via cookies. The cookie must have a path of "/" else it
* won't be provided for requests to all web applications.
*
* Any session ID provided by the client should be for a session
* that already exists somewhere on the host. Check if the context
* is configured for this to be confirmed.
*/
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
// Ignore. Problems with this manager will be
// handled elsewhere.
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
// 9. 从客户端读取 sessionID, 并且根据这个 sessionId 创建
session = manager.createSession(sessionId);
// Creating a new session cookie based on that session
if (session != null && trackModesIncludesCookie) {
// 10. 根据 sessionId 来创建一个 Cookie
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
// 11. 最后在响应体中写入 cookie
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
// 12. session access 计数器 + 1
session.access();
return session;
}
我们看看 manager.createSession(null);
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
//Manager管理着当前Context的所有session
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
@Override
public Session findSession(String id) throws IOException {
if (id == null) {
return null;
}
//通过JssionId获取session
return sessions.get(id);
}
public Session createSession(String sessionId) {
// 1. 判断 单节点的 Session 个数是否超过限制
if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
// Recycle or create a Session instance
// 创建一个 空的 session
// 2. 创建 Session
Session session = createEmptySession();
// Initialize the properties of the new session and return it
// 初始化空 session 的属性
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
// 3. StandardSession 最大的默认 Session 激活时间
session.setMaxInactiveInterval(this.maxInactiveInterval);
String id = sessionId;
// 若没有从 client 端读取到 jsessionId
if (id == null) {
// 4. 生成 sessionId (这里通过随机数来生成)
id = generateSessionId();
}
//这里会将session存入Map<String, Session> sessions = new ConcurrentHashMap<>();
session.setId(id);
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
// 5. 每次创建 Session 都会创建一个 SessionTiming, 并且 push 到 链表 sessionCreationTiming 的最后
sessionCreationTiming.add(timing);
// 6. 并且将 链表 最前面的节点删除
sessionCreationTiming.poll();
}
// 那这个 sessionCreationTiming 是什么作用呢, 其实 sessionCreationTiming 是用来统计 Session的新建及失效的频率 (好像Zookeeper 里面也有这个的统计方式)
return (session);
}
@Override
public void add(Session session) {
//将创建的Seesion存入Map<String, Session> sessions = new ConcurrentHashMap<>();
sessions.put(session.getIdInternal(), session);
int size = getActiveSessions();
if( size > maxActive ) {
synchronized(maxActiveUpdateLock) {
if( size > maxActive ) {
maxActive = size;
}
}
}
}
}
@Override
public void setId(String id) {
setId(id, true);
}
@Override
public void setId(String id, boolean notify) {
if ((this.id != null) && (manager != null))
manager.remove(this);
this.id = id;
if (manager != null)
manager.add(this);
if (notify) {
tellNew();
}
}
其主要的步骤就是:
-
若 request.Session != null, 则直接返回 (说明同一时刻之前有其他线程创建了Session, 并且赋值给了 request)
-
若 requestedSessionId != null, 则直接通过 manager 来进行查找一下, 并且判断是否有效
-
调用 manager.createSession 来创建对应的Session,并将Session存入Manager的Map中
-
根据 SessionId 来创建 Cookie, 并且将 Cookie 放到 Response 里面
-
直接返回 Session
6. Session清理
6.1 Background 线程
前面我们分析了 Session 的创建过程,而 Session 会话是有时效性的,下面我们来看下 tomcat 是如何进行失效检查的。在分析之前,我们先回顾下 Container 容器的 Background 线程。
tomcat 所有容器组件,都是继承至 ContainerBase 的,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,而 ContainerBase 在启动的时候,如果 backgroundProcessorDelay 参数大于 0 则会开启 ContainerBackgroundProcessor 后台线程,调用自己以及子容器的 backgroundProcess 进行一些后台逻辑的处理,和 Lifecycle 一样,这个动作是具有传递性的,也就
ContainerBase.java关键代码如下所示:
protected synchronized void startInternal() throws LifecycleException {
...
// Start our thread
// 启动层级的后台任务处理,包括Cluster后台任务处理(包括部署变更检测,心跳),Realm后台任务处理,
// Pipeline中Value的后台任务处理
if (backgroundProcessorDelay > 0) {
monitorFuture = Container.getService(ContainerBase.this).getServer()
.getUtilityExecutor().scheduleWithFixedDelay(
new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
}
}
protected class ContainerBackgroundProcessorMonitor implements Runnable {
@Override
public void run() {
if (getState().isAvailable()) {
threadStart();
}
}
}
protected void threadStart() {
// 开启ContainerBackgroundProcessor线程用于处理子容器,默认情况下backgroundProcessorDelay=-1,不会启用该线程
if (backgroundProcessorDelay > 0
&& (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
&& (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
// There was an error executing the scheduled task, get it and log it
try {
backgroundProcessorFuture.get();
} catch (InterruptedException | ExecutionException e) {
log.error(sm.getString("containerBase.backgroundProcess.error"), e);
}
}
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
.scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
backgroundProcessorDelay, backgroundProcessorDelay,
TimeUnit.SECONDS);
}
}
接下来看ContainerBackgroundProcessor 类:
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
processChildren(ContainerBase.this);
}
protected void processChildren(Container container) {
ClassLoader originalClassLoader = null;
try {
if (container instanceof Context) {
Loader loader = ((Context) container).getLoader();
// Loader will be null for FailedContext instances
if (loader == null) {
return;
}
// Ensure background processing for Contexts and Wrappers
// is performed under the web app's class loader
originalClassLoader = ((Context) container).bind(false, null);
}
// todo
container.backgroundProcess();
Container[] children = container.findChildren();
for (Container child : children) {
// 如果子容器的 backgroundProcessorDelay 参数小于0,则递归处理子容器
// 因为如果该值大于0,说明子容器自己开启了线程处理,因此父容器不需要再做处理
if (child.getBackgroundProcessorDelay() <= 0) {
processChildren(child);
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("containerBase.backgroundProcess.error"), t);
} finally {
if (container instanceof Context) {
((Context) container).unbind(false, originalClassLoader);
}
}
}
}
6.2 Session 检查
backgroundProcessorDelay 参数默认值为 -1,单位为秒,即默认不启用后台线程,而 tomcat 的 Container 容器需要开启线程处理一些后台任务,比如监听 jsp 变更、tomcat 配置变动、Session 过期等等,因此 StandardEngine 在构造方法中便将 backgroundProcessorDelay 参数设为 10(当然可以在 server.xml 中指定该参数),即每隔 10s 执行一次。那么这个线程怎么控制生命周期呢?我们注意到 ContainerBase 有个 threadDone 变量,用 volatile 修饰,如果调用 Container 容器的 stop 方法该值便会赋值为 false,那么该后台线程也会退出循环,从而结束生命周期。另外,有个地方需要注意下,父容器在处理子容器的后台任务时,需要判断子容器的 backgroundProcessorDelay 值,只有当其小于等于 0 才进行处理,因为如果该值大于0,子容器自己会开启线程自行处理,这时候父容器就不需要再做处理了
前面分析了容器的后台线程是如何调度的,下面我们重点来看看 webapp 这一层,以及 StandardManager 是如何清理过期会话的。StandardContext 重写了 backgroundProcess 方法,除了对子容器进行处理之外,还会对一些缓存信息进行清理,StandardContext.java关键代码如下所示:
@Override
public void backgroundProcess() {
if (!getState().isAvailable())
return;
// 热加载 class,或者 jsp
Loader loader = getLoader();
if (loader != null) {
loader.backgroundProcess();
}
// 清理过期Session
Manager manager = getManager();
if (manager != null) {
manager.backgroundProcess();
}
// 清理资源文件的缓存
WebResourceRoot resources = getResources();
if (resources != null) {
resources.backgroundProcess();
}
// 清理对象或class信息缓存
InstanceManager instanceManager = getInstanceManager();
if (instanceManager instanceof DefaultInstanceManager) {
((DefaultInstanceManager)instanceManager).backgroundProcess();
}
// 调用子容器的 backgroundProcess 任务
super.backgroundProcess();
}
StandardContext 重写了 backgroundProcess 方法,在调用子容器的后台任务之前,还会调用 Loader、Manager、WebResourceRoot、InstanceManager 的后台任务,这里我们只关心 Manager 的后台任务。弄清楚了 StandardManager 的来龙去脉之后,我们接下来分析下具体的逻辑。
StandardManager 继承至 ManagerBase,它实现了主要的逻辑,关于 Session 清理的代码如下所示。backgroundProcess 默认是每隔10s调用一次,但是在 ManagerBase 做了取模处理,默认情况下是 60s 进行一次 Session 清理。tomcat 对 Session 的清理并没有引入时间轮,因为对 Session 的时效性要求没有那么精确,而且除了通知 SessionListener。ManagerBase.java代码:
public void backgroundProcess() {
// processExpiresFrequency 默认值为 6,而backgroundProcess默认每隔10s调用一次,也就是说除了任务执行的耗时,每隔 60s 执行一次
count = (count + 1) % processExpiresFrequency;
if (count == 0) // 默认每隔 60s 执行一次 Session 清理
processExpires();
}
/**
* 单线程处理,不存在线程安全问题
*/
public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions(); // 获取所有的 Session
int expireHere = 0 ;
for (int i = 0; i < sessions.length; i++) {
// Session 的过期是在 isValid() 里面处理的
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
// 记录下处理时间
processingTime += ( timeEnd - timeNow );
}
6.3 清理过期 Session
在上面的代码,我们并没有看到太多的过期处理,只是调用了 sessions[i].isValid(),原来清理动作都在这个方法里面处理的,相当的隐晦。在 StandardSession#isValid() 方法中,如果 now - thisAccessedTime >= maxInactiveInterval则判定当前 Session 过期了,而这个 thisAccessedTime 参数在每次访问都会进行更新。
public boolean isValid() {
// other code......
// 如果指定了最大不活跃时间,才会进行清理,这个时间是 Context.getSessionTimeout(),默认是30分钟
if (maxInactiveInterval > 0) {
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}
return this.isValid;
}
而 expire 方法处理的逻辑较繁锁,下面我用伪代码简单地描述下核心的逻辑,由于这个步骤可能会有多线程进行操作,因此使用 synchronized 对当前 Session 对象加锁,还做了双重校验,避免重复处理过期 Session。它还会向 Container 容器发出事件通知,还会调用 HttpSessionListener 进行事件通知,这个也就是我们 web 应用开发的 HttpSessionListener 了。由于 Manager 中维护了 Session 对象,因此还要将其从 Manager 移除。Session 最重要的功能就是存储数据了,可能存在强引用,而导致 Session 无法被 gc 回收,因此还要移除内部的 key/value 数据。由此可见,tomcat 编码的严谨性了,稍有不慎将可能出现并发问题,以及出现内存泄露。StandardSession.expire()源码:
public void expire(boolean notify) {
// Check to see if session has already been invalidated.
// Do not check expiring at this point as expire should not return until
// isValid is false
if (!isValid) {
return;
}
// 1. 加锁
synchronized (this) {
// Check again, now we are inside the sync so this code only runs once
// Double check locking - isValid needs to be volatile
// The check of expiring is to ensure that an infinite loop is not
// entered as per bug 56339
if (expiring || !isValid) {
return;
}
if (manager == null) {
return;
}
// Mark this session as "being expired"
expiring = true;
// Notify interested application event listeners
// FIXME - Assumes we call listeners in reverse order
// 2、双重校验 isValid 值,避免并发问题
Context context = manager.getContext();
// The call to expire() may not have been triggered by the webapp.
// Make sure the webapp's class loader is set when calling the
// listeners
if (notify) {
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
//3、判断是否为 HttpSessionListener,不是则继续循环
if (!(listeners[j] instanceof HttpSessionListener)) {
continue;
}
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
//4、向容器发出Destory事件,并调用 HttpSessionListener.sessionDestroyed() 进行通知
context.fireContainerEvent("beforeSessionDestroyed",
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent("afterSessionDestroyed",
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
"afterSessionDestroyed", listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString("standardSession.sessionEvent"), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
if (ACTIVITY_CHECK) {
accessCount.set(0);
}
// Remove this session from our manager's active sessions
//5、从 manager 中移除该 session
manager.remove(this, true);
// Notify interested session event listeners
//6、向 tomcat 的 SessionListener 发出事件通知,非 HttpSessionListener
if (notify) {
fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
}
// Call the logout method
if (principal instanceof TomcatPrincipal) {
TomcatPrincipal gp = (TomcatPrincipal) principal;
try {
gp.logout();
} catch (Exception e) {
manager.getContext().getLogger().error(
sm.getString("standardSession.logoutfail"),
e);
}
}
// We have completed expire of this session
setValid(false);
expiring = false;
// Unbind any objects associated with this session
//7、清除内部的 key/value,避免因为强引用而导致无法回收 Session 对象
String keys[] = keys();
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
for (String key : keys) {
removeAttributeInternal(key, notify);
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
}
由前面的分析可知,tomcat 会根据时间戳清理过期 Session,那么 tomcat 又是如何更新这个时间戳呢? tomcat 在处理完请求之后,会对 Request 对象进行回收,并且会对 Session 信息进行清理,而这个时候会更新 thisAccessedTime、lastAccessedTime 时间戳。此外,我们通过调用 request.getSession() 这个 API 时,在返回 Session 时会调用 Session#access() 方法,也会更新 thisAccessedTime 时间戳。这样一来,每次请求都会更新时间戳,可以保证 Session 的鲜活时间。
Request.recycleSessionInfo()如下:
protected void recycleSessionInfo() {
if (session != null) {
session.endAccess(); // 更新时间戳
}
// 回收 Request 对象的内部信息
session = null;
requestedSessionCookie = false;
requestedSessionId = null;
requestedSessionURL = false;
requestedSessionSSL = false;
}
org.apache.catalina.session.StandardSession.java.endAccess()方法如下:
public void endAccess() {
isNew = false;
if (LAST_ACCESS_AT_START) { // 可以通过系统参数改变该值,默认为false
this.lastAccessedTime = this.thisAccessedTime;
this.thisAccessedTime = System.currentTimeMillis();
} else {
this.thisAccessedTime = System.currentTimeMillis();
this.lastAccessedTime = this.thisAccessedTime;
}
}
public void access() {
this.thisAccessedTime = System.currentTimeMillis();
}
参考文章
tomcat-9.0.60-src源码解析
Tomcat架构解析
Tomcat源码分析(十)- 彻底理解 Session机制