Tomcat Session管理机制(Tomcat源代码解析七)
前面几篇我们分析了Tomcat的启动。关闭,请求处理的流程,tomcat的classloader机制,本篇将接着分析Tomcat的session管理方面的内容。
在開始之前。我们先来看一下整体上的结构,熟悉了整体结构以后,我们在一步步的去分析源码。Tomcat session相光的类图例如以下:
通过上图,我们能够看出每个StandardContext会关联一个Manager,默认情况下Manager的实现类是StandardManager,而StandardManager内部会聚合多个Session,当中StandardSession是Session的默认实现类,当我们调用Request.getSession的时候,Tomcat通过StandardSessionFacade这个外观类将StandardSession包装以后返回。
上面清楚了整体的结构以后,我们来进一步的通过源码来分析一下。咋们首先从Request的getSession方法看起。
org.apache.catalina.connector.Request#getSession
public HttpSession getSession() { Session session = doGetSession(true); if (session == null) { return null; } return session.getSession(); }从上面的代码。我们能够看出首先首先调用doGetSession方法获取Session,然后再调用Session的getSession方法返回HttpSession,那接下来我们再来看看doGetSession方法:
org.apache.catalina.connector.Request#doGetSession 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 // 1 Manager manager = null; if (context != null) { manager = context.getManager(); } if (manager == null) { return (null); // Sessions are not supported } // 2 if (requestedSessionId != null) { try { session = manager.findSession(requestedSessionId); } catch (IOException e) { session = null; } if ((session != null) && !session.isValid()) { session = null; } if (session != null) { session.access(); return (session); } } // Create a new session if requested and the response is not committed // 3 if (!create) { return (null); } if ((context != null) && (response != null) && context.getServletContext().getEffectiveSessionTrackingModes(). contains(SessionTrackingMode.COOKIE) && response.getResponse().isCommitted()) { throw new IllegalStateException (sm.getString("coyoteRequest.sessionCreateCommitted")); } // Attempt to reuse session id if one was submitted in a cookie // Do not reuse the session id if it is from a URL, to prevent possible // phishing attacks // Use the SSL session ID if one is present. // 4 if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) { session = manager.createSession(getRequestedSessionId()); } else { session = manager.createSession(null); } // Creating a new session cookie based on that session if ((session != null) && (getContext() != null) && getContext().getServletContext(). getEffectiveSessionTrackingModes().contains( SessionTrackingMode.COOKIE)) { // 5 Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie( context, session.getIdInternal(), isSecure()); response.addSessionCookieInternal(cookie); } if (session == null) { return null; } session.access(); return session; }
以下我们就来重点分析一下,上面代码中标注了数字的地方:
- 标注1(第17行)首先从StandardContext中获取相应的Manager对象,缺省情况下,这个地方获取的事实上就是StandardManager的实例。
- 标注2(第26行)从Manager中依据requestedSessionId获取session,假设session已经失效了。则将session置为null以便以下创建新的session,假设session不为空则通过调用session的access方法标注session的訪问时间,然后返回。
- 标注3(第43行)推断传递的參数。假设为false,则直接返回空。这事实上就是相应的Request.getSession(true/false)的情况,当传递false的时候,假设不存在session,则直接返回空,不会新建。
- 标注4 (第59行)调用Manager来创建一个新的session,这里默认会调用到StandardManager的方法。而StandardManager继承了ManagerBase,那么默认事实上是调用了了ManagerBase的方法。
- 标注5 (第72行)创建了一个Cookie,而Cookie的名称就是大家熟悉的JSESSIONID,另外JSESSIONID事实上也是能够配置的,这个能够通过context节点的sessionCookieName来改动。比方….
通过doGetSession获取到Session了以后,我们发现调用了session.getSession方法,而Session的实现类是StandardSession,那么我们再来看下StandardSession的getSession方法。
org.apache.catalina.session.StandardSession#getSession public HttpSession getSession() { if (facade == null){ if (SecurityUtil.isPackageProtectionEnabled()){ final StandardSession fsession = this; facade = AccessController.doPrivileged( new PrivilegedAction<StandardSessionFacade>(){ @Override public StandardSessionFacade run(){ return new StandardSessionFacade(fsession); } }); } else { facade = new StandardSessionFacade(this); } } return (facade); }
通过上面的代码。我们能够看到通过StandardSessionFacade的包装类将StandardSession包装以后返回。
到这里我想大家应该熟悉了Session创建的整个流程。
接着我们再来看看,Sesssion是怎样被销毁的。
我们在Tomcat启动过程(Tomcat源码阅读系列之三)中之处,在容器启动以后会启动一个ContainerBackgroundProcessor线程,这个线程是在Container启动的时候启动的。这条线程就通过后台周期性的调用org.apache.catalina.core.ContainerBase#backgroundProcess。而backgroundProcess方法终于又会调用org.apache.catalina.session.ManagerBase#backgroundProcess,接下来我们就来看看Manger的backgroundProcess方法。
org.apache.catalina.session.ManagerBase#backgroundProcess public void backgroundProcess() { count = (count + 1) % processExpiresFrequency; if (count == 0) processExpires(); }上面的代码里。须要注意一下,默认情况下backgroundProcess是每10秒执行一次(StandardEngine构造的时候,将backgroundProcessorDelay设置为了10),而这里我们通过processExpiresFrequency来控制频率,比如processExpiresFrequency的值默觉得6,那么相当于没一分钟执行一次processExpires方法。接下来我们再来看看processExpires。
org.apache.catalina.session.ManagerBase#processExpires public void processExpires() { long timeNow = System.currentTimeMillis(); Session sessions[] = findSessions(); int expireHere = 0 ; if(log.isDebugEnabled()) log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); for (int i = 0; i < sessions.length; i++) { if (sessions[i]!=null && !sessions[i].isValid()) { expireHere++; } } long timeEnd = System.currentTimeMillis(); if(log.isDebugEnabled()) log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere); processingTime += ( timeEnd - timeNow ); }上面的代码比較简单,首先查找出当前context的全部的session,然后调用session的isValid方法,接下来我们在看看Session的isValid方法。
org.apache.catalina.session.StandardSession#isValid public boolean isValid() { if (this.expiring) { return true; } if (!this.isValid) { return false; } 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); }
查看上面的代码。主要就是通过对照当前时间和上次訪问的时间差是否大于了最大的非活动时间间隔,假设大于就会调用expire(true)方法对session进行超期处理。这里须要注意一点,默认情况下LAST_ACCESS_AT_START为false,读者也能够通过设置系统属性的方式进行改动,而假设採用LAST_ACCESS_AT_START的时候,那么请求本身的处理时间将不算在内。比方一个请求处理開始的时候是10:00,请求处理花了1分钟,那么假设LAST_ACCESS_AT_START为true,则算是否超期的时候,是从10:00算起,而不是10:01。
接下来我们再来看看expire方法,代码例如以下:
org.apache.catalina.session.StandardSession#expire public void expire(boolean notify) { // Check to see if expire is in progress or has previously been called if (expiring || !isValid) return; synchronized (this) { // Check again, now we are inside the sync so this code only runs once // Double check locking - expiring and isValid need to be volatile if (expiring || !isValid) return; if (manager == null) return; // Mark this session as "being expired" // 1 expiring = true; // Notify interested application event listeners // FIXME - Assumes we call listeners in reverse order Context context = (Context) manager.getContainer(); // 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 ClassLoader oldTccl = null; if (context.getLoader() != null && context.getLoader().getClassLoader() != null) { oldTccl = Thread.currentThread().getContextClassLoader(); if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl( context.getLoader().getClassLoader()); AccessController.doPrivileged(pa); } else { Thread.currentThread().setContextClassLoader( context.getLoader().getClassLoader()); } } try { // 2 Object listeners[] = context.getApplicationLifecycleListeners(); if (notify && (listeners != null)) { HttpSessionEvent event = new HttpSessionEvent(getSession()); for (int i = 0; i < listeners.length; i++) { int j = (listeners.length - 1) - i; if (!(listeners[j] instanceof HttpSessionListener)) continue; HttpSessionListener listener = (HttpSessionListener) listeners[j]; try { 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.getContainer().getLogger().error (sm.getString("standardSession.sessionEvent"), t); } } } } finally { if (oldTccl != null) { if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl(oldTccl); AccessController.doPrivileged(pa); } else { Thread.currentThread().setContextClassLoader(oldTccl); } } } if (ACTIVITY_CHECK) { accessCount.set(0); } setValid(false); // Remove this session from our manager's active sessions // 3 manager.remove(this, true); // Notify interested session event listeners if (notify) { fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null); } // Call the logout method if (principal instanceof GenericPrincipal) { GenericPrincipal gp = (GenericPrincipal) principal; try { gp.logout(); } catch (Exception e) { manager.getContainer().getLogger().error( sm.getString("standardSession.logoutfail"), e); } } // We have completed expire of this session expiring = false; // Unbind any objects associated with this session // 4 String keys[] = keys(); for (int i = 0; i < keys.length; i++) removeAttributeInternal(keys[i], notify); } }
上面代码的主流程我已经标注了数字。我们来逐一分析一下:
- 标注1(第18行)标记当前的session为超期
- 标注2(第41行)出发HttpSessionListener监听器的方法。
- 标注3(第89行)从Manager里面移除当前的session
- 标注4(第113行)将session中保存的属性移除。
到这里我们已经清楚了Tomcat中对与StandardSession的创建以及销毁的过程,事实上StandardSession不过实现了内存中Session的存储,而Tomcat还支持将Session持久化。以及Session集群节点间的同步。这些内容我们以后再来分析。