Tomcat session相关的类图
通过上图,可以看出每一个StandardContext
会关联一个Manager,默认情况下Manager的实现类是StandardManager
,而StandardManager
内部会聚合多个Session,其中StandardSession
是Session的默认实现类,当我们调用Request#getSession()
的时候,Tomcat通过StandardSessionFacade
这个外观类将StandardSession
包装以后返回。
org.apache.catalina.connector.Request#getSession()1
2
3
4
5
6
7
8
9
10
11
12
13/**
* Return the session associated with this Request, creating one
* if necessary.
*/
@Override
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()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86protected 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的代码:首先从
StandardContext
中获取对应的Manager对象,缺省情况下,这个地方获取的其实就是StandardManager
的实例。 - 标注2的代码:从Manager中根据requestedSessionId获取session,如果session已经失效,则将session置为null以便下面创建新的session,如果session不为空则通过调用
session#access()
标注session的访问时间,然后返回。 - 标注3的代码:判断传递的参数,如果为false,则直接返回空,这其实就是对应的
Request#getSession(true/false)
的情况,当传递false的时候,如果不存在session,则直接返回空,不会新建。 - 标注4的代码:调用Manager来创建一个新的session,这里默认会调用到
StandardManager()
,而StandardManager
继承ManagerBase
,那么默认其实是调用ManagerBase()
。 - 标注5的代码:创建一个Cookie,而Cookie的名称就是大家熟悉的JSESSIONID,另外JSESSIONID其实也是可以配置的,这个可以通过context节点的sessionCookieName来修改。
通过doGetSession()
获取到Session以后,调用session#getSession()
,而Session的实现类是StandardSession
,那么再来看下StandardSession#getSession()
。
org.apache.catalina.session.StandardSession#getSession()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* Return the <code>HttpSession</code> for which this object
* is the facade.
*/
@Override
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启动过程中,在容器启动以后会启动一个ContainerBackgroundProcessor
线程,这个线程是在Container
启动的时候启动的,这条线程就通过后台周期性的调用org.apache.catalina.core.ContainerBase#backgroundProcess()
(ContainerBase实现Container接口)。
org.apache.catalina.core.ContainerBase#threadStart()thread = new Thread(new ContainerBackgroundProcessor(), threadName);
org.apache.catalina.core.ContainerBase#ContainerBackgroundProcessor1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47/**
* Private thread class to invoke the backgroundProcess method
* of this container and its children after a fixed delay.
*/
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
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(); //ContainerBase实现Container接口
} catch (Throwable t) {
ExceptionUtils.handleThrowable(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);
}
}
}
}
org.apache.catalina.core.ContainerBase#backgroundProcess()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51/**
* Execute a periodic task, such as reloading, etc. This method will be
* invoked inside the classloading context of this container. Unexpected
* throwables will be caught and logged.
*/
@Override
public void backgroundProcess() {
if (!getState().isAvailable())
return;
if (cluster != null) {
try {
cluster.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);
}
}
if (loader != null) {
try {
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);
}
}
if (manager != null) {
try {
manager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);
}
}
Realm realm = getRealmInternal();
if (realm != null) {
try {
realm.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);
}
}
Valve current = pipeline.getFirst();
while (current != null) {
try {
current.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);
}
current = current.getNext();
}
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
backgroundProcess()
最终又会调用org.apache.catalina.session.ManagerBase#backgroundProcess
。
org.apache.catalina.session.ManagerBase#backgroundProcess()1
2
3
4
5
6
7
8
9/**
* Implements the Manager interface, direct call to processExpires
*/
@Override
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
默认情况下backgroundProcess
是每10秒运行一次(StandardEngine
构造的时候,将backgroundProcessorDelay
设置为10),而这里通过processExpiresFrequency
来控制频率,例如processExpiresFrequency
的值默认为6,那么相当于每一分钟运行一次processExpires()
。
org.apache.catalina.session.ManagerBase#processExpires()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* Invalidate all sessions that have expired.
*/
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()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33/**
* Return the <code>isValid</code> flag for this session.
*/
@Override
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。
org.apache.catalina.session.StandardSession#expire()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125/**
* Perform the internal processing required to invalidate this session,
* without triggering an exception if the session has already expired.
*
* @param notify Should we notify listeners about the demise of
* this session?
*/
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);
// 3
// Remove this session from our manager's active sessions
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:标记当前的session为超期。
- 标注2:出发HttpSessionListener监听器的方法。
- 标注3:从Manager里面移除当前的session。
- 标注4:将session中保存的属性移除。
到这里已经清楚Tomcat中对与StandardSession
的创建以及销毁的过程,其实StandardSession
仅仅是实现内存中Session的存储,而Tomcat还支持将Session持久化,以及Session集群节点间的同步。