Tomcat的Session管理(一)


摘要:本文目的在介紹tomcat中session相關的架構以及session的查詢。

在Servlet開發中,Session代表用戶會話,開發人員經常使用Session來臨時存儲一些信息,那么Session到底是什么,Tomcat中是如何對Session進行管理的,我們今天到源碼中查看下。

查看相關資料,我們先看下Session相關的類圖

從圖上可以看到Session對應的接口有兩個SessionHttpSessionStandardSession實現了這2個接口,StandardSessionFacade實現了HttpSessionStandardSessionFacede包含了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

PersistentManagerBaseBackupManager類中抽象出來的一個接口,這個接口表示了兩者一個共同的屬性:不會持有所有session的對象,但是能找到所有的session。例如PersistentManagerBase可以將session同時存放在內存和持久化介質中。

ClusterManager

分布式集群session處理器父類,定義了一些基本方法例如獲取所有tomcat集群的機器,獲取其他集群的信息等基本功能。

ClusterManagerBase

抽象類,對ClusterManager作了基本實現。

BackupManager

集群間session復制策略的一種實現,會話數據只有一個備份節點,這個備份節點的位置集群中所有節點都可見。

DeltaManager

集群建session復制策略的一種實現,采用的方式是只復制差異部分,是分布式集群session同步中最好的同步方式。

了解一些基本信息后,我們來查看我們最常用的getSession()方法,從而來了解session運作的方式。

在Servlet中我們使用HttpServletRequestgetSession()方法來獲取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,所以查看StandardManagerfindSession()方法,最后在其父類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請求的ConnectorHttp11Processor。首先用戶發送一個http請求傳遞給Http11Processor,經由Http11Processor解析封裝在 org.apache.coyote.Request然后傳遞給CoyoteAdaptercoyoteAdapter是一個適配器,將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類圖的時候描述的一致。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM