摘要:本文目的在介紹tomcat中session相關的架構以及session的查詢。
在Servlet開發中,Session代表用戶會話,開發人員經常使用Session來臨時存儲一些信息,那么Session到底是什么,Tomcat中是如何對Session進行管理的,我們今天到源碼中查看下。
查看相關資料,我們先看下Session
相關的類圖
從圖上可以看到Session對應的接口有兩個Session
和HttpSession
,StandardSession
實現了這2個接口,StandardSessionFacade
實現了HttpSession
,StandardSessionFacede
包含了StandardSession
。看起來有點饒,我們詳細講解下每個類的含義。
- HttpSession
我們都知道Tomcat是支持Servlet規范的web容器,所以內部會包含一些Servlet的內容。HttpSession
就是在Servlet規范中標准的Session定義。注意HttpSession
的全路徑是javax.servlet.http.HttpSession
- Session
Session
接口是Catalina內部的Session的標准定義。全路徑org.apache.catalina.Session
- StandardSession
Catalina內部Session
接口的標准實現,基礎功能的提供類,需要重點分析,全路徑org.apache.catalina.session.StandardSession
- StandardSessionFacade
StandardSession
的外觀類,也是Catalina內部的類,之所以存在這個類是因為不想直接讓開發人員操作到StandardSession
,這樣可以保證類的安全。該類的全路徑org.apache.catalina.session.StandardSession
。
在Catalina中,Session由Session管理器組件負責管理,例如創建和銷毀Session管理器是org.apache.catalina.Manager
接口的實例。我們來看看Manager
管理器相關的類圖。
Manager
Catalina中session管理器概念的頂層接口。其中定義了一些對session的基本操作,如創建,查找,添加,移除以及對特定session對象的屬性的修改。除了基本操作以外還定義了一些特殊的例如session的序列化反序列化等等。
ManagerBase
抽象類,對Manager
接口作了基本實現,方便繼承類的復寫以及實現,就像其他xxxBase
類起到了一樣的作用。
StandardManager
Catalina中默認的Session管理器的實現類,具體功能我們下面分析。
PersistentManagerBase
Session管理器中打標持久化Session的父類,雖然StandardManager
也可以將Session持久化,但是只是將Session持久化為一個文件(后面會說到),PersistentManagerBase
類和StandardManager
類的區別在於前者的存儲器的表現形式可以有多種,比如數據庫,文件等(具體后面討論)
PersistentManager
在PersistentManager
基礎上增加了兩個屬性。
DistributedManager
從PersistentManagerBase
和BackupManager
類中抽象出來的一個接口,這個接口表示了兩者一個共同的屬性:不會持有所有session的對象,但是能找到所有的session。例如PersistentManagerBase
可以將session同時存放在內存和持久化介質中。
ClusterManager
分布式集群session處理器父類,定義了一些基本方法例如獲取所有tomcat集群的機器,獲取其他集群的信息等基本功能。
ClusterManagerBase
抽象類,對ClusterManager
作了基本實現。
BackupManager
集群間session復制策略的一種實現,會話數據只有一個備份節點,這個備份節點的位置集群中所有節點都可見。
DeltaManager
集群建session復制策略的一種實現,采用的方式是只復制差異部分,是分布式集群session同步中最好的同步方式。
了解一些基本信息后,我們來查看我們最常用的getSession()
方法,從而來了解session運作的方式。
在Servlet中我們使用HttpServletRequest
的getSession()
方法來獲取session對象,而真正執行getSession
方法的其實是org.apache.catalina.connector.RequestFacade
對象
public class RequestFacade implements HttpServletRequest
RequestFacade
對象實現了HttpServletRequest
內部封裝了org.apache.catalina.connector.Request
對象,我們查看getSession()
方法:
@Override
public HttpSession getSession() {
if (request == null) {
throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
}
return getSession(true);
}
@Override
public HttpSession getSession(boolean create) {
if (request == null) {
throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
}
if (SecurityUtil.isPackageProtectionEnabled()){
return AccessController.doPrivileged(new GetSessionPrivilegedAction(create));
} else {
return request.getSession(create);
}
}
可以看出最終調用的還是org.apache.catalina.connector.Request
對象的getSession()
方法,源碼如下:
@Override
public HttpSession getSession(boolean create) {
Session session = doGetSession(create);
if (session == null) {
return null;
}
return session.getSession();
}
下面我們看doGetSession()
方法,由於源碼過長,刪減了部分內容,保留了核心代碼。
protected Session doGetSession(boolean create) {
//判斷context
if (context == null) {
return (null);
}
//判斷session是否有效
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return (session);
}
//獲取跟context綁定的sessionManager 這里默認返回的是StandardManager
Manager manager = context.getManager();
if (manager == null) {
return null; // Sessions are not supported
}
if (requestedSessionId != null) {
try {
//根據requestSessionId 查詢指定的session
//111111111111
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
//判斷獲取到的session是否有效
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
//session有效 增加訪問時間和次數
session.access();
return (session);
}
}
//如果沒有找到已存在的session並且 要求創建新session
//獲取此次request對應的sessionid
String sessionId = getRequestedSessionId();
//。。。略部分代碼
//222222222222
session = manager.createSession(sessionId);
//創建cookie
Cookie cookie =ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(),isSecure());
//將cookie設置到response 中
response.addSessionCookieInternal(cookie);
if (session == null) {
return null;
}
//增加訪問時間和次數
session.access();
return session;
}
代碼很簡單,思路也很清晰,很容易理解,這里有3處需要關注下。
在代碼1的地方,調用了manager.findSession()
,requestedSessionId
是個String類型的字符串(稍后說如何創建一個這樣的字符串)。在Catalina中默認的是StandardManager
,所以查看StandardManager
的findSession()
方法,最后在其父類ManagerBase
中找到相關方法:
@Override
public Session findSession(String id) throws IOException {
if (id == null) {
return null;
}
return sessions.get(id);
}
/**
* The set of currently active Sessions for this Manager, keyed by
* session identifier.
*/
protected Map<String, Session> sessions = new ConcurrentHashMap<String, Session>();
從源碼可以得出結論,一個類型的session管理器,他都有一個總的session容器,就是一個ConcurrentHashMap
實例,key是sessionId
,value是對應的session對象。
在代碼2的地方由於在已有的session的map中沒有找到session,所以需要創建一個新的session,createSession()
源碼如下(在ManagerBase
類中):
@Override
public Session createSession(String sessionId) {
//判斷可容納session數量
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
//創建一個全新的session對象
Session session = createEmptySession();
//設置基本屬性
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
String id = sessionId;
//設置sessionId 唯一標識負
if (id == null) {
//如果id為空 新生成一個sessionId
id = generateSessionId();
}
session.setId(id);
//session數量+1
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);
}
需要關注下創建新的session對象其實就是new 了一個StandardSession
對象,還有generateSessionId()
方法是生成一個唯一的sessionId,有興趣的可以自行查看。最后在代碼session.setId(id)
中往下查看可以看到:
sessions.put(session.getIdInternal(), session);
也就是把新創建的session放入到管理器的session容器中(ConcurrentHashMap)對象。
在doGetSession
方法中最后需要關注的就是requestedSessionId
是如何生成的,我們先看下定義
/**
* The requested session ID (if any) for this request.
*/
protected String requestedSessionId = null;
可以看出是個字符串類型的標識符並且和request有關,由於tomcat的請求流程我們還沒梳理到,所以這里直接給出生成的方法。
首先,這個所謂的requestedSessionId
對應的就是我們每次請求都有一個唯一session標識符,為了以后同一個用戶再次請求的時候可以使用同一個session(會話)。既然跟request有關,那么就可以查看request相關方法,而之前在講解啟動的時候提到過Connector
是專門來處理請求的,而對應http請求的Connector
是Http11Processor
。首先用戶發送一個http請求傳遞給Http11Processor
,經由Http11Processor
解析封裝在 org.apache.coyote.Request
然后傳遞給CoyoteAdapter
,coyoteAdapter
是一個適配器,將coyote框 架封裝的org.apache.coyote.Request
適配給org.apache.catalina.connector.Request
,而解析的過程就在CoyoteAdapter
中。
解析requestSessionId
分為兩步,一個是從請求url中解析,另一步是從cookie中解析,之所以需要解析兩次是因為有的用戶會禁用cookie,而禁用cookie的時候sessionId則會帶在請求url中(其實就是我們熟知的jsessionId
)
從請求url中解析(只保留了核心代碼)
首先如果sessionId保留在url中是以如下形式
http://xxx.com?a=1&b=1&c=1;k1=v1;k2=v2;jsessionId=xxxx
可以看出requestSessionId
是保存在;
之后的,之所以解釋下 是方便理解。
protected void parsePathParameters(org.apache.coyote.Request req,
Request request) {
//轉碼uri
req.decodedURI().toBytes();
ByteChunk uriBC = req.decodedURI().getByteChunk();
//查詢;位置
int semicolon = uriBC.indexOf(';', 0);
//設置編碼
String enc = connector.getURIEncoding();
if (enc == null) {
enc = "ISO-8859-1";
}
Charset charset = null;
try {
charset = B2CConverter.getCharset(enc);
} catch (UnsupportedEncodingException e1) {
log.warn(sm.getString("coyoteAdapter.parsePathParam",
enc));
}
//省略部分代碼
//循環遍歷,設置所有的kv
while (semicolon > -1) {
// 11111111111111
int start = uriBC.getStart();
int end = uriBC.getEnd();
int pathParamStart = semicolon + 1;
int pathParamEnd = ByteChunk.findBytes(uriBC.getBuffer(),
start + pathParamStart, end,
new byte[] {';', '/'});
String pv = null;
if (pathParamEnd >= 0) {
if (charset != null) {
pv = new String(uriBC.getBuffer(), start + pathParamStart,
pathParamEnd - pathParamStart, charset);
}
// Extract path param from decoded request URI
byte[] buf = uriBC.getBuffer();
for (int i = 0; i < end - start - pathParamEnd; i++) {
buf[start + semicolon + i]
= buf[start + i + pathParamEnd];
}
uriBC.setBytes(buf, start,
end - start - pathParamEnd + semicolon);
} else {
if (charset != null) {
pv = new String(uriBC.getBuffer(), start + pathParamStart,
(end - start) - pathParamStart, charset);
}
uriBC.setEnd(start + semicolon);
}
//22222222222
if (pv != null) {
int equals = pv.indexOf('=');
if (equals > -1) {
String name = pv.substring(0, equals);
String value = pv.substring(equals + 1);
request.addPathParameter(name, value);
}
}
semicolon = uriBC.indexOf(';', semicolon);
}
}
代碼還算容易理解,比較清楚的都標注在代碼中了,從標注1到標注2的地方主要做的就是獲取一對kv結構(k1=v1
這樣的結構)。而在代碼2的判斷的地方可以看到很明顯request.addPathParameter(name, value);
,request把kv都設置進去了,這其中就包括了jsessionId
,以上就是在解析url的時候第一次設置requestSessionId
。
從cookie中解析
protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {
//如果cookie被禁用 直接返回
Context context = (Context) request.getMappingData().context;
if (context != null && !context.getServletContext()
.getEffectiveSessionTrackingModes().contains(
SessionTrackingMode.COOKIE)) {
return;
}
//獲取cookie
Cookies serverCookies = req.getCookies();
int count = serverCookies.getCookieCount();
if (count <= 0) {
return;
}
//獲取cookie中session的key
String sessionCookieName = SessionConfig.getSessionCookieName(context);
for (int i = 0; i < count; i++) {
ServerCookie scookie = serverCookies.getCookie(i);
//
if (scookie.getName().equals(sessionCookieName)) {
// Override anything requested in the URL
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
convertMB(scookie.getValue());
//設置requestSessionId
request.setRequestedSessionId(scookie.getValue().toString());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
} else {
if (!request.isRequestedSessionIdValid()) {
// Replace the session id until one is valid
convertMB(scookie.getValue());
request.setRequestedSessionId
(scookie.getValue().toString());
}
}
}
}
}
代碼比較簡單,以上就是requestSessionId
是如何設置的。
看到這里,doGetSession()
方法我們就看完了,可以看到它返回的是一個StandardSession
對象,最后在getSession()
方法中返回的是StandardSession
對象的getSession()
方法,查看下源碼。
@Override
public HttpSession getSession() {
Session session = doGetSession(true);
if (session == null) {
return null;
}
return session.getSession();
}
/**
* 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);
}
可以看出最后getSession()
方法返回的並不是StandardSession
對象,而是StandardSession
對象的外觀類StandardSessionFacade
。這也和我們一開始介紹session類圖的時候描述的一致。