本文是Tomcat源码阅读系列的第七篇文章,本系列前六篇文章如下:
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码
Tomcat源码阅读系列(二)Tomcat总体架构
Tomcat源码阅读系列(三)启动和关闭过程
Tomcat源码阅读系列(四)Connector连接器
Tomcat源码阅读系列(五)Catalina容器
Tomcat源码阅读系列(六)类加载器
本文首先介绍Session管理的主要架构和相关类及其功能,然后介绍Session的创建以及销毁过程。
Session管理是JavaEE容器比较重要的一部分,在app中也经常会用到。在开发app时,我们只是获取一个session,然后向session中存取数据,然后再销毁session。那么如何产生session,以及session池如何维护及管理,这些并没有在app涉及到,这些Session的管理工作都是由容器来完成的。Session管理器的类继承结构图如下,
对应Session的定义说明如下
1. Session:是作为catalina的外观类使用的。session对象总是存在于manager组件中,getManager/setManager方法用于设置session和manager的关联。对某个session实例来说,在与其关联的manager内,该session有唯一标识,该标识可通过setId/getId方法来访问。manager调用getLastAccessedTime方法来决定某个session对象的有效性。manager调用setValid方法将某个session对象标识为有效。每次访问session对象时,它的调用方法都会修改该session对象的最后访问时间。最后,manager可以通过调用expire方法将某个session对象标识为过期,也可以通过getSession方法获取一个经过session外观类包装过的HttpSession对象。
2. StandardSession:StandardSession是catalina中Session接口的标准实现。除了实现javax.servlet.http.HttpSession接口和org.apache.catalina.Session接口外,StandardSession类还实现了java.lang.Serializable接口。StandardSession的构造函数接收一个Manager类的实例,迫使一个session实例必须属于一个manager实例。StandardSession是Session的默认实现类。
3. StandardSessionFacade:为了传递一个session对象给servlet,catalina会实例化一个Session对象,填充session对象内容,然后再传给servlet。但是,实际上,catalina传递的是session的外观类StandardSessionFacade的实例,该类仅仅实现了javax.servlet.http.HttpSession接口。这就,servlet开发人员就不能将HttpSession对象向下转换为StandardSessionFacade类型,也就不会暴露出StandardSessionFacade所有的公共接口了。当我们调用Request.getSession的时候,Tomcat通过StandardSessionFacade这个外观类将StandardSession包装以后返回。为了就是保护其中除HttpSession之外的公用方法。
对应Manager的定义说明如下
其他说明
1. StandardManager内部会聚合多个Session
2. StandardContext内部包含Manager
3. PersistentManagerBase类中有个成员变量Store,持久化session管理器的存储策略就是有这个Store对象定义的,接口Store及其实例是为session管理器提供了一套存储策略,store定义了基本的接口,而StoreBase提供了基本的实现。其中FileStore类实现的策略是将session存储在以setDirectory()指定目录并以.session结尾的文件中的。JDBCStore类是将Session通过JDBC存入数据库中,因此需要使用JDBCStore,需要分别调用setDriverName()方法和setConnectionURL()方法来设置驱动程序名称和连接URL。
文章Tomcat源码阅读系列(五)Catalina容器中说明了HTTP请求在CoyoteAdapter#service会调用connector.getContainer().getPipeline().getFirst().invoke(request, response);将请求由Coyote交给Catalina处理之前会做很多操作,比如postParseRequest对Request参数进行解析,转换完之后会调用parsePathParameters方法去解析路径参数中的cookie信息,先尝试从url中尝试解析出sessionId,然后会调用parseSessionCookiesId,这个就是从cookie中解析sessionId存到request,解析到sessionId就放到了request里面,解析SessionId的逻辑就ok了。
parsePathParameters的主要代码如下:
protected void parsePathParameters(org.apache.coyote.Request req,
Request request) {
// Process in bytes (this is default format so this is normally a NO-OP
req.decodedURI().toBytes();
ByteChunk uriBC = req.decodedURI().getByteChunk();
//主要用来分析表现形式为 http://...../xxx;jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng的SessionId
int semicolon = uriBC.indexOf(';', 0);
boolean warnedEncoding = false;
//同样适用于表现形式为http://...../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng的SessionId
while (semicolon > -1) {
//.................
if (pv != null) {
//将jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng分为jsessionid和对应值
int equals = pv.indexOf('=');
if (equals > -1) {
String name = pv.substring(0, equals);
String value = pv.substring(equals + 1);
//关键步骤,将jsessionid和对应值放到request中
request.addPathParameter(name, value);
if (log.isDebugEnabled()) {
log.debug(sm.getString("coyoteAdapter.debug", "equals",
String.valueOf(equals)));
log.debug(sm.getString("coyoteAdapter.debug", "name",
name));
log.debug(sm.getString("coyoteAdapter.debug", "value",
value));
}
}
}
semicolon = uriBC.indexOf(';', semicolon);
}
}
//从request中取出SessionId信息
String sessionID =
request.getPathParameter(Globals.SESSION_PARAMETER_NAME);
if (sessionID != null && !isURLRewritingDisabled(request)) {
//设置RequestedSessionId
request.setRequestedSessionId(sessionID);
request.setRequestedSessionURL(true);
}
需要注意点
当cookie被浏览器禁用时,会将cookie信息进行URL重写,把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为 http://…../xxx;jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng 另一种是作为查询字符串附加在URL后面,表现形式为 http://…../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng ,对于这两种sessionId表现形式,以上代码均能够将jsessionid和对应值放到request中。
调用完parsePathParameters之后,仍然会调用parseSessionCookiesId从cookie中解析出sessionId解析sessionId,parseSessionCookiesId的主要代码如下,
protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {
Context context = (Context) request.getMappingData().context;
//失效判断
if (context != null && !context.getCookies())
return;
// Parse session id from cookies
Cookies serverCookies = req.getCookies();
int count = serverCookies.getCookieCount();
if (count <= 0)
return;
//得到在Cookie中存储的SessionId的名字。
String sessionCookieName = getSessionCookieName(context);
for (int i = 0; i < count; i++) {
ServerCookie scookie = serverCookies.getCookie(i);
//找到sessionId
if (scookie.getName().equals(sessionCookieName)) {
// Override anything requested in the URL
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
convertMB(scookie.getValue());
request.setRequestedSessionId
(scookie.getValue().toString());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
if (log.isDebugEnabled())
log.debug(" Requested cookie session id is " +
request.getRequestedSessionId());
} else {
//设置SessionID信息
if (!request.isRequestedSessionIdValid()) {
// Replace the session id until one is valid
convertMB(scookie.getValue());
request.setRequestedSessionId
(scookie.getValue().toString());
}
}
}
}
}
getSessionCookieName的主要代码如下,
private String getSessionCookieName(Context context) {
String result = null;
//是否在<Context path=”/” docBase=”webapp” reloadable=”false” sessionCookieName=”yoursessionname”></Context>
//中配置sessionCookieName,如果设置,则返回设置的值;如果没有设置,则返回空。
if (context != null) {
result = context.getSessionCookieName();
}
//返回JSESSIONID
if (result == null) {
result = Globals.SESSION_COOKIE_NAME;
}
return result;
}
/** * The name of the cookie used to pass the session identifier back * and forth with the client. * 主要用于在cookie中标识sessionId的key。。大写!!! */
public static final String SESSION_COOKIE_NAME =
System.getProperty("org.apache.catalina.SESSION_COOKIE_NAME",
"JSESSIONID");
/** * The name of the path parameter used to pass the session identifier * back and forth with the client. * 主要在URL参数中标识sessionId的key。。小写!! */
public static final String SESSION_PARAMETER_NAME =
System.getProperty("org.apache.catalina.SESSION_PARAMETER_NAME",
"jsessionid");
需要注意点
parsePathParameters和parseSessionCookiesId方法,在调用过程中两者都执行了,一个用于处理URL重写设置sessionId,另外一个用于处理放到cookie中传递过来的sessionId,两者方式只有用一个才会得到SessionId信息!
Session对象的创建一般是源于这样的一条语句: Session session = request.getSession(false);或者Session session = request.getSession();在Tomcat的实现中,这个request是org.apache.catalina.connector.Request类的包装类org.apache.catalina.connector.RequestFacade的对象(门面模式(外观模式)),它的#getSession()会继续调用#doGetSession(),其中#doGetSession()方法如下
protected Session doGetSession(boolean create) {
//..............
// 首先从StandardContext中获取对应的Manager对象,缺省情况下,这个地方获取的其实就是StandardManager的实例。
Manager manager = null;
if (context != null)
manager = context.getManager();
if (manager == null)
return (null); // Sessions are not supported
//此处的requestedSessionId为解析HTTP请求时获取的,这个requestedSessionId有可能为空,之前可能未曾使用Session。
if (requestedSessionId != null) {
try {
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
//从Manager中根据requestedSessionId获取session,如果session已经失效了,则将session置为null以便下面创建新的
//session,如果session不为空则通过调用session的access方法标注session的访问时间,然后返回。
if ((session != null) && !session.isValid())
session = null;
if (session != null) {
session.access();
return (session);
}
}
//判断传递的参数,如果为false,则直接返回空,这其实就是对应的Request.getSession(true/false)的情况,
//当传递false的时候,如果不存在session,则直接返回空,不会新建。默认情况下调用getSession()为true 创建。
// Create a new session if requested and the response is not committed
if (!create)
return (null);
if ((context != null) && (response != null) &&
context.getCookies() &&
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
//调用Manager来创建一个新的session,这里默认会调用到StandardManager的方法,而StandardManager继承了ManagerBase,
//那么默认其实是调用了了ManagerBase的方法。
if (connector.getEmptySessionPath()
&& isRequestedSessionIdFromCookie()) {
session = manager.createSession(getRequestedSessionId());
} else {
session = manager.createSession(null);
}
// Creating a new session cookie based on that session
if ((session != null) && (getContext() != null)
&& getContext().getCookies()) {
String scName = context.getSessionCookieName();
if (scName == null) {
scName = Globals.SESSION_COOKIE_NAME;
}
//创建了一个Cookie,而Cookie的名称就是大家熟悉的JSESSIONID,另外JSESSIONID其实也是可以配置的,
//这个可以通过context节点的sessionCookieName来修改。
Cookie cookie = new Cookie(scName, session.getIdInternal());
configureSessionCookie(cookie);
response.addSessionCookieInternal(cookie, context.getUseHttpOnly());
}
if (session != null) {
session.access();
return (session);
} else {
return (null);
}
}
通过doGetSession获取到Session了以后,我们发现调用了session.getSession方法,而Session的实现类是StandardSession,那么我们再来看下StandardSession的getSession方法。通过StandardSessionFacade的包装类将StandardSession包装以后返回。
public HttpSession getSession() {
if (facade == null){
if (SecurityUtil.isPackageProtectionEnabled()){
final StandardSession fsession = this;
facade = (StandardSessionFacade)AccessController.doPrivileged(new PrivilegedAction(){
public Object run(){
return new StandardSessionFacade(fsession);
}
});
} else {
facade = new StandardSessionFacade(this);
}
}
return (facade);
}
manager.createSession(实际是ManagerBase.createSession方法),比较奇葩,其调用generateSessionId方法产生SessionId,然后调用session.setId(sessionId);设置Session的Id,这个时候,其实不仅仅是设置了sessionId,同时还将Session放到sessions的Map中。
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);//注意这里,将Session放到sessions当中
if (notify) {
tellNew();
}
}
public void add(Session session) {
sessions.put(session.getIdInternal(), session);//将Session放到sessions当中。比较奇葩的调用方式!
int size = sessions.size();
if( size > maxActive ) {
synchronized(maxActiveUpdateLock) {
if( size > maxActive ) {
maxActive = size;
}
}
}
}
需要注意点
修改TOMCAT 默认的生成SESSION ID的算法和字符长度非常简单,只需修改Server.xml中的 Context 结点下的标签值,比如:
<Manager sessionIdLength="20" pathname="SESSIONS.ser" maxActiveSessions="8000" secureRandomAlgorithm="SHA1PRNG" secureRandomClass="java.security.SecureRandom" maxInactiveInterval="60" />
关于Manager结点的说明可参考:https://tomcat.apache.org/tomcat-6.0-doc/config/manager.html
有两种方式销毁Session,主动式和被动式。
Session创建完之后,不会一直存在,或是主动注销,或是超时清除。即是出于安全考虑也是为了节省内存空间等。例如,常见场景:用户登出系统时,会主动触发注销操作。 主动注销时,是调用标准的HttpSession接口的 public void invalidate();方法。
public void invalidate() {
if (!isValidInternal())
throw new IllegalStateException
(sm.getString("standardSession.invalidate.ise"));
// Cause this session to expire
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"
//标记当前的session为超期
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 {
//出发HttpSessionListener监听器的方法。
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 {
fireContainerEvent(context,
"beforeSessionDestroyed",
listener);
listener.sessionDestroyed(event);
fireContainerEvent(context,
"afterSessionDestroyed",
listener);
} catch (Throwable t) {
try {
fireContainerEvent(context,
"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);
/* * Compute how long this session has been alive, and update * session manager's related properties accordingly */
long timeNow = System.currentTimeMillis();
int timeAlive = (int) ((timeNow - creationTime)/1000);
synchronized (manager) {
if (timeAlive > manager.getSessionMaxAliveTime()) {
manager.setSessionMaxAliveTime(timeAlive);
}
int numExpired = manager.getExpiredSessions();
if (numExpired < Integer.MAX_VALUE) {
numExpired++;
manager.setExpiredSessions(numExpired);
}
int average = manager.getSessionAverageAliveTime();
// Using long, as otherwise (average * numExpired) might overflow
average = (int) (((((long) average) * (numExpired - 1)) + timeAlive)
/ numExpired);
manager.setSessionAverageAliveTime(average);
}
if (manager instanceof ManagerBase) {
ManagerBase mb = (ManagerBase) manager;
SessionTiming timing = new SessionTiming(timeNow, timeAlive);
synchronized (mb.sessionExpirationTiming) {
mb.sessionExpirationTiming.add(timing);
mb.sessionExpirationTiming.poll();
}
}
//从Manager里面移除当前的session
// Remove this session from our manager's active sessions
manager.remove(this);
// 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;
//将session中保存的属性移除。
// Unbind any objects associated with this session
String keys[] = keys();
for (int i = 0; i < keys.length; i++)
removeAttributeInternal(keys[i], notify);
}
}
大多数时候,我们并不主动销毁Session,而是依靠Tomcat自行管理Session。
在容器启动以后会启动一个(StandardContext维度的,即每个StandardContext都会有一个ContainerBackgroundProcessor线程)ContainerBase.ContainerBackgroundProcessor线程,这个线程是在Container启动的时候启动的,这条线程就通过后台周期性的调用org.apache.catalina.core.ContainerBase#backgroundProcess,而backgroundProcess方法最终又会调用org.apache.catalina.session.ManagerBase#backgroundProcess,接下来我们就来看看ContainerBackgroundProcessor。
protected class ContainerBackgroundProcessor implements Runnable {
public void run() {
while (!threadDone) {
try {
//每隔10秒钟处理一次!!!backgroundProcessorDelay在StandardEngine中设置为10!
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
;
}
if (!threadDone) {
Container parent = (Container) getMappingObject();
ClassLoader cl =
Thread.currentThread().getContextClassLoader();
if (parent.getLoader() != null) {
cl = parent.getLoader().getClassLoader();
}
processChildren(parent, cl);
}
}
}
protected void processChildren(Container container, ClassLoader cl) {
try {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
container.backgroundProcess();
} catch (Throwable t) {
log.error("Exception invoking periodic operation: ", t);
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i], cl);
}
}
}
}
Manger的backgroundProcess方法。
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
默认情况下backgroundProcess是每10秒运行一次(StandardEngine构造的时候,将backgroundProcessorDelay设置为了10),而这里我们通过processExpiresFrequency来控制频率,例如processExpiresFrequency的值默认为6,那么相当于每一分钟运行一次processExpires方法。接下来我们再来看看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 );
}
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 = (int) ((timeNow - thisAccessedTime) / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}
return (this.isValid);
}
上面的代码比较简单,首先查找出当前context的所有的session,然后调用session的isValid方法,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。
其他
StandardSession仅仅是实现了内存中Session的存储,而Tomcat还支持将Session持久化,以及Session集群节点间的同步。这些都是其他的一些Manager所处理的,在此不在分析。